Skip to content

Artifacts

An artifact is a published file or folder (a bundle) with a stable reference, scion://artifact/<id>, that works from any runtime broker, in any project the reader can access, and in the web UI. The hub stores the bytes, so a reader never needs access to the publisher’s filesystem or shared directories.

This page covers what is available today: publishing files and folders, versions, fetching, reviews, the web pages, artifact references in messages, share links, grants, expiry and deletion.

Each artifact has numbered versions. A version is an immutable snapshot of the bundle: its files, one entry file (the one the web page opens and get prints), an optional note, who published it, and its kind: publish, or review for a version that carries a reviewer’s marks (see Reviews). Publishing again under the same --key adds a version; the latest one is the artifact’s current version, and scion://artifact/<id>@<seq> names one version for good.

  • An artifact is owned by the agent or user that published it (recorded by stable id, never by name). The owner can always read it.
  • It is homed in a project: an agent’s own project, or the project a user publishes into. Members who may read in that project may read the artifact.
  • Anyone else gets 404 — the same answer as for an artifact that does not exist, so responses do not reveal whether an artifact exists.
  • Deleting the project does not delete its artifacts; the owner keeps access.
  • Deleting the owner does not delete its artifacts either. When the hub finds an artifact’s owner gone (a deleted agent or user), it marks the artifact owner deleted (see Owner deleted); everything else about it stays the same.
  • Only the owner (or a principal the owner gave write access) can add versions. A user with review access may add review versions only; see Grants.

publish, get and versions are available in every CLI mode, including agent mode. share and delete are available in human mode only.

Publishes a file or a folder and prints its reference, the new version and the URL of its web page. An agent’s artifact is homed in the agent’s project; a user’s in the hub project the current checkout is linked to.

$ scion artifact publish design.md --title "Artifact system design" --key design
scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d (v1)
https://hub.example.com/projects/<project-id>/artifacts/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d
$ scion artifact publish design.md --key design --note "round 2"
scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d (v2)
https://hub.example.com/projects/<project-id>/artifacts/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d
$ scion artifact publish ./report --entry index.html --title "Q3 report"
  • --title <title>: Artifact title, set when the artifact is created; a new version under an existing key keeps the title (the CLI prints a note). Default: the entry file’s name.
  • --key <key>: A stable key of your choosing. Publishing again under the same key (same publisher, same project) adds a version to that artifact instead of creating a new one. Files unchanged since the current version are not uploaded again.
  • --note <text>: A note describing the version.
  • --entry <path>: The entry file of a folder, relative to it. Default: index.html, index.md or README.md at the top of the folder, or the only file.
  • --version-of <ref>: Publish the file or folder as the next version of the artifact <ref>, which you own or may write, whether or not it has a key. A single file replaces the current version’s entry file, whatever the local file is named, and the version’s other files are kept unchanged without being uploaded; a folder is the whole new bundle, its entry defaulting to the current version’s. If <ref> names a version (@<seq>) that is no longer current, the CLI stops before uploading. --title and --key do not apply.
  • --review <ref>: Publish the file or folder as a review of the artifact <ref> instead of as a new artifact or version. See Reviews. --title and --key do not apply.

A folder is published as all the regular files under it, keeping their relative paths. Files and folders whose names start with . are left out, and symbolic links inside the folder are refused (the file or folder you name may itself be a link). The CLI sends each file’s SHA-256, and the hub rejects bytes that do not match. Limits (hub settings): 32 MiB per file (artifacts.max_file_bytes), 256 MiB per version (artifacts.max_bundle_bytes) and 200 files per version (artifacts.max_files).

If the entry file is Markdown, the hub fetches the remote images it references while publishing (see Images in Markdown artifacts). An image that could not be fetched does not fail the publish; the CLI prints a warning for it on stderr.

Fetches an artifact. Without --out it writes the entry file to stdout; with --out it writes a single file, or every file of a bundle.

$ scion artifact get scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d > design.md
$ scion artifact get scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d@1 --out ./docs/
$ scion artifact get scion://artifact/7a2b...@2 --out ./report-v2/
  • <ref>: scion://artifact/<id>, scion://artifact/<id>@<seq> for a specific version, or a bare <id>.
  • --out, -o <path>: For a single file, write to this file instead of stdout; if the path is an existing directory, the file is written into it under its own name. For a bundle (several files, or one file inside a folder), the directory to write every file into (created if needed), keeping relative paths.
  • --force: Replace files that already exist under --out. Without it, get refuses to replace an existing file.
  • --clean: Resolve the CriticMarkup marks in every text file by rejecting them all: the text the reviewer started from. See Reviews.
  • --accept: Resolve the marks by accepting them all.
  • --kind <publish|review>: Fetch the newest version of that kind at or before the version <ref> names (the current version for a bare reference). --kind publish skips reviews. The CLI prints the version it used on stderr.

Every file is checked against the size and SHA-256 the hub recorded at publish time (before --clean or --accept is applied) before it is written, to stdout or to disk, and moved into place atomically; a mismatch fails without writing that file. With --out, get writes only plain relative names (none starting with .), refuses to write through a symbolic link below the --out directory, and replaces an existing file only with --force. A path ending in / names a directory. On Linux and macOS these checks are part of each write, except that on a file system without hard links (such as FAT or exFAT) the existing-file check is made just before the write. On Windows all of these checks are made just before each write.

Lists an artifact’s versions, newest first, with kind, publish time, publisher, file count, size and note. The current version is marked *.

$ scion artifact versions scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d
REF KIND PUBLISHED BY FILES SIZE NOTE
* scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d@2 publish 2026-10-06 18:20 UTC agent:<id> 1 4.1 KiB round 2
scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d@1 publish 2026-10-06 17:02 UTC agent:<id> 1 3.9 KiB

Creates a share link for the artifact and prints it, with its expiry and the command that revokes it. Anyone holding the link can open the artifact’s current version in a browser without signing in, until the link expires or is revoked. The link is shown only once; it cannot be printed again. Only the artifact’s owner or a user with an admin grant on it can share it; agents cannot, and the command is available in human mode only. See Share links.

  • --ttl <n>h|<n>d: the link’s lifetime in whole hours or days. Without it the hub’s default applies (7 days unless changed). A lifetime above the hub’s maximum (30 days unless changed) is refused, not shortened. When the artifact itself expires sooner, the link ends with it and the command says so.
  • --list: list the active links instead, each with its id, who created it and when, and when it expires.
  • --revoke <link-id>: revoke one link instead.
$ scion artifact share scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d --ttl 24h
https://hub.example.com/api/v1/artifacts/shared/<token>
Expires 2026-10-07 18:20 UTC. This link is shown only once; anyone holding it can open the artifact.
Revoke it with: scion artifact share scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d --revoke <link-id>

Deletes an artifact after asking for confirmation (--yes skips the prompt and is required when stdin is not a terminal). Its grants and share links end with it, message references to it show as unavailable, and its files are removed after the hub’s grace period. A deleted artifact cannot be restored. The owner, a user with an admin grant on it, or an admin of its home project can delete it; see Deletion. The command is available in human mode only: agents cannot delete artifacts, and the command is not on the agent allow-list.

$ scion artifact delete scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d
Delete artifact scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d and end its grants and share links? (y/N): y
Deleted scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d.

A review is a version of kind review: a complete copy of the artifact whose text carries CriticMarkup marks. The reviewer marks up the current version; the owner reads the review back and publishes the resolved text as the next version (--version-of).

Mark Meaning --clean (reject all) --accept (accept all)
{++text++} Insert removed text
{--text--} Delete text removed
{~~old~>new~~} Substitute old new
{>>text<<} Comment removed removed
{==text==} Highlight text text

Marks do not nest: a mark ends at the first closing token of its kind, and other marks inside it are plain text. A mark with no closing token, or a substitution with no ~>, is plain text. Marks may span lines.

Code is literal text. Text inside Markdown code is shown, kept and compared exactly as written, so a page can show CriticMarkup in code (`{++x++}`) and still be reviewed. This holds for the hub’s projections, the CLI, the web preview and the Review toolbar alike. Code is recognised with these rules, which follow CommonMark for code spans and fenced code blocks:

  • An inline code span opens at a run of backticks and closes at the next run of the same length; a run with no partner is plain text. A backslash before the opening backtick escapes it. Spans pair up within a run of consecutive non-blank lines that are not fence lines: a span may cross line ends, but not a blank line or a fence.
  • A fenced code block opens at a line of three or more backticks or tildes, indented at most three spaces, with an optional info string (a backtick fence’s info string holds no backtick). It closes at a line of the same character, at least as long, followed only by spaces or tabs. A fence with no closing line runs to the end of the document.

A mark may contain code ({++use `make test`++}), and code never contains a mark. Some Markdown structure is read as ordinary text: indented code blocks (four or more spaces), where marks are marks; fences inside block quotes or indented four or more spaces, such as in nested list items (their backtick lines still pair up as code spans, a tilde fence’s do not); HTML tags or autolinks that contain backticks, whose backticks still pair up as code spans; and block boundaries other than blank lines and fences, such as headings, list items and table rows, which do not end a run of lines, so backticks on either side of them still pair up as a code span.

To suggest a change to code, mark around the code: delete the whole span or block and insert the new one ({--`old()`--}{++`new()`++}), substitute it ({~~`old()`~>`new()`~~}), or comment beside it. Around a fenced block, type these marks in the editor, with the opening token at the end of the line before the block and the closing token at the start of the line after it; the Review toolbar does not mark fenced blocks. Text typed inside code is a change to the code itself, so the check below refuses it as an unmarked change.

A document whose text already contains complete CriticMarkup marks outside code is hard to review with marks: rejecting every mark also changes that text, so the check below refuses a review that adds marks while the document’s own marks are left as they are. A review passes when it wraps each of the document’s marks in a deletion ({--{++ins++}--} keeps {++ins++} when every mark is rejected), and a review identical to the document passes. A lone opening token such as {++ stays plain text.

Publishing a review. scion artifact publish <file|folder> --review <ref> publishes a review of the artifact <ref>; any reader with write access to the artifact (its owner, or a principal granted write) may review it, and so may a reader with a review grant (see Grants). A single file reviews the current version’s entry file, whatever the local file is named, and the version’s other files are kept unchanged without being uploaded. A folder is the whole reviewed bundle. If <ref> names a version (@<seq>) that is no longer current, the CLI stops before uploading.

$ scion artifact get scion://artifact/5f1c2d3e-... > plan.md
$ # add marks to plan.md
$ scion artifact publish plan.md --review scion://artifact/5f1c2d3e-...
scion://artifact/5f1c2d3e-... (v3, review)

A review changes only marks. A review is checked against the version it was made against, its base: the client names it when it finalizes the review ({"base": <seq>}; the CLI and the web page send the version they started from), and it must still be the artifact’s current version. For every text file, the review with every mark rejected must equal the base’s text, compared after Unicode NFC normalisation with CRLF and CR line endings read as LF. Normalisation changes the line-ending style only, not whether the file ends with a newline: a review that adds or removes only a final newline does not equal the base and is refused with code unmarked_changes. The base’s text is its content for a publish version, and its content with every mark rejected for a review version, so a review of a review is checked against the same published text. A file identical to the base’s passes. Other files must be identical to the base’s, every file of the base must be present, no file may be added, and the entry file may not change. Otherwise finalize answers 422 with code unmarked_changes and discards the review; the current version does not change. details.files lists up to 20 files, each with its path, a change (modified, added, removed or entry) and, for a modified text file, up to 20 hunks. Each hunk gives the base’s line number (line), the number of lines on each side (parent_lines, clean_lines) and the start of each side (parent, clean), at most 256 bytes each; truncated marks a cut. whitespace_only: true marks a hunk whose sides differ only in spaces, tabs and line breaks, and final_newline (added or removed) is set on the hunk holding the last line when only one of the two texts ends with a newline; both are decided on the whole of each side, not on the quoted start, and are omitted otherwise. details.truncated is true when more files differ. The CLI prints the hunks; in a whitespace-only hunk it shows each tab as →, each trailing space as · and each line’s newline as ⏎, it names a final-newline change, and it prints (no lines) for an empty side.

Reviews and new versions. A finalized review becomes the artifact’s current version, so get returns the marked-up text. Finalize answers 409 with code stale_review and discards the review when its base is no longer the current version, or when a version of kind publish newer than the base is still being published (pending), so a review never becomes current over a version its reviewer did not see; the message then says that a newer version is being published, to be reviewed once it is finalized. A pending review does not block other reviews. Review the current version again. A finalize of a review without a base answers 400 with code base_required.

Notice to the owner. When an agent owns the artifact, the hub sends it a system message (category artifact-review) when someone else’s review is finalized:

A review (v3, by a user) was published on your artifact; it is now the current version.
Artifact: v3 - scion artifact get scion://artifact/5f1c2d3e-...@3
Without the marks: scion artifact get scion://artifact/5f1c2d3e-...@3 --clean
Apply or answer the marks, then publish the result as the next version: scion artifact publish <file> --version-of scion://artifact/5f1c2d3e-...

The message comes from the hub and carries the reference in its text only; whether the owner may read the version is decided when it runs get. The Review pending badge in the artifacts list stays until a version of kind publish is current again.

Reading a review. scion artifact get <ref> --clean returns the base text, --accept the text with every suggestion applied, and --kind publish the newest published version. Over the API, add ?resolve=clean or ?resolve=accept to a file route.

Images in a Markdown artifact are fetched at publish time and served from the hub, so opening an artifact never makes the hub contact another server.

  • Remote images. When a version whose entry file is Markdown is published, the hub fetches each absolute https image the entry references inline (![alt](https://...) and <img src="https://...">) once, and stores it in the version as a file at _remote/<sha256 of the URL>. Images are taken from the first 2 MiB of the entry; if the entry is larger, the response and the CLI add one warning saying that later images were not fetched. Fetched images count toward the version’s file and size limits (max_files, max_bundle_bytes); images past either limit are not fetched, with a warning. If the entry holds more images than the hub reads, a warning says that later images were not fetched. Reference-style images (![alt][label] with the URL in a definition elsewhere) are not fetched yet (ptone/scion#3678); they show as “not fetched” in the preview. Image URLs written in code are fetched as well, though they are not shown. Only PNG, JPEG, GIF and WebP images are kept; SVG and other types are not. An image that cannot be fetched (for example a non-https URL, an address the hub does not fetch from, a timeout, a missing image or another type) is recorded as failed, the publish still succeeds, and the response and the CLI list one warning per such image. Publishing a new version fetches again.
  • Which URLs. An <img src> is read as the browser reads it. A URL is fetched only in a plain form: http or https, a host name (no IP literal in brackets), an optional port, no user name or password, and no spaces or parentheses. Other images are not fetched and show as “not fetched” in the preview.
  • Reading a remote image. GET /api/v1/artifacts/{id}/versions/{seq}/files/_remote/<hash> follows the same access checks as any other file. A fetched image is served with its detected type; a failed one answers 404 with the header X-Artifact-Remote-Status: failed. The version’s file manifest lists each remote image with origin: "remote", its sourceUrl and its fetchStatus (ok or failed).
  • Reserved names. Files may not be published under _remote/.

Settings (in the artifacts section): remote_images_enabled (default true), remote_image_max_count (images fetched per version, default 32; further images are not fetched and get one warning), remote_image_max_bytes (per image, default 5 MiB, at most max_file_bytes), remote_image_fetch_timeout_s (per image, default 10) and remote_image_total_budget_s (all images of one version, default 30, between the fetch timeout and 60 seconds; images are fetched while the publish or finalize request is open, and the hub keeps that request open for the budget plus a margin). remote_image_max_count may not exceed max_files; when the two remote image caps are not set, they follow a lower max_files or max_file_bytes. A settings write with an invalid value, including a remote image cap above the matching file limit or a budget below the fetch timeout, is refused. A stored document with an invalid remote image value turns remote images off and is logged; any other invalid stored value disables the artifact service. A deployment that embeds the artifact service with its own limits and leaves the remote image limits unset gets remote image fetching off.

An artifact whose entry file is HTML (a single page or a small site published as a folder) is shown in a sandboxed frame. The page’s scripts run, but in an isolated origin with no access to the hub page around it, to your session or to the hub’s API.

  • The frame loads the version through a view URL, /api/v1/artifacts/view/<capability>/<entry>, issued to a reader of the artifact and valid for 30 minutes. When server.keyring_file is configured, view URLs are signed by the active generation of the keyring’s signing ring and also stop being accepted at that generation’s verify_until, whichever comes first; once the active generation reaches sign_until, POST .../view and share-link views answer 503 (see Server Configuration). Relative links in the bundle (img/chart.png, css/site.css) resolve under it, so the page loads its own files. The view URL names one version of one artifact and gives access to nothing else; it is not a share link. scion artifact view <ref> prints one from the CLI (in agent mode only with --show-link; without it, the command prints the version and the expiry); with --check it also reports href and src links in the version’s HTML files that name no file of the version (see the CLI reference).
  • Every view response carries a Content-Security-Policy with a sandbox directive, so the page stays isolated even when the URL is opened directly. The page may load scripts, styles, images, fonts and media only from its own files under the view URL; it cannot make network requests from script, embed other frames or plugins, submit forms, open windows or navigate the page around it.
  • Remote images are not loaded in HTML artifacts; include them in the bundle. When an HTML entry references images by absolute http(s) URL, the publish response and the CLI print that warning, and the viewer shows it above the frame.

A share link lets anyone who holds it open an artifact’s current version in a browser, without signing in. Links always expire.

  • Who creates them. The artifact’s owner, a user holding an admin grant on it, or, on an artifact owned by an agent, one of its stewards, while their credential allows artifact.manage in the artifact’s home project. Agents cannot create share links. A caller that cannot read the artifact gets 404, the same as for a missing one; a reader that may not manage links gets 403 with code forbidden. Its message states who may share; an agent gets one that says agents cannot share or change sharing (even of an artifact the agent owns) and which users can.
  • Lifetime. A link lasts link_default_ttl_hours (default 168, seven days) unless the request asks for less or more, up to link_max_ttl_hours (default 720, thirty days). A request for exactly the maximum succeeds; a longer one is rejected with 400 and code ttl_too_long, and no link is created (the lifetime is not shortened to the maximum). Separately, when the artifact itself expires sooner than the link would, the link is cut to end with the artifact and the response says so (clampedToArtifactExpiry). An artifact has at most 50 unexpired links at a time.
  • The token. The link is /api/v1/artifacts/shared/<token>, where the token is 256 random bits. It is shown once, in the response that creates the link. The hub stores only its SHA-256 and never logs it; listing the links shows their ids and expiry, never the token. Keep the link private: anyone holding it can open the artifact until it expires or is revoked.
  • Opening a link. The link answers 303 to a view URL for the current version’s entry, and /api/v1/artifacts/shared/<token>/files/<path> to one for another file of it. Everything a link reaches is served through the view route, with its sandbox and Content-Security-Policy, and every response carries Referrer-Policy: no-referrer. The view URL lasts at most 30 minutes and no longer than the link, and stops working as soon as the link is revoked or expires. The view route serves files as they are: an HTML entry renders in its sandbox, while a Markdown entry opens as plain text.
  • When a link stops working. A revoked or expired link, a link whose artifact was deleted or has expired, and a token that never existed all get the same 404. Share-link reads are rate limited per client address (429 with Retry-After). Revoking a link that has already expired still answers 204 until the next link created on the artifact clears expired links; after that it answers 404.
  • Links and access. A link never makes the artifact readable through the other routes and never adds it to anyone’s list.
  • When the home project is deleted. Deleting a project does not delete its artifacts (see Expiry, moving and deletion), and it does not revoke their share links: a link created before the deletion keeps opening the artifact’s current version, without signing in, until the link expires or is revoked, or the artifact expires. The owner, and users it grants admin, can still end that access:
    • Revoke the links. From the CLI, scion artifact share <ref> --list lists the active links with their ids, and scion artifact share <ref> --revoke <link-id> revokes one. In the web UI, open the artifact from the Artifacts list (its row shows Deleted project), choose Share and Revoke each link. Through the API, DELETE /api/v1/artifacts/{id}/links/{linkId}.
    • Or set the artifact to expire, under Retention in the Share dialog or with PATCH /api/v1/artifacts/{id}. From the moment it expires, its share links stop working.

From the CLI, scion artifact grants <ref> lists an artifact’s grants and adds, changes or removes one (see the CLI reference).

A grant gives someone else access to one artifact. The subject is a person or an agent (user:<id>, agent:<id>) or a project (its members and agents), with one of three permissions: read (open the artifact and its versions), write (also publish new versions) and admin (also manage its grants and share links, change its expiry and move it). The owner holds admin implicitly.

A user (user:<id>), and only a user, can also be given review: publish reviews of the artifact, and nothing else.

  • Review does not give read access. A review grant only counts while its holder can read the artifact some other way: through the home project, or a grant to one of their projects. A person has one grant of their own per artifact, so a review grant cannot sit beside a read, write or admin grant to the same person.
  • It is checked on every request. The hub asks whether the holder can still read each time they start, upload or finalize a review. If that other access ends, the review grant stops working at once: the artifact answers 404, exactly as for a missing one, and nothing needs to be removed. If the access comes back, so does review.
  • What it allows. A review grantee can publish versions of kind review only, through the usual upload and finalize steps, so the marks-only rule and the stale-review check apply. An ordinary version is refused with 403 and code review_only when it is started, when a file is uploaded to it and when it is finalized.
  • What it does not allow. No title or key edits, grants, share links, expiry changes, moves or deletion.

The owner and admin grantees can give review to a user with the grant route; it cannot be given to an agent or a project, including the home project. It cannot replace a read, write or admin grant the user already holds: that request is refused with 400 and code review_replaces_access, and the existing grant stays. The grant route does not check that the user can read the artifact, so a review grant given there to someone without other access does nothing until they gain it.

  • The home project. Every artifact has a read grant for its home project, listed first (home: true). The owner or an admin may raise it to write; it cannot be removed (move the artifact instead).
  • Other projects. A grant to another project can be created, or its permission changed, only while the hub’s messaging.cross_project_messaging_enabled setting is on. The setting is checked only when a grant is created or changed (and when an artifact is moved), not when it is read. Turning the setting off is not retroactive: grants to other projects that already exist stay in place and keep giving access. To end that access, remove those grants one by one; removing a grant works whether the setting is on or off.
  • Who manages grants. The same users who manage share links: the owner or a user with an admin grant, with a credential that allows artifact.manage in the home project, and, on an artifact owned by an agent, its stewards, within their limits. Agents cannot manage grants; they get the same 403 and code as users, with a message written for agents.
  • Review access on artifacts owned by an agent. An agent cannot share its own artifacts, so it cannot choose who reviews its work. Its stewards (the delegating user while they hold a role in the home project, and the home project’s owners) give any grant a steward may give, review included, through the steward rules below, so the review-only path described here serves the callers who are not stewards, such as a home-project admin who is not an owner. For an artifact owned by an agent, two kinds of user may also give a person review access, that is, a review grant to a user (user:<id>) who can already read the artifact through the home project, or a grant to one of their projects: the user who delegated to the agent (the user at the root of the agent’s delegation chain, checked when the request is made), and anyone holding project.manage in the artifact’s home project (its project owners and admins, and super-admins). After the artifact is moved, the project in question is its current home: admins of the previous home lose this, and the delegating user keeps it only while their credential allows artifact.manage in the new home. They need to be able to read the artifact, and their credential must allow artifact.manage in the home project. They can add a review grant for such a user, and nothing else: no read, write or admin grants, no grants to agents or projects, no share links, no changes to expiry and no moves. Nobody else gains this, the owning agent included. The request is refused with the same 403 as any other non-admin request if the caller does not hold this authority (for example, the delegation has been revoked or cannot be checked), if the request is not exactly a review grant to a user, or if the artifact was moved to another project while the request was being handled. A caller who holds the authority learns why a well-formed request was refused: 403 with code review_subject_cannot_read if the person cannot read the artifact through the home project or a grant to one of their projects, and 403 with code review_replaces_access if the person already holds a read, write or admin grant of their own (it is never replaced this way). Every refused review-access request is logged by the hub with its reason, the artifact id and, when the hub checked the request and it named a user, that user’s id. Review access given this way before the review level existed was a write grant; those grants stay as they are. Artifacts owned by users are not affected. On the web, these users give review access with Grant review access on the artifact page (see Web pages), and GET /api/v1/artifacts/{id} reports canGrantReview for them.

An agent cannot share its own artifacts. On an artifact owned by an agent, the people accountable for it, its stewards, may share it within limits (ptone/scion#5144). Stewardship is computed on each request from the artifact’s current home project and owner; it is never stored as a grant.

  • Who. A user whose credential allows artifact.manage in the home project, who also is either:

    • an owner of the home project: holds artifact.steward there, which the built-in project-owner role and super-admins carry (and any custom role an operator gives it); recorded as steward_project_owner; or
    • the delegating user: the user at the root of the owning agent’s live, recorded delegation chain, who also reads in the home project through a role (artifact.read from a role binding; a grant on the artifact does not count); recorded as steward_delegator.
  • Who is not. Project admins who are not owners, members, visitors and hub-admins, unless one of the clauses applies. Agents never are. Artifacts owned by users have no stewards. A home project that no longer exists has no stewards.

  • What a steward may do. Give, change or remove principal and scope grants up to write (and review to a user), lower an admin grant, raise the home project’s grant to write, create, list and revoke share links, list grants, set, change or clear the expiry, and delete the artifact. Grants to other projects follow messaging.cross_project_messaging_enabled as for anyone.

  • What a steward may not do. Give an admin grant (to a person, an agent or a project) or move the artifact. Both answer 403 with code steward_action_not_allowed. An owner or admin grantee still can.

  • When it ends. When the role or delegation behind it ends: the user leaves the home project or loses the owner role, the delegation to the agent is revoked, the artifact moves to a project where the user no longer qualifies (an owner: one they do not own; the delegating user: one where they hold no role). Ownership transfer (ptone/scion#4530, not yet available) will also end it, as an artifact owned by a user has no stewards. A steward who also holds an admin grant acts by the admin grant.

  • Concurrent changes. A steward’s grant, link, removal, expiry change or delete is checked again, under the artifact’s lock, against the home project and owner it was decided on. If the artifact moved or changed owner in the meantime, the request answers 404 and changes nothing.

  • Access tokens. A user access token must reach the home project and carry artifact:manage. The owner clause also needs artifact:steward, and the delegating user clause artifact:read, in the token. Tokens minted without them fail closed.

  • What a grant from a steward is worth. A grant a steward creates stays after the steward loses their role, like any grant; any current owner, admin grantee or steward can remove it.

  • Responses. GET /api/v1/artifacts/{id} reports canShare: true for a steward, and shareAuthority (project_owner or delegator); canManage stays false. The artifact page does not offer Share to stewards yet; they share from the CLI (scion artifact grants, scion artifact share). It offers them Retention… and Delete… (see Web pages).

  • Limits of a grant. A grant never reaches past the caller’s own credential: an agent token without project:artifact:read, or whose delegation is gone, gets 404 even on an artifact granted to it, and a user access token stays within its project boundary. An artifact has at most 100 grants. Grants do not expire on their own; they end when they are removed or when the artifact is deleted.

  • Retention. default_retention_days (default 0, keep until deleted) gives each new artifact an expiry that many days after it is created. The owner or an admin can set, change or clear an artifact’s expiry with PATCH /api/v1/artifacts/{id}; the response counts the share links that will end early (linksCutShort) and the grants that will be removed (grantsRemoved) when it expires. These counts describe what will happen when the artifact expires. The PATCH itself does not remove any grant or share-link row, so listing the grants immediately after the PATCH still shows them. From the moment the artifact expires, every read is refused when it is requested, on every route and through every share link; a share link therefore ends at its own expiry or the artifact’s, whichever comes first. The rows are removed later by the hub’s maintenance pass, which runs every 10 minutes and handles up to 500 expired artifacts each time: it deletes the artifact together with all its grants and share links. Between expiry and that pass, the rows still exist, but they give no access.
  • Moving. PATCH /api/v1/artifacts/{id} with scopeRef moves an artifact to another home project, for example after its project was deleted (deleting a project never deletes its artifacts, and their share links keep working; see Share links). Any move, that one included, needs the hub’s messaging.cross_project_messaging_enabled setting on and the caller allowed to publish into the new project through a role there (owning the artifact is not enough). The new project gets a read grant (an existing grant of it is kept); the old home project loses its grant, so to keep its access, add a grant for it explicitly. A move that would take the artifact over the grant limit is refused with 409 and code too_many_grants. A move to a project that holds an admin grant on the artifact is refused with 409 and code home_admin_grant, because a home project’s grant is read or write only; lower that grant first. Naming the current home project in scopeRef is not a move.
  • Recorded. Every grant creation and permission change, grant removal, share-link creation and revoke, expiry change, move and deletion is recorded in the artifact’s history with who made it and by what authority (owner, admin grant, steward as home-project owner or delegating user, home-project admin, or the hub’s retention and owner checks). A share link’s record carries its id and expiry, never its token. A move’s record names the old and new home project and the old home’s grant it removed.
  • Storage. File contents are stored once per hub and shared by every version and artifact with the same bytes. Contents no remaining artifact uses are deleted after gc_grace_hours (default 168, at least 24); until then a deleted artifact’s contents still exist on the hub.

DELETE /api/v1/artifacts/{id} (or scion artifact delete <ref>) deletes an artifact: in one step the artifact is marked deleted and all its grants and share links are removed, exactly as when it expires. From then on it answers 404 on every route, its share links answer 404, message references to it show as unavailable, and its key is free again. Its files are removed by the maintenance pass after gc_grace_hours. There is no undelete.

Who may delete:

  • the owner;
  • a user with an admin grant on the artifact;
  • on an artifact owned by an agent, a steward: the delegating user or an owner of the home project;
  • an admin of its home project: the project owners and admins of the artifact’s home project, and super-admins (anyone holding project.manage in the artifact’s current home project). Hub-admins do not hold project.manage. A super-admin holds it for every project, so a super-admin can also delete an artifact whose home project was deleted. After a move, the new home’s admins have this and the old home’s lose it.

Each of them must be a user whose credential allows artifact.manage in the home project, so a user access token stays within its boundary and permissions. Agents never may, the owning agent included. The read check comes first: a caller that cannot read the artifact gets the same 404 as for one that does not exist; a reader without the right to delete gets 403 with one fixed message, whatever the reason.

Every 10 minutes the hub’s maintenance pass checks the owners of live artifacts: an agent is live while it exists and is not soft-deleted, a user while it exists and is active. Where the owner is gone, the pass marks the artifact; where a marked artifact’s owner is back (an agent restored from a soft delete, or a user made active again), it clears the mark. This covers every way an agent stops existing: a soft or hard delete, the purge of soft-deleted agents, and deleting its project. A marked artifact shows ownerDeleted: true in GET, list and PATCH responses. Nothing else changes: its expiry, grants, share links and readers stay as they were, and it is deleted only if someone deletes it (an admin of its home project can) or it expires. Marking and clearing are recorded in the artifact’s history. If the hub cannot check owners, it changes nothing and tries again on the next pass.

A message can name artifacts. The message carries only the reference, never the bytes and never a copy of the title; each reader sees what their own access allows.

$ scion message @reviewer "Design ready for review." --artifact scion://artifact/5f1c2d3e-6b1a-4c55-9f3e-0d6e7a1b2c3d
  • Sending. --artifact (repeatable, at most 10 per message) adds the reference to the message text and to the message’s artifacts metadata entry. The Hub keeps only references the sender can read at send time; it drops the others and returns a warning that does not say why a reference was dropped. If a reference could not be checked because the hub could not read the artifact, its grants or the named version, the reference is not attached either, and a separate warning counts those references as not checked because of a server error, so that sending again may succeed. Neither warning names a reference. If artifacts are not enabled on the hub, every reference is dropped and the warning says so. When a message goes to one agent or one user directly, the hub also checks whether that recipient may be able to read the references it attached, and if some may not, a further warning gives their count: “N artifact reference(s) may not be readable by RECIPIENT; ask someone who manages them to grant access, or check the recipient’s scopes.” The message and its references are still delivered. The check applies the artifact read rules (owner, home-project access, grants, expiry) to the recipient and, for an agent, also whether the token it would be issued next carries project:artifact:read; it can only warn, never grant access, and it names no reference and no reason. It is not run for group, conversation or broadcast sends. If a lookup the check needs fails (the recipient, the artifact, its grants, or a recipient agent’s delegation chain), no warning is given; a policy evaluation that cannot complete counts as a refusal and may give the warning (ptone/scion#4575).
  • What an agent receives. The delivered message keeps the artifacts metadata entry and ends with one fetch hint per artifact, built from the reference alone:
    Artifact: v2 - scion artifact get scion://artifact/5f1c2d3e-...@2
    Artifact: current - scion artifact get scion://artifact/5f1c2d3e-...
    The hint carries no title. Whether the recipient may read the artifact is decided when it runs scion artifact get, from any broker, without shared directories.
  • What web chat shows. A message with artifact references shows a chip for each one, in the order the references appear in the message text (references attached without appearing in the text follow): the title, version and owner of an artifact you can read, or Artifact unavailable for any other reference, the same as for an artifact that does not exist. A reference the hub could not check because of a server error also shows as unavailable. scion://artifact/ references in message text are links. Clicking a chip or a link opens the artifact in a preview over the conversation, with Copy link and Open in artifact viewer. Images show in the preview. Markdown renders there in the same sandboxed frame as on the artifact page, and HTML runs in the same sandboxed view frame as on the artifact page, under the bar that names its publisher (see HTML artifacts). Other text shows as its source in a read-only editor. Open in artifact viewer opens the artifact page. Chat history responses include messageArtifacts, keyed by message ID, resolved for the viewing user. When the hub cannot read the references recorded for a page of history, that response carries no messageArtifacts and lists the page’s messages that are not deleted in messageArtifactsUnavailable instead, so a client does not take them for messages without references; web chat then shows no chips for those messages and one notice that artifact references could not be loaded.
  • Attaching in web chat. The composer’s paperclip offers Upload file and Attach artifact…. The picker lists the artifacts you can read, with search and Owned by me / This project filters, and attaches the current version of up to 10 per message. With nothing to list, it offers New artifact for the conversation’s project to a caller who may publish there (it asks the hub once per project, with a one-row list narrowed to the project). Picked artifacts show as chips above the input; they travel as references, never as copies.
  • Where references are kept. Direct messages to an agent, messages from an agent to a user or conversation, and web chat messages to agents carry references. Group (group[...]) and broadcast messages, @-mention copies, and messages arriving through chat plugins do not; any artifacts metadata they arrive with is removed.

Artifacts list. The Artifacts item in the sidebar, under Management beside Skills, opens /artifacts. It lists the same artifacts as the list endpoint: the ones you own, the ones shared with you, and the ones published in your projects, newest first. Each row shows the title (with the key, when the artifact has one), why you see it (Owned, Project or Shared with you), the owner, the home project, when it was last updated and a Review pending badge when the current version is a review. A row whose home project was deleted shows Deleted project; while the hub’s cross-project sharing setting is on, its owner and users it grants admin also see Move…, which moves it to one of their projects (see Expiry, moving and deletion). Search filters by title or key; Review pending, Owned by me and Shared with me narrow the list further. Load more fetches the next page. A row opens the artifact’s page. Owner and project names show as ids when you may not look them up. New artifact opens the publish dialog with a Project field. The field lists only the projects where you may publish (for example, as owner, as a member with publish rights, or as an admin whose role grants it). The global project (slug global) is looked up on its own (GET /api/v1/projects?slug=global); the other projects come from the projects you can see (GET /api/v1/projects, without mine=true, which means owned projects only). For each project, the page asks the list endpoint for one row narrowed to that project and reads canCreate, six projects at a time, global first. Choices appear as their checks pass. The list of other projects stops at the first 2000 you can see, and the dialog says so when it does; global is offered either way. Global is preselected when you may publish there; otherwise the only choice is preselected, or nothing, and Publish waits for a pick. To set the button, the page checks global first and stops there when you may publish in it. Otherwise it checks the other projects and stops at the first one you may publish in. The button is disabled while the check runs. When you may publish nowhere it stays disabled, and hovering or focusing it says why. If the check fails, the button is enabled and the dialog shows the error with Retry. After publishing, you land on the new artifact’s page.

Artifact page. /projects/<project-id>/artifacts/<id> shows the current version and /projects/<project-id>/artifacts/<id>/v/<seq> an earlier one. Its back link goes to the artifact’s home project, or to the artifacts list (/artifacts) when that project was deleted (the response’s scopeDeleted). The header shows the title, owner, key, last update and the version’s reference with a copy button, a Version menu listing the versions, Edit and Review (Markdown entries) for a caller that may publish new versions (canPublish), only Review for a caller with a review grant (canReview), Share (for the owner and users it grants admin), Move… (for the owner and users it grants admin, while the hub’s cross-project sharing setting is on: it uses the same dialog as the artifacts list, which says when the home project was deleted, and, after a move, opens the artifact under its new home project), Grant review access (instead of Share, for a caller that may give review access but not manage the artifact; see Grant review access below) and, for a single file, Download. Three tabs:

  • Preview renders the entry file: Markdown as formatted text, text and code (including JSON, YAML, CSV) in a read-only editor, PNG, JPEG, GIF and WebP images inline, and HTML in the sandboxed frame described under HTML artifacts, with a border and a bar saying who published it, plus Full screen and Open in new tab. The view URL lasts 30 minutes; after that the page says the view has expired and offers Reload view. Other types (such as SVG and PDF) are offered as a download.
  • Files lists the version’s files with size and type, marks the entry file, and opens or downloads each one. Copies of remote images are counted, not listed.
  • History lists the versions, newest first, with who published each one, when, and its note.

Markdown preview. The preview is a frame that runs no scripts and loads images from the hub only. A relative image path loads that file from the same version, and an absolute image URL loads the copy the hub fetched when the version was published. An image without a copy shows “Image not fetched”, one whose fetch failed shows “Image could not be fetched”, and a relative path that is not in the version shows “Image not in this version”. Inline (data:) images are shown. Links open in a new tab; a relative link to a file of the version opens that file.

Edit and new versions. On the current version, when the entry file is Markdown or text, Edit opens it in an editor, and Publish new version saves the change as a new version with an optional note; the other files are kept as they are. If a publish fails part-way, publishing again continues it rather than starting over; an unfinished version that is not continued is removed after 24 hours. Upload new version (on the History tab) publishes a file or a folder as the next version. Earlier versions are never changed. Edit, Review and Upload new version each publish a version, so they are shown only to a caller that may publish one (the response’s canPublish). A caller with a review grant (canReview) sees only Review; other readers see the artifact without them.

Review. On the current version, when the entry file is Markdown, Review opens it in an editor beside a live preview. Select text and choose Comment, Suggest, Insert or Delete to add a mark (see Reviews); the toolbar counts the suggestions and comments. A selection inside or across an existing mark, one that contains CriticMarkup outside code, or one whose mark an unclosed opener earlier in the text would swallow, is refused with a short hint, since marks do not nest. Code is literal text (see Reviews): a whole inline code span can be selected, but a selection or insertion point inside a code span, or across its edge, is refused. The toolbar does not mark fenced code blocks: a selection or insertion point that touches a fence line or falls inside the block is refused, with a hint to type the marks in the editor or comment on the text beside the block. The lines before and after a block can be marked as usual. Any other mark that would change which text is code is refused too. When the web.artifact_review_editor experiment is on (web-only, requires hub.artifacts), Review opens a single wide workspace with a Graphical and Source toggle instead (and a Reviewing badge in the header). In Graphical view (Track changes is always on), the rendered document is shown with its CriticMarkup marks inline: typing, deleting and replacing text are recorded as CriticMarkup suggestions, and Comment (Ctrl+Alt+M or Cmd+Alt+M) adds a note on the selection (Enter edits a comment you added in this review; comments from earlier reviews are read-only). Changes that cannot be recorded in the graphical view (formatting, headings, lists, tables, code blocks, paragraph breaks, multi-paragraph paste, or edits that would move an earlier comment) show a short hint suggesting the Source view; if the graphical editor chunk cannot be loaded, the review falls back to Source view. Source view is the CriticMarkup code editor and toolbar (Comment, Suggest, Insert, Delete); suggestions made in the graphical view become read-only in the graphical view after switching to Source, though they remain editable in Source. Undo history is kept per view and resets when switching, and the chosen view is stored in localStorage (scion:artifact-review-view). Save review publishes the marked-up copy as a version of kind review, with an optional note (the Hub saves and checks reviews the same way whether or not web.artifact_review_editor is on). It stays disabled until there is at least one mark, and while text outside the marks differs from the version you started from, the page says so (use Edit for a plain edit). If the hub refuses the review, the page names the lines that changed outside marks. If a newer version was published (or is being published) while you reviewed, the review is not saved; the review restarts from the current version (with any marks others added), and your discarded text is shown read-only with Copy so you can redo your marks. If the current version cannot be reviewed on the page, or cannot be loaded, Review mode closes with a notice that says which, and your text stays readable with Copy. Edit and Review are hidden while you review. While the current version is a review, the header shows a Review pending badge (also when an older version is shown) and a banner names the reviewer and counts the marks; both clear when a new version of kind publish is current. A review’s preview has a Marks · Clean · Accepted switch: Marks shows insertions, deletions, substitutions and highlights inline and comments as numbered notes headed by the reviewer (in a right-hand margin when the window is at least 1100 pixels wide, or 1400 pixels in Review mode where the preview takes half the page; under the reference otherwise); Clean shows the text with every mark rejected, Accepted with every mark accepted, as ?resolve= does.

Share dialog. Share on the artifact page opens a dialog with three parts:

  • Share links. Choose a lifetime and Create link. The dialog offers 1, 7 and 30 days up to the hub’s maximum (link_max_ttl_hours), plus the hub’s default and maximum when they are not among those, and preselects the default (link_default_ttl_hours); it reads both from GET /api/v1/artifacts/{id}/links. The new link is shown once, with Copy; it cannot be shown again, so copy it then. When the artifact expires before the chosen lifetime ends, the dialog says the link ends with the artifact. The table lists the active links with who created them, when, and when they expire, each with Revoke. A link opens the current version in the sandboxed viewer described under HTML artifacts.
  • People and projects. Search for a user, or for a project when cross-project sharing is on, pick a permission (Can view, Can edit, Can manage: read, write, admin) and Add. Each grant’s permission can be changed or the grant removed; a review grant is listed as Can review, and can be removed but not changed here; the home project is listed first, its permission can be set to Can view or Can edit (never Can manage), and it cannot be removed. When cross-project sharing is off, the dialog says so and offers users only, and a grant to another project can be removed but its permission not changed.
  • Retention. Set when the artifact expires (never, or 1, 7, 30 or 90 days from now). Before you save a change, the dialog warns how many share links it cuts short and how many grants are removed with the artifact when it expires.

Grant review access. On an artifact owned by an agent, the user who delegated to the agent, the project owners and admins of the artifact’s home project, and super-admins may give a person review access (see Grants), even though they cannot manage the artifact. For them the artifact page shows Grant review access where the owner sees Share (the response’s canGrantReview). It opens the people part of the Share dialog, limited to that one action: search for a user who can already read the artifact through the home project or a grant to one of their projects, and Add gives them Can review (a review grant: they can publish reviews, and nothing else; it gives no read access of its own). The dialog offers no share links, no other permission, no projects, no retention and no list of grants, since only the owner and admins may see those. The dialog says that review access is only for people who can already read the artifact. Each person added is confirmed in the dialog. A refused request shows the hub’s message, for example that the person cannot read the artifact, or that the delegation was revoked in the meantime.

Owner deleted, Retention and Delete. An artifact whose owner is gone (see Owner deleted) has an Owner deleted badge, with the owner’s name struck through, in the Status column of the artifacts list and of a project’s Artifacts view, and beside the title on the artifact page; a name that can no longer be looked up is shown as a short id. The artifact page’s header also shows when the artifact expires (Never when it is kept until deleted), to every reader. Delete… is shown when the response’s canRetire is true, that is, to everyone the delete route allows (see Deletion). It opens a confirmation that says the artifact stops being readable everywhere at once, that messages show it as unavailable and that its versions are deleted; for a caller who may list the artifact’s links and grants it also counts the share links that stop working and the grants removed. After a delete the page says so and goes back to the home project, or to the artifacts list when that project was deleted; a refused delete keeps the dialog open with the reason. Retention… is shown to a steward (canShare without canManage): it opens Retention and access, the Share dialog limited to setting the expiry, revoking share links and removing grants. It offers no way to create links or give access, and says to do that with scion artifact share and scion artifact grants; the home project’s grant is shown but cannot be removed. The Owner deleted badge can be focused, and its explanation is its accessible name. Owners and admin grantees set retention inside Share.

Project page. With artifacts on, the Files area of a project page has three views: Artifacts (shown first), Shared dirs and Workspace. Artifacts lists the artifacts homed in the project that you can open, and the artifacts other projects shared with it (badged Shared with this project, with the project they come from), newest first, with search, a Shared with this project filter, owner, version, last update and a Review pending badge; a row opens the artifact page. New artifact, shown only to a caller who may publish in the project (the list’s canCreate), uploads a file, or a folder with a chosen entry file (hidden files and folders are skipped), with a title, an optional key and an optional note. Shared dirs and Workspace show the project’s files as before.

All routes are under /api/v1/artifacts and use the hub’s usual authentication (session, user access token or agent token). Errors use the hub’s JSON error envelope. The hub’s request logs record every artifact request path as /api/v1/artifacts/REDACTED (paths that contain the word artifacts anywhere are treated the same way), and artifact requests are not traced, because some artifact URLs carry share-link tokens or view capabilities.

Method and path Purpose
POST /api/v1/artifacts?name=<file>[&title=<title>][&scope=<project-id>] Publish the raw request body as a new single-file artifact. scope defaults to the caller’s project (agents); users must set it. Optional header X-Content-SHA256 (hex) is verified. Returns 201 with the artifact, its first version and any publish warnings.
GET /api/v1/artifacts?mine=1[&q=<text>][&review_pending=1][&owner=me][&scope=<project-id>][&shared=1][&limit=<n>][&cursor=<c>] The artifacts the caller can read among those it owns, those shared with it directly, and those homed in projects it is a member of, newest first. See Listing.
GET /api/v1/artifacts/{id} The artifact and its current version, including the file manifest (path, size, sha256, mediaType, and for remote images origin, sourceUrl and fetchStatus; see Images in Markdown artifacts), canManage: true when the caller may share and change the artifact (the owner, or a user with an admin grant), canShare: true when the caller may share it (canManage, or a steward) with shareAuthority naming a steward’s clause (project_owner or delegator), canRetire: true when the caller may delete the artifact: the same decision DELETE /api/v1/artifacts/{id} makes for that caller (the owner, a user with an admin grant, a steward, or an owner or admin of its home project; see Owner deleted), canPublish: true when the caller may publish new versions of it of any kind: the same decision POST /api/v1/artifacts/{id}/versions makes for that caller (the owner or a write or admin grantee, while its credential allows artifact.create in the home project), canReview: true when the caller may publish reviews only (a review grantee that can read the artifact another way, with the same credential condition; never true together with canPublish), and canGrantReview: true when the caller cannot manage the artifact but may give a user review access to it: the decision POST /api/v1/artifacts/{id}/grants makes for a review grant to a user from that caller (on an artifact owned by an agent, the delegating user, the project owners and admins of the home project, and super-admins; see Grants). canGrantReview is never true together with canManage. These flags are omitted when false. artifact.ownerDeleted: true marks an artifact whose owner is gone (see Owner deleted); it is omitted otherwise. scopeDeleted: true marks an artifact whose home project was deleted, by the same rule as scopeDeleted on list rows; it is omitted otherwise, and also when the hub cannot check the project, since the field is advisory and the request does not fail over it. They are computed only for a caller that can read the artifact; anyone else gets the 404 of a missing artifact. If the artifact’s grants cannot be read while computing them, the answer is 500 with code internal.
GET /api/v1/artifacts/{id}/files/{path}[?resolve=clean|accept] A file of the current version. With resolve, see Resolving marks.
GET /api/v1/artifacts/{id}/versions/{seq}/files/{path}[?resolve=clean|accept] A file of version seq.
POST /api/v1/artifacts (JSON manifest) Start a two-step publish: a new artifact with a pending first version, or, when key names an artifact the caller already published in the scope, a new pending version of it. Returns 201 with the artifact, the pending version and upload.required, the paths to upload.
POST /api/v1/artifacts/{id}/versions (JSON manifest) Start a new pending version of an artifact. With "kind": "review", a review of the current version (409 with code nothing_to_review when the artifact has no published version).
PUT /api/v1/artifacts/{id}/versions/{seq}/files/{path} Upload one file of a pending version (raw body). Its size and SHA-256 must match the manifest; X-Content-SHA256, when sent, must too. Returns 204.
POST /api/v1/artifacts/{id}/versions/{seq}/finalize Make a pending version ready once every file has arrived; it becomes the current version unless a later one already is. Returns 200 with the artifact, the version and any warnings (remote images that could not be fetched); 409 with code incomplete and details.missing while files are missing. For a review, the optional JSON body {"base": <seq>} is required: 400 with code base_required without it, 422 with code unmarked_changes or 409 with code stale_review, both discarding the review (see Reviews).
POST /api/v1/artifacts/{id}/versions/{seq}/view For a version whose entry is HTML: a view URL for showing it in a sandboxed frame (url, expiresAt, and remoteImages when the entry references images on other servers). Requires read access; the URL is valid for 30 minutes.
GET /api/v1/artifacts/view/{capability}/{path} A file of the version the view URL was issued for; see HTML artifacts.
DELETE /api/v1/artifacts/{id} Delete the artifact, with its grants and share links. Returns 204. Allowed for the owner, an admin grantee, a steward and an admin of the home project, each a user whose credential allows artifact.manage there; 403 with code forbidden for any other reader, 404 for a caller that cannot read it (the same as for a missing artifact). See Deletion.
PATCH /api/v1/artifacts/{id} Change an artifact: {"expiresAt": "<time>"} sets the expiry (null clears it; a time in the past is refused), {"scopeRef": "<project-id>"} moves it. Admin only; a steward may change the expiry, but a move from a steward answers 403 with code steward_action_not_allowed. Both changes apply together or not at all. Returns 200 with artifact, and linksCutShort and grantsRemoved when it has an expiry; 403 with code cross_project_sharing_disabled for a move while that is off; 409 when the owner already has an artifact with the same key in the target project, with code too_many_grants when the move would exceed the grant limit, or with code home_admin_grant when the target project holds an admin grant on the artifact. See Expiry, moving and deletion.
GET /api/v1/artifacts/{id}/grants The artifact’s grants (id, subjectKind, subjectRef, permission, home, createdAt, createdBy), the home project’s first, and crossProjectSharing: whether grants to other projects (and moves to them) are on. Admin only.
POST /api/v1/artifacts/{id}/grants Add a grant, {"subjectKind": "principal" or "scope", "subjectRef": "user:<id>", "agent:<id>" or a project id, "permission": "read", "review", "write" or "admin"}, or change the permission of the subject’s existing grant. Review is for a user only. Returns 201 when it added one and 200 when it changed one; 400 for an admin grant to the home project or a review grant to anything but a user, and 400 with code review_replaces_access for a review grant to a user who holds a read, write or admin grant; 403 with code cross_project_sharing_disabled for another project while that is off; 409 with code too_many_grants at the cap. Admin only, or a steward, whose admin grant answers 403 with code steward_action_not_allowed; except that on an artifact owned by an agent the agent’s delegating user and a home-project admin may add a review grant for a user who can already read it through the home project or a grant to one of their projects (see Grants); such a caller gets 403 with code review_subject_cannot_read when that user cannot read the artifact and 403 with code review_replaces_access when the user holds a read, write or admin grant; every other request from a reader who is not an admin gets 403. See Grants.
DELETE /api/v1/artifacts/{id}/grants/{grantId} Remove a grant. Returns 204; 409 for the home project’s grant.
POST /api/v1/artifacts/{id}/links Create a share link. Optional body {"ttlHours": <n>}. Returns 201 with link (id, createdAt, expiresAt, createdBy), url (the link, shown only here) and clampedToArtifactExpiry when the artifact’s own expiry shortened it; 400 with code ttl_too_long for a lifetime above the maximum (rejected, not shortened), 409 with code too_many_links at the per-artifact cap, 409 for an artifact with no published version. See Share links.
GET /api/v1/artifacts/{id}/links The artifact’s unexpired share links, oldest first, without tokens, and defaultTtlHours and maxTtlHours: the lifetime a link created without ttlHours gets and the longest one POST accepts, in hours (see Lifetime).
DELETE /api/v1/artifacts/{id}/links/{linkId} Revoke a share link. Returns 204.
GET /api/v1/artifacts/shared/{token}[/files/{path}] Open a share link, with no credentials: 303 to a view URL.
GET /api/v1/artifacts/{id}/versions[?limit=][&before=] The ready versions, newest first, without their files, and in discarded the discarded versions of the same page (see Discarded versions). A page holds up to limit (default 100, at most 500) ready and discarded versions together with a version number below before, so it can hold fewer than limit ready versions; nextBefore in the response gives the next page, and clients follow it.
GET /api/v1/artifacts/{id}/versions/{seq} One ready version with its files, and canManage, canShare, shareAuthority, canRetire, canPublish, canReview, canGrantReview and scopeDeleted as for GET /api/v1/artifacts/{id}.

Status codes: 400 for a malformed request (including a list request without mine=1 or with an invalid cursor), 401 unauthenticated, 403 when the caller may not publish in the scope (with code missing_scope when an agent token lacks project:artifact:read or project:artifact:write: details.scopes lists every missing scope, and details.scope holds the first of them; reads by such a token answer 404), 404 for an absent or unreadable artifact, 413 when the file exceeds artifacts.max_file_bytes (rejected before anything is stored), 500 with code internal when the hub cannot read what an access decision needs, such as the artifact’s grants (on read, write and management routes and for the list, which then returns no partial page; access is never given in that case), 503 when the hub has no artifact storage configured.

Two-step publish. The manifest is {"title", "key", "scope", "kind", "entry", "note", "files": [{"path", "size", "sha256", "mediaType"}]}; kind is publish (the default) or review, and a review is posted to /api/v1/artifacts/{id}/versions; title, key and scope apply only when posting to /api/v1/artifacts (and title only when that creates the artifact), and entry may be omitted for a single file. Paths are relative, use /, may not contain a name starting with ., and may not start with _remote/, a prefix the hub reserves; no path may also be a folder of another. A file whose path and SHA-256 match the current version’s needs no upload. Only the publisher of a pending version may upload to it or finalize it, and one finalize request at a time completes it (a finalize cut short, for example by a hub restart, can be retried after a few minutes); an artifact has at most 4 pending versions at a time, and a version still pending after 24 hours is discarded (an artifact left with no version is removed, freeing its key). Limits are checked against the manifest (413) and again at finalize, so a limit lowered meanwhile applies. Until its first version is finalized, a new artifact is visible only to its publisher.

?resolve=clean or ?resolve=accept on a file route serves a text file (text/*, JSON, YAML, TOML or XML) with its CriticMarkup marks resolved as in Reviews. The hub produces the bytes, so the response is always streamed by the hub, never redirected to object storage. It carries X-Artifact-Resolve naming the projection, an ETag distinct from the stored file’s, and the usual file headers. Other files ignore the parameter and are served unchanged, without X-Artifact-Resolve. resolve=raw or no parameter serves the stored bytes; any other value answers 400 whatever the artifact. Access checks are those of any file read: an artifact the caller cannot read answers the same 404 as one that does not exist.

GET /api/v1/artifacts?mine=1 lists, newest update first:

  • artifacts the caller owns;
  • artifacts shared with the caller directly;
  • artifacts homed in a project the caller is a member of (for an agent, its own project), or shared with such a project.

Every listed artifact passes the same check as GET /api/v1/artifacts/{id} for the same caller, so the list never shows an artifact the caller could not open. A caller that may read nothing gets 200 with an empty list. That includes a user access token without artifact:read and an agent token without project:artifact:read. Membership is read on every request, so after a user leaves a project, that project’s artifacts drop out of the list unless the user owns them or they were shared with the user directly. A hub-wide role does not add artifacts: an admin sees the same kinds of rows as anyone else.

mine=1 is required: without it the request is rejected with 400, because there is no hub-wide listing. Optional parameters:

Parameter Effect
q Keep artifacts whose title or key contains this text (at most 200 characters). Case is ignored for ASCII letters. For other letters, whether case is ignored depends on the database, so search for them in their exact case.
review_pending=1 Keep artifacts whose current version is a review awaiting the owner.
owner=me Keep only artifacts the caller owns.
scope Keep only artifacts homed in this project (a project ID) or shared with it; shared ones carry sharedWithScope: true. Artifacts shared with the project are included only when the caller may read in that project; otherwise only those homed there are kept. It narrows the list and never adds an artifact the list would not show without it. The project’s Artifacts view in the web UI uses it.
shared=1 With scope, keep only the artifacts shared with that project from elsewhere (an empty list when the caller may not read in that project). Without scope, keep only the artifacts a grant shares with the caller (access is shared).
limit Page size, 1-100 (default 50).
cursor The nextCursor of the previous page.

The response is {"artifacts": [...], "nextCursor": "..."}, plus, on a list narrowed with scope=, canCreate: true when the caller may publish artifacts in that project (the decision a publish there makes; omitted when false, and always omitted without scope=). Each entry has the same fields as artifact in the single-artifact response, plus reviewPending and access: owned (the caller owns it), project (the caller may read artifacts of its home project) or shared (a grant to the caller or to one of its projects). An artifact whose owner is gone has ownerDeleted: true. An artifact whose home project was deleted has scopeDeleted: true, and on those rows canManage: true when the caller may move it and cross-project sharing is on (moves need it). nextCursor is absent on the last page. Cursors are opaque and only work for the same caller and the same q, review_pending, owner, scope and shared values. A page may hold fewer than limit entries and still have a nextCursor, because one request examines a bounded number of rows. Keep following the cursor until it is absent. Artifacts created or updated while you page move to the front of the order: they appear on a fresh listing, not later in the current walk.

File delivery. On a hub with local storage the hub streams the bytes. On a hub with object storage (GCS) it answers 302 to a short-lived signed URL; add ?stream=1 to have the hub serve the bytes itself (the web page does this for text). Either way the response carries Content-Disposition (inline only for plain text, Markdown, CSV, TSV, JSON, YAML, TOML and raster images; attachment otherwise) and X-Content-Type-Options: nosniff; streamed responses also carry a sandboxing Content-Security-Policy and an ETag.

Permissions. Reading checks artifact.read in the artifact’s home project; publishing checks artifact.create in the target project. Adding a version to an existing artifact also checks artifact.create, in the artifact’s home project, and requires being its owner or holding a write grant on it (or, for a review version only, a review grant together with read access). Project owners, admins and members hold both for their project; sharing an existing artifact, changing its expiry and moving it are left to its owner and to users it grants admin (with one narrow exception for review access on artifacts owned by an agent; see Grants). Agent tokens carry them through the project:artifact:read and project:artifact:write scopes: readonly, baseline and full agents can read, baseline and full agents can publish, and no agent can delete artifacts or manage their grants. Deleting an artifact checks artifact.manage in its home project (the reserved artifact.delete permission is not used). An agent token without project:artifact:read gets 404 for every artifact, even one shared with it directly. A user access token is limited by its own project boundary and permissions; removing a user from a project does not by itself stop such a token from reading artifacts the user owns or was given access to, so revoke the token for an immediate cut-off. Artifact access uses dedicated agent scopes. If an agent gets 403 missing_scope when publishing, or 404 for every artifact, the usual cause is the scope ceiling of the agent’s creator, which leaves the artifact scopes out of the agent’s tokens (the hub warns about this when it creates or starts the agent, and scion whoami --full lists the token’s scopes), or the agent was created before those scopes existed. Recreating the agent from the same parent gives it the same scopes. Once the creator holds the scopes, a Hub super-admin can run scion reset-auth <agent> --reissue-scopes (parents before children, or --all; see Reset Auth and Scope Re-issue). Agents created from an access token issued before artifacts existed still need to be recreated from a current credential.

A version number is taken when a version is created, before its content is checked. A version that stops being pending without becoming ready keeps its number, so the numbering can have gaps. The versions list explains them: next to versions, GET /api/v1/artifacts/{id}/versions returns discarded, the discarded versions of the same page, newest first (ptone/scion#4577).

Each discarded entry has only four fields: seq, state (failed), reason and createdAt (when the version number was taken). It never names who submitted the version, its note, kind or files, which stay as hidden as before, and GET /api/v1/artifacts/{id}/versions/{seq} on a discarded number still answers 404. Discarded versions are listed only to callers who can read the artifact.

reason is a fixed code:

Code Why the version was discarded
unmarked_changes A review changed text outside its CriticMarkup marks (finalize answered 422 unmarked_changes).
stale_review The version a review was started from was no longer the latest (finalize answered 409 stale_review).
newer_publish_pending A newer version was being published when the review was finalized (409 stale_review).
expired The upload was not finished within the pending-version time limit and was removed.
discarded The version was discarded before reasons were recorded.

scion artifact versions lists discarded versions among the ready ones by number, and the artifact page’s History tab shows consecutive discarded versions as one collapsed row.

The Hub writes one structured entry per artifact decision to a dedicated log, scion-artifacts (ptone/scion#5120). With direct Cloud Logging (SCION_CLOUD_LOGGING=true) it is a separate Cloud Logging log that shares the client and circuit breaker of the request and message logs. Without Cloud Logging the entries go to stdout, like the message log. The request log keeps its redacted artifact paths. The decision log is where an operator finds which artifact a request touched and why it was refused.

Levels. Lifecycle decisions are logged at INFO. Authorization refusals on management and write routes are logged at WARN. A refused POST /api/v1/artifacts/{id}/grants from a caller who may read the artifact but not administer it (it is answered by the review-grant path, whatever grant it asks for) is logged at WARN with action share and one of the review_ reasons; when the request named a well-formed user, that user is the entry’s subject. A read answered with the 404 of a missing artifact, or of one the caller cannot read, is logged at DEBUG only, so it costs nothing at the default level. Successful reads are not logged. Entries follow the server log level (SCION_LOG_LEVEL).

Actions. One entry is written per decision:

action When
published A single-file publish created an artifact and its ready version 1.
created A manifest publish created an artifact with a pending version 1.
version_created A pending version was added to an artifact.
version_finalized A version was made ready.
version_failed A publish, a new or added version, or a finalize could not be recorded (outcome error, reason store_error), or a review was discarded (outcome refused, reason one of the discarded version codes).
grant_created, grant_changed, grant_removed A principal or scope grant was added, had its permission changed, or was removed. This covers grants made by administrators and review grants.
link_created, link_revoked A share link was created or revoked.
moved The artifact moved to another home project.
expiry_set, expiry_cleared The expiry was set or cleared.
deleted The artifact was deleted by a user or by the expiry sweep.
orphaned, unorphaned The owner liveness pass marked an artifact whose owner is gone, or cleared the mark.
owner_pass, retention_pass A summary of one owner liveness pass or one expiry sweep. They carry owners_checked, orphaned_count and unorphaned_count, or count. Each summary is written only when the pass changed something or failed.
read, administer, write, create, delete, move, share What a refused request attempted. route names the request.

Fields. Each entry carries the fields that apply to it:

Field Meaning
artifact_id The artifact. It comes from the parsed route, never from the raw path.
version_seq The version number.
project_id The artifact’s home project. For a move, the home it moved from. For a refused publish, the requested target project, only when it has a project id’s shape (letters, digits, - or _, at most 64 bytes); otherwise it is left out. For an expiry deletion, the home the sweep found it in. A summary entry has none.
route The method and route template, for example POST /api/v1/artifacts/{id}/grants. It never contains a path segment. The share-link route appears as /api/v1/artifacts/shared/{token}.
outcome allowed, refused or error.
reason Why a request was refused or failed. It is always a fixed code, never free text: not_found, hidden, unauthenticated, credential_not_permitted, agent_cannot_administer, not_owner_or_admin_grant, not_owner_admin_grant_or_project_admin, no_write_access, expired, review_only, not_version_publisher, not_authorized_in_scope, cross_project_sharing_disabled, steward_action_not_allowed (a steward asked for an admin grant or a move), steward_precondition_failed (a steward’s change found the artifact moved or transferred, and answered 404), store_error, principal_check_failed, a discarded version code, or, for a refused review grant, review_caller (the caller may not give review access), review_body (the request is not exactly a review grant to a user), review_subject_unreadable (the user cannot read the artifact), review_existing_access (the user holds a read, write or admin grant) or review_store_refused (the artifact’s home or owner changed meanwhile).
principal_kind, principal_id Who made the decision: user or agent and its id, or system and retention or reconciler for the maintenance pass.
authority Why the caller was allowed: owner, admin_grant, steward_project_owner and steward_delegator (a steward acting as an owner of the home project or as the owning agent’s delegating user), custodian_project_admin (deleting as an administrator of the home project, or giving review access on the review-only path as one), or retention and reconciler for the maintenance pass. On the review-only path, which serves only callers who are not stewards, a delegating user is recorded as review_grant, as is a review grant on a hub that does not report the clause. A version written through a grant has no authority either; its permission is write or review instead.
subject_kind, subject_ref A grant’s subject: principal with user:<id> or agent:<id>, or scope with a project id. A share link is logged with subject_kind link and no subject_ref.
permission The grant’s permission, or the grant a version was written through.
owner_kind, orphaned The artifact owner’s kind, and whether the artifact is marked as having a deleted owner.
request_id The id of the request log line (scion_request_log) of the same request. Maintenance entries have none.

With Cloud Logging, artifact_id, project_id, action, outcome, reason, principal_kind and principal_id are also labels.

Never logged. No entry contains a share link’s token, the token’s hash or the link’s id, or any other path segment of the link routes. No entry contains an artifact’s title, a version note, file names or contents, or any credential. Artifact ids and grantee ids do appear. Anyone with read access to the log can see them, as with the message log. The Hub diagnostics log API (GET /api/v1/admin/diagnostics/logs) classifies entries from this log as the artifacts source and accepts source=artifacts. The diagnostics log viewer in the web UI shows them under its ART source toggle.

Example query:

logName="projects/YOUR_PROJECT/logs/scion-artifacts"
labels.artifact_id="ARTIFACT_ID"