Skip to content

API reference

The reference is generated from artifactr.files' docstrings and type annotations. It documents each package's public API: the names in its __all__.

artifactr.files.manifest

Native files as artifacts: the manifest types, and the revision that tests its base.

An artifact of a native type is a small Pydantic manifest. It names the file's bytes, by their SHA-256, in a Blobs store, and a revision is ordinary JSON Patch that first tests the digest it was made from (revise).

REVISED module-attribute

REVISED: Final = (
    "content",
    "provenance",
    "fingerprint",
    "text",
    "review",
    "outline",
    "pages",
)

What a revision replaces: everything in the manifest but the file's name.

DOCX module-attribute

DOCX: Final = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"

The media type of a Word document, .docx.

DWG module-attribute

DWG: Final = 'image/vnd.dwg'

The media type of an AutoCAD drawing, .dwg.

DXF module-attribute

DXF: Final = 'image/vnd.dxf'

The media type of a drawing in AutoCAD's exchange format, .dxf.

IDML module-attribute

IDML: Final = 'application/vnd.adobe.indesign-idml-package'

The media type of an InDesign markup package, .idml.

OUTLINE_LIMIT module-attribute

OUTLINE_LIMIT: Final = 4000

The most characters an outline holds: what the agent sees of a file by default.

PDF module-attribute

PDF: Final = 'application/pdf'

The media type of a PDF document, .pdf.

PPTX module-attribute

PPTX: Final = "application/vnd.openxmlformats-officedocument.presentationml.presentation"

The media type of a PowerPoint presentation, .pptx.

PSD module-attribute

PSD: Final = 'image/vnd.adobe.photoshop'

The media type of a Photoshop document, .psd.

SVG module-attribute

SVG: Final = 'image/svg+xml'

The media type of an SVG image, .svg.

XLSX module-attribute

XLSX: Final = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"

The media type of an Excel workbook, .xlsx.

Sha256 module-attribute

Sha256 = Annotated[str, Field(pattern='^[0-9a-f]{64}$')]

A SHA-256 digest, as 64 lowercase hexadecimal digits.

Source module-attribute

Source = Literal['upload', 'check_in', 'commands']

How a revision's content was made: uploaded, checked in by a person, or by an app's commands.

Blob pydantic-model

Bases: BaseModel

Bytes in the blob store, named by their digest within one artifact.

Config:

  • frozen: True
  • extra: forbid

Fields:

NativeFile pydantic-model

Bases: Artifact

A file in a native format: its manifest. Subclass it once per format.

Agents' edits are proposals by default (write_policy), and write policies bind agents only: a person's check-in commits directly.

Config:

  • extra: forbid
  • validate_assignment: True

Fields:

Validators:

  • _of_its_format

formats class-attribute

formats: frozenset[str]

The media types its content may have.

name pydantic-field

name: str

The file's name, such as contract.docx.

fingerprint pydantic-field

fingerprint: Sha256

The digest of the content, normalized: what each save stamps, such as the time, left out, so that a save that changes nothing else makes no revision.

text pydantic-field

text: Blob | None = None

The file's words as plain text, for diffs and the agent.

review pydantic-field

review: Blob | None = None

Its comments and tracked changes, as JSON.

render_for_agent

render_for_agent() -> str

Return the file's name, its pages if they are known, and its outline.

It does no IO: the agent sees what the app read from the file when the revision was made. Its words, review and pages are read through the package's tools.

Provenance pydantic-model

Bases: BaseModel

How a revision's content was made. Who made it is the revision's actor.

It is a claim: any surface that commits edit_artifact can write anything here, so nothing is authorized by it.

Config:

  • frozen: True
  • extra: forbid

Fields:

app pydantic-field

app: str | None = None

The app that made it, such as wordcraft.

build pydantic-field

build: str | None = None

The digest of the image the app ran in, when a worker ran it.

base pydantic-field

base: Sha256 | None = None

The content digest it was made from; None for a new file.

commands pydantic-field

commands: Blob | None = None

The batch of commands that made it, as canonical JSON.

WordDocument pydantic-model

Bases: NativeFile

A Word document, .docx: registered as word_document.

Fields:

Validators:

  • _of_its_format

revise

revise(
    artifact_id: ArtifactId,
    after: NativeFile,
    *,
    base_version: int,
    tested: str,
    summary: str,
    thread_id: ThreadId | None = None,
) -> EditArtifact

Build the edit that gives an artifact after's content, if its content is tested.

Parameters:

Name Type Description Default
artifact_id ArtifactId

The native file to revise.

required
after NativeFile

The manifest it is to have; its name is not changed.

required
base_version int

The version the edit is based on.

required
tested str

The content digest the edit was made from, which the patch tests first.

required
summary str

The change note, from the batch or the check-in.

required
thread_id ThreadId | None

The thread the edit is made in, if any.

None

artifactr.files.blobs

The blob store: the bytes of native files, by tenant, workspace, artifact and digest.

Blobs is the port, with InMemoryBlobs here and SqlBlobs in the sql extra. Bytes are put first, then the revision that names them is committed; what no revision or pending proposal names is collected later, so a put that was never committed is only garbage.

DEFAULT_LIMIT module-attribute

DEFAULT_LIMIT: Final = 25 * MIB

How large a blob may be unless the store is told otherwise.

MOST module-attribute

MOST: Final = 100 * MIB

The largest limit a store may be given.

Clock module-attribute

Clock = Callable[[], datetime]

Returns the current time; injectable, so that tests control when a blob was put.

InMemoryBlobs

InMemoryBlobs(
    *, clock: Clock = utc_now, limit: int = DEFAULT_LIMIT
)

The whole Blobs port in memory: nothing outlives it.

Parameters:

Name Type Description Default
clock Clock

Returns the current time, which a put is stamped with.

utc_now
limit int

The largest blob, in bytes: 25 MiB unless given, and at most 100 MiB.

DEFAULT_LIMIT

put async

put(
    scope: Scope,
    artifact_id: ArtifactId,
    data: AsyncIterable[bytes],
    media_type: str,
) -> Blob

Store bytes for an artifact, and name them, as Blobs.put does.

get async

get(
    scope: Scope, artifact_id: ArtifactId, sha256: str
) -> bytes | None

Return an artifact's bytes with this digest, or None if it has none.

collect async

collect(
    scope: Scope,
    referenced: Set[tuple[ArtifactId, str]],
    *,
    before: datetime,
) -> int

Delete the workspace's unreferenced blobs last put before before; return how many.

Blobs

Bases: Protocol

Where the bytes of native files are kept, by artifact and digest.

put async

put(
    scope: Scope,
    artifact_id: ArtifactId,
    data: AsyncIterable[bytes],
    media_type: str,
) -> Blob

Store bytes for an artifact, and name them.

The digest and size are computed as the bytes arrive, never taken from a client. Putting bytes the artifact already has stores nothing new, and marks them as put now, so that collect keeps them for the commit that follows.

Raises:

Type Description
TooLarge

If the bytes are larger than the store's limit.

get async

get(
    scope: Scope, artifact_id: ArtifactId, sha256: str
) -> bytes | None

Return an artifact's bytes with this digest, or None if it has none.

collect async

collect(
    scope: Scope,
    referenced: Set[tuple[ArtifactId, str]],
    *,
    before: datetime,
) -> int

Delete the workspace's blobs last put before before that aren't referenced.

A blob put again meanwhile is kept, whatever referenced says.

Parameters:

Name Type Description Default
scope Scope

The workspace.

required
referenced Set[tuple[ArtifactId, str]]

Every (artifact, digest) a revision or a pending proposal names.

required
before datetime

Only blobs last put before this, such as a day ago.

required

Returns:

Type Description
int

How many were deleted.

TooLarge

TooLarge(limit: int)

Bases: Exception

The bytes are larger than the store's limit; nothing was stored.

Attributes:

Name Type Description
limit

The store's limit, in bytes.

once async

once(data: bytes) -> AsyncIterable[bytes]

Bytes already in memory, as the stream that Blobs.put takes.

artifactr.files.checkouts

Check-outs: the record behind a person's lease on a native file.

Checkouts is the port, with InMemoryCheckouts here and SqlCheckouts in the sql extra.

InMemoryCheckouts

InMemoryCheckouts()

The whole Checkouts port in memory.

save async

save(scope: Scope, checkout: Checkout) -> None

Keep a check-out, replacing one with its id.

get async

get(scope: Scope, checkout_id: str) -> Checkout | None

Return a check-out, or None.

Checkout pydantic-model

Bases: BaseModel

A person's check-out of a native file.

Config:

  • frozen: True

Fields:

base_version pydantic-field

base_version: int

The version checked out, which a check-in is based on.

base_sha256 pydantic-field

base_sha256: Sha256

The content digest checked out, which a check-in's patch tests.

actor pydantic-field

actor: Actor

Who checked it out: only they check it in or extend it.

expires_at pydantic-field

expires_at: datetime

When the lease ends, unless it is extended.

checked_in pydantic-field

checked_in: bool = False

Whether it was checked in, which ends it: only the same file is checked in again.

holder property

holder: str

The lease's holder: checkout:<id>:<participant>.

Checkouts

Bases: Protocol

Where check-outs are kept, by workspace and id.

save async

save(scope: Scope, checkout: Checkout) -> None

Keep a check-out, replacing one with its id.

get async

get(scope: Scope, checkout_id: str) -> Checkout | None

Return a check-out, or None.

holder_of

holder_of(checkout_id: str, actor: Actor) -> str

The holder of a check-out's lease, which names the check-out and who has it.

artifactr.files.app

The app port: a native application, run headless, that edits a file by its own commands.

App is the port, and FakeApp the fake that every pull request's tests run with. Each real app's adapter is a layer of its own, starting with WordCraft's, artifactr.files.wordcraft.

Failure module-attribute

Failure = Literal[
    "timeout", "crashed", "too_large", "refused"
]

How a call can fail: past its deadline, the app crashed, a limit was breached, or the app refused the file or a command.

Talk module-attribute

Talk = Literal[
    "save", "bridged", "commands", "polled", "pushed"
]

How much the app talks to the host while a person edits in it (lattice RFC-0003): save, only the bytes when the person saves (level 1); bridged, through the editor's shim, the host opens the file in the editor and hears each save and whether there are unsaved changes, but sends no commands (level 1+, native files ADR-0010); commands, the host's commands, with replies; polled, changes the host asks for; pushed, changes as they happen.

FakeApp

FakeApp(
    *,
    fails: Failure | None = None,
    integration: str | None = None,
)

A fake App, for tests: it reads and writes its own JSON files.

Parameters:

Name Type Description Default
fails Failure | None

How every call fails, to test what happens when the app does; None for never.

None
integration str | None

The key that switches it, if not its class's, files:fake: another, to stand in for one of several apps.

None

file staticmethod

file(
    *paragraphs: str, comments: Sequence[str] = ()
) -> bytes

Make a file, as an upload would bring it.

apply async

apply(
    source: bytes | None,
    commands: Sequence[AppCommand],
    *,
    deadline: float,
) -> Applied

Apply a batch of commands; refuse the batch if any command doesn't apply.

inspect async

inspect(source: bytes, *, deadline: float) -> Inspection

Read a file's fingerprint, outline, text, review and pages.

render async

render(
    source: bytes,
    page: int,
    *,
    scale: float,
    deadline: float,
) -> bytes

Render a page as a one-pixel PNG whose colour comes from the page's paragraphs.

commands async

commands(query: str) -> Sequence[CommandSpec]

Find the commands whose name or description mentions the query.

App

Bases: Protocol

A native application, run headless, that edits one format by its own commands.

Every call is given a deadline, in seconds, and fails with AppFailed when it can't finish.

name property

name: str

The app's name, such as wordcraft, which provenance records.

version property

version: str

The app's version, which provenance records.

build property

build: str | None

The digest of the image the app runs in, if it runs in one.

talk property

talk: Talk

How much the app talks to the host while a person edits in it.

At save, a person checks the file out, edits it in the app and checks it in.

integration property

integration: str

The key that switches the app on or off in a workspace, such as files:wordcraft.

apply async

apply(
    source: bytes | None,
    commands: Sequence[AppCommand],
    *,
    deadline: float,
) -> Applied

Apply a batch of commands to a file, or to a new one if source is None.

inspect async

inspect(source: bytes, *, deadline: float) -> Inspection

Read a file's fingerprint, outline, text, review and pages.

render async

render(
    source: bytes,
    page: int,
    *,
    scale: float,
    deadline: float,
) -> bytes

Render one page of a file, from 1, as a PNG.

commands async

commands(query: str) -> Sequence[CommandSpec]

Find the commands that match a query, ten at most.

AppCommand pydantic-model

Bases: BaseModel

One of an app's own commands, as an agent asks for it.

Config:

  • frozen: True
  • extra: forbid

Fields:

AppFailed

AppFailed(failure: Failure, message: str)

Bases: Exception

A call to the app failed, in one of the ways Failure names.

Attributes:

Name Type Description
failure Failure

How it failed.

Applied dataclass

Applied(content: bytes, inspection: Inspection)

A batch of commands applied: the new bytes, and what the app read from them.

CommandSpec pydantic-model

Bases: BaseModel

A command an app offers: its name, how it is called, and what it does.

Config:

  • frozen: True

Fields:

  • name (str)
  • signature (str)
  • description (str)

Inspection pydantic-model

Bases: BaseModel

What an app reads from a file.

Config:

  • frozen: True

Fields:

fingerprint pydantic-field

fingerprint: Sha256

The digest of the content, normalized: what each save stamps left out.

review pydantic-field

review: dict[str, JsonValue] = {}

Its comments and tracked changes.

media_type pydantic-field

media_type: str | None = None

The format the app read the file as, if it tells formats apart: a check-in in another format than the one checked out is refused, never converted.

artifactr.files.craft

The crafting apps: each declared once, and run headless through one of two dialects.

CRAFTS declares every app, a Craft each, as its pinned release offers it. An app's adapter subclasses one of the two dialects its CLI's MCP server speaks, ToolsDialect (WordCraft's) or CommandsDialect (PdfCraft's), over Spawn, which starts the CLI for each call, confined to a directory of the call's own (native files ADR-0009).

PREVIEW module-attribute

PREVIEW: Final = 1024

How many pixels a page's longer side is rendered at, at scale 1.

FOUND module-attribute

FOUND: Final = 10

How many commands commands returns at most.

CRAFTS module-attribute

CRAFTS: Final = _registry(
    Craft(
        name="wordcraft",
        title="WordCraft",
        dialect="tools",
        roots="split",
        formats=frozenset({DOCX}),
        render=True,
        reach="file",
    ),
    Craft(
        name="gridcraft",
        title="GridCraft",
        dialect="tools",
        roots="none",
        formats=frozenset({XLSX}),
        render=False,
        reach="file",
        bridge="parent",
    ),
    Craft(
        name="deckcraft",
        title="DeckCraft",
        dialect="tools",
        roots="none",
        formats=frozenset({PPTX}),
        render=True,
        reach="file",
    ),
    Craft(
        name="pdfcraft",
        title="PdfCraft",
        dialect="commands",
        roots="single",
        formats=frozenset({PDF}),
        render=True,
        reach="file",
        bridge="parent",
        notices=(
            Notice(
                licence="CC-BY-SA-4.0",
                material="The text detection and recognition models its OCR runs (ocrs-models)",
                attribution="Robert Knight, ocrs-models (https://github.com/robertknight/ocrs-models), trained on HierText",
                files=(
                    "share/pdfcraft/models/ATTRIBUTION.txt",
                    "share/pdfcraft/models/text-detection.rten.LICENCE.txt",
                    "share/pdfcraft/models/text-recognition.rten.LICENCE.txt",
                ),
            ),
        ),
    ),
    Craft(
        name="cadcraft",
        title="CADCraft",
        dialect="tools",
        roots="none",
        formats=frozenset({DXF, DWG}),
        render=True,
        reach="file",
        bridge="parent",
    ),
    Craft(
        name="photocraft",
        title="PhotoCraft",
        dialect="commands",
        roots="split",
        formats=frozenset({PSD}),
        render=True,
        reach="file",
    ),
    Craft(
        name="vectorcraft",
        title="VectorCraft",
        dialect="commands",
        roots="split",
        formats=frozenset({SVG}),
        render=True,
        reach="file",
    ),
    Craft(
        name="designcraft",
        title="DesignCraft",
        dialect="tools",
        roots="split",
        formats=frozenset({IDML}),
        render=True,
        reach="file",
    ),
    Craft(
        name="figcraft",
        title="FigCraft",
        dialect="tools",
        roots="none",
        formats=frozenset(),
        render=False,
        reach="file",
    ),
    Craft(
        name="filmcraft",
        title="FilmCraft",
        dialect="commands",
        roots="none",
        formats=frozenset(),
        render=True,
        reach="export",
        bridge="global",
    ),
    Craft(
        name="effectcraft",
        title="EffectCraft",
        dialect="commands",
        roots="split",
        formats=frozenset(),
        render=True,
        reach="export",
        bridge="global",
    ),
    Craft(
        name="soundcraft",
        title="SoundCraft",
        dialect="tools",
        roots="split",
        formats=frozenset(),
        render=False,
        reach="export",
    ),
    Craft(
        name="lightcraft",
        title="LightCraft",
        dialect="commands",
        roots="none",
        formats=frozenset(),
        render=True,
        reach="export",
        bridge="global",
    ),
)

The crafting apps, by name, read-only: what each pinned release offers, checked on 2026-10-18 against its CLI's own MCP tools and options.

Bridge module-attribute

Bridge = Literal['parent', 'global']

The host surface of the browser build, as released: parent, a postMessage bridge to a same-origin parent page (?host=parent) that opens a file and hears its saves and whether it has unsaved changes; global, a JavaScript API on the page's window.

Dialect module-attribute

Dialect = Literal['tools', 'commands']

How the CLI's MCP server is driven: tools, a tool for each step (open_document, batch, save_document, list_commands), as WordCraft's; commands, every step a command run through command_run and command_batch, rendered by render_preview, as PdfCraft's and FilmCraft's (whose servers read a file through doc_inspect).

Reach module-attribute

Reach = Literal['file', 'export']

What lattice keeps of the app's work: file, the file itself, as a native file that people and agents edit; export, only what the app renders or exports from a project of its own, as a single file, since its projects point at media outside them.

Roots module-attribute

Roots = Literal['split', 'single', 'none']

How the CLI confines the files it reads and writes: split, to a read root and a write root (--automation-read-root, --automation-write-root); single, to one root for both (--root); none, to nothing, so it reads and writes wherever its user can.

CommandsDialect

CommandsDialect(
    cli: str | Path,
    *,
    version: str,
    arguments: Sequence[str] = (),
    trusted: bool = False,
    limit: int = 256 << 20,
    lookup: float = 10.0,
)

Bases: Headless

An app whose CLI's MCP server runs every step as a command, spawned for every call.

A subclass names its app's craft, its files' extension, and how it reads a file, inspection; and, where they differ from PdfCraft's, its commands and the parameter that names the open document.

new_command class-attribute

new_command: str | None = None

The command that makes a blank file, or None if the server has none headless.

handle class-attribute

handle: str | None = 'doc'

The parameter by which each command names the open document, as the open command's result names it; None for a server whose commands act on the document open last.

open async

open(server: Server, source: bytes | None) -> JsonValue

Open the source, or make a blank file, and return what the server calls it.

Raises:

Type Description
AppFailed

crashed too if the open command's result names no document.

batch async

batch(
    server: Server,
    document: JsonValue,
    commands: Sequence[AppCommand],
) -> None

Run the commands through command_batch, which stops at the first that fails.

A step its reply says failed refuses the batch, whether the reply is an error or not.

save async

save(server: Server, document: JsonValue) -> bytes

Save the file into the write root by the save command, and return its bytes.

page async

page(
    server: Server,
    document: JsonValue,
    page: int,
    scale: float,
) -> bytes

Render one page through render_preview, its longer side PREVIEW times scale.

find async

find(server: Server, query: str) -> Sequence[CommandSpec]

The commands command_list finds for the query, ten at most.

params

params(
    document: JsonValue, arguments: Mapping[str, JsonValue]
) -> dict[str, JsonValue]

A command's parameters, naming the open document as the server called it.

Headless

Headless(
    cli: str | Path,
    *,
    version: str,
    arguments: Sequence[str] = (),
    trusted: bool = False,
    limit: int = 256 << 20,
    lookup: float = 10.0,
)

Bases: ABC

A crafting app's CLI, spawned for every call: an App.

Parameters:

Name Type Description Default
cli str | Path

The app's CLI, <app>-cli.

required
version str

The version it must be, which provenance records: a CLI of another version is refused.

required
arguments Sequence[str]

What the CLI is given after mcp, before the roots.

()
trusted bool

Whether its files are trusted, or this process confined: required of an app whose CLI confines nothing.

False
limit int

The most bytes a file may hold, saved or inflated: 256 MiB by default.

256 << 20
lookup float

How long, in seconds, finding commands may take: 10 by default.

10.0

Raises:

Type Description
ValueError

If the app's CLI confines nothing and trusted isn't given.

extension class-attribute

extension: str

What the source and the saved file are named with, such as .docx.

build class-attribute instance-attribute

build: str | None = None

It runs on this machine, in no image.

name property

name: str

The app's name, which provenance records.

talk property

talk: Talk

bridged if its build has a ?host=parent bridge, which the shim reaches; else save.

A JavaScript API could carry commands, but no app's has been run through the shim, so none is claimed.

integration property

integration: str

The key that switches it in a workspace, files:<name>.

apply async

apply(
    source: bytes | None,
    commands: Sequence[AppCommand],
    *,
    deadline: float,
) -> Applied

Run a batch of commands on a file, or a blank one, and save it.

The batch stops at the first command that fails, which refuses it whole.

inspect async

inspect(source: bytes, *, deadline: float) -> Inspection

Read a file's fingerprint, outline, text, review and pages.

render async

render(
    source: bytes,
    page: int,
    *,
    scale: float,
    deadline: float,
) -> bytes

Render one page of a file, from 1, as a PNG, at scale times the app's own size.

Raises:

Type Description
AppFailed

refused too if the app renders no page headless.

commands async

commands(query: str) -> Sequence[CommandSpec]

Find the commands whose id, label or place in the app mentions the query.

open abstractmethod async

open(server: Server, source: bytes | None) -> JsonValue

Open the source, or a blank file if it is None, and return what the server calls it.

batch abstractmethod async

batch(
    server: Server,
    document: JsonValue,
    commands: Sequence[AppCommand],
) -> None

Run the commands in order on the open file, refused whole if one fails.

save abstractmethod async

save(server: Server, document: JsonValue) -> bytes

Save the open file into the write root, and return its bytes.

page abstractmethod async

page(
    server: Server,
    document: JsonValue,
    page: int,
    scale: float,
) -> bytes

Render one page of the open file, from 1, as a PNG.

find abstractmethod async

find(server: Server, query: str) -> Sequence[CommandSpec]

The commands that match the query, ten at most.

inspection abstractmethod async

inspection(
    server: Server, document: JsonValue, content: bytes
) -> Inspection

Read the open file, whose bytes are content, into an inspection: each app's own.

It names the format it read the file as, media_type, which a check-in is held to.

Craft dataclass

Craft(
    name: str,
    title: str,
    dialect: Dialect,
    roots: Roots,
    formats: frozenset[str],
    render: bool,
    reach: Reach,
    bridge: Bridge | None = None,
    notices: tuple[Notice, ...] = (),
)

One crafting app: what its pinned release offers, and how lattice may use it.

name instance-attribute

name: str

Its name in lowercase, such as wordcraft: of its key, its CLI and its repository.

title instance-attribute

title: str

Its name as written, such as WordCraft.

formats instance-attribute

formats: frozenset[str]

The media types of the native files it keeps; none for an app whose formats have no registered type yet, or whose reach is export.

render instance-attribute

render: bool

Whether its CLI renders a page as a PNG headless.

key property

key: str

The integration that switches it in a workspace: files:<name>.

integration property

integration: Integration

Its integration in lattice-core's registry: its default, and its admin's sentence.

cli property

cli: str

Its CLI's name: <name>-cli.

repository property

repository: str

Where it is released: storytold/<name> on GitHub.

Notice dataclass

Notice(
    licence: str,
    material: str,
    attribution: str,
    files: tuple[str, ...],
)

Third-party material a release carries beside its CLI, under a licence of its own.

Whoever ships the CLI, such as the worker's image, ships its files with it.

licence instance-attribute

licence: str

Its SPDX identifier, such as CC-BY-SA-4.0.

material instance-attribute

material: str

What it is.

attribution instance-attribute

attribution: str

Whose it is, and where it comes from, as its own attribution gives it.

files instance-attribute

files: tuple[str, ...]

The attribution and licence files, as the release's Linux tarball holds them.

Server

Server(
    session: ClientSession,
    read_root: Path,
    write_root: Path,
    limit: int,
)

The CLI's MCP server for one call, and the directories it may read and write.

Attributes:

Name Type Description
session

The MCP session, initialized.

read_root

Where the call's source is written, for the CLI to open.

write_root

Where the CLI saves what it is asked to.

source

source(content: bytes, name: str) -> Path

Write the call's source into the read root, and return its path.

saved

saved(path: Path) -> bytes

The bytes the CLI saved at path, refused as too large past the limit.

call async

call(tool: str, **arguments: JsonValue) -> CallToolResult

Call a tool, and refuse with its reply's text if it failed.

text async

text(tool: str, **arguments: JsonValue) -> str

Call a tool, and return its reply's text.

json async

json(tool: str, **arguments: JsonValue) -> JsonValue

Call a tool, and return its reply's structured content, or its text read as JSON.

Raises:

Type Description
AppFailed

crashed if it replied with neither.

png async

png(tool: str, **arguments: JsonValue) -> bytes

Call a tool that renders, and return the PNG it replied with.

Spawn

Spawn(
    craft: Craft,
    cli: str | Path,
    *,
    version: str,
    arguments: Sequence[str] = (),
    trusted: bool = False,
    limit: int,
)

Starts a crafting app's CLI as an MCP server on stdio, once for each call.

The CLI's file access is confined to the call's own directory by its root flags. An app whose CLI confines nothing (its descriptor's roots is none) reads and writes wherever this process's user can, so it is started only when the operator says the files are trusted, or that this process is already confined, as the sandboxed worker is. Either way it is not a sandbox: the CLI runs as this process's user, with its network.

Parameters:

Name Type Description Default
craft Craft

The app.

required
cli str | Path

Its CLI, <app>-cli.

required
version str

The version it must be, which provenance records: a CLI of another version is refused.

required
arguments Sequence[str]

What the CLI is given after mcp, before the roots.

()
trusted bool

Whether the files are trusted, or this process confined: required of an app whose CLI confines nothing.

False
limit int

The most bytes a saved file may hold.

required

Raises:

Type Description
ValueError

If the app's CLI confines nothing and trusted isn't given.

run async

run(
    deadline: float,
    work: Callable[[Server], Awaitable[T]],
) -> T

Spawn the CLI in a directory of the call's own, do the work, and end both.

Raises:

Type Description
AppFailed

timeout past the deadline; crashed if the CLI ended, or its replies made no sense, mid-call; refused if it is another version, or the work refused.

ToolsDialect

ToolsDialect(
    cli: str | Path,
    *,
    version: str,
    arguments: Sequence[str] = (),
    trusted: bool = False,
    limit: int = 256 << 20,
    lookup: float = 10.0,
)

Bases: Headless

An app whose CLI's MCP server has a tool for each step, spawned for every call.

A subclass names its app's craft, its files' extension, its tools, and how it reads a file, inspection.

open async

open(server: Server, source: bytes | None) -> JsonValue

Open the source with the open tool, or make a blank file with the new tool.

The server holds one file, so its tools name none: None.

batch async

batch(
    server: Server,
    document: JsonValue,
    commands: Sequence[AppCommand],
) -> None

Run the commands as one batch, refused whole, naming the command that failed.

save async

save(server: Server, document: JsonValue) -> bytes

Save the file into the write root with the save tool, and return its bytes.

page async

page(
    server: Server,
    document: JsonValue,
    page: int,
    scale: float,
) -> bytes

Render one page with the render tool, at scale pixels per point.

find async

find(server: Server, query: str) -> Sequence[CommandSpec]

The commands the list tool finds for the query, ten at most.

artifactr.files.wordcraft

WordCraft's adapter: .docx files edited by WordCraft's own commands, run headless.

Subprocess spawns WordCraft's CLI on this machine, a fresh MCP server on stdio for every call, for tests and trusted files. The fingerprint is read from the file itself, by fingerprint: the digest of its parts without the one each save stamps (lattice RFC-0003, phase 2).

STAMPED module-attribute

STAMPED: Final = frozenset({'docProps/core.xml'})

The parts every save stamps, which the fingerprint leaves out.

Subprocess

Subprocess(
    cli: str | Path,
    *,
    version: str,
    author: str = "lattice",
    limit: int = 256 << 20,
    lookup: float = 10.0,
)

Bases: ToolsDialect

WordCraft's headless MCP server, spawned from its CLI on this machine for every call.

It confines the CLI's file access to the call's own directory, but it is not a sandbox: the CLI runs as this process's user, with its network. It is for tests and trusted files; the sandboxed worker runs hostile ones (lattice RFC-0003).

Parameters:

Name Type Description Default
cli str | Path

The wordcraft-cli executable.

required
version str

The WordCraft version it must be, which provenance records: a call to a CLI of another version is refused.

required
author str

Who the CLI says tracked changes, comments and saves are by.

'lattice'
limit int

The most bytes a file may hold, saved or inflated: 256 MiB by default.

256 << 20
lookup float

How long, in seconds, finding commands may take: 10 by default.

10.0

inspection async

inspection(
    server: Server, document: JsonValue, content: bytes
) -> Inspection

The document's text, headings, comments, tracked changes, pages and fingerprint.

fingerprint

fingerprint(content: bytes, *, limit: int) -> str

The digest of a .docx's parts, by name, without the parts each save stamps.

Each part is hashed on its own, and the fingerprint is the digest of the parts' names and digests, so no two different files share one. Each part is read as it inflates, so a zip bomb is refused once it passes limit.

Raises:

Type Description
AppFailed

refused if the bytes aren't a zip, or name a part twice, and too_large if its parts, inflated, pass limit bytes.

media_type

media_type(content: bytes) -> str | None

The format a package says it is, or None if it says none.

A Word package says it by its main part's content type, and an OpenDocument by its mimetype. Call it once fingerprint has read the package, which refuses one that isn't a zip or inflates past its limit.

Raises:

Type Description
AppFailed

refused if its content types aren't XML.

outline

outline(blocks: Sequence[Block]) -> str

The headings among the blocks, indented by level, or else every block's first line.

A title counts as a heading of the first level.

artifactr.files.native

Native files in workspaces: NativeFiles.

Uploads, a person's check-out and check-in, an agent's edit by the app's commands, reads of a revision's bytes, and the collection of bytes nothing names.

Part module-attribute

Part = Literal['content', 'text', 'review']

What of a revision to read: the file, its words, or its comments and tracked changes.

ContentUnavailable

ContentUnavailable(artifact_id: ArtifactId, part: Part)

Bases: InvalidState

A manifest names bytes the blob store doesn't have.

Every surface can commit edit_artifact, so a hand-written patch can name a digest that was never put. Reads show the content as unavailable, and edits are refused until it is replaced.

FormatChanged

FormatChanged(
    artifact_id: ArtifactId,
    *,
    checked_out: str,
    checked_in: str,
)

Bases: InvalidState

A check-in is in another format than the file checked out, which is never converted.

An editor can save a file in another format than it opened, such as a workbook opened from .csv saved as .xlsx, or a layout opened from INDD saved as IDML. The head keeps its format: check the file in as a new one instead (native files ADR-0012).

details

details() -> dict[str, JsonValue]

Return the format checked out and the one checked in.

NativeFiles

NativeFiles(
    blobs: Blobs,
    checkouts: Checkouts,
    app: App | Mapping[type[NativeFile], App],
    *,
    checkout_ttl: timedelta = timedelta(hours=8),
    deadline: float = 30.0,
    edit_ttl: timedelta = timedelta(seconds=45),
    keep: timedelta = timedelta(days=1),
    clock: Clock = utc_now,
    integrations: Integrations | None = None,
)

Native files in workspaces: their bytes, their check-outs and the apps that read them.

Every method takes the Workspace to act in, as the actor who acts, and reads and writes through it, so authorize and the hiding of restricted artifacts apply as for any read or command.

Parameters:

Name Type Description Default
blobs Blobs

Where the files' bytes are kept.

required
checkouts Checkouts

Where check-outs are kept.

required
app App | Mapping[type[NativeFile], App]

The app that reads every native file and runs agents' commands on it; or, for several, the app for each native type, a subclass's own or else its nearest base's.

required
checkout_ttl timedelta

How long a check-out lasts, unless it is extended: eight hours by default.

timedelta(hours=8)
deadline float

How long, in seconds, a call to the app may take: 30 by default.

30.0
edit_ttl timedelta

How long an agent's edit holds the lease: the app's deadline and the commit, 45 seconds by default. It is never renewed: an edit that outlives it is still refused at commit, by its base version and its test, if someone else committed meanwhile.

timedelta(seconds=45)
keep timedelta

How long bytes no revision names are kept after they were last put, for the commit that follows a put: a day by default.

timedelta(days=1)
clock Clock

Returns the current time, which collection keeps bytes from.

utc_now
integrations Integrations | None

Whether a file's app's integration, app.integration, is on in a workspace: while it is off there, none of its files is uploaded, checked out, extended, checked in or edited, and each refuses with NotEnabled; reads and collection go on. None, the default, is on everywhere.

None

Raises:

Type Description
ValueError

If an edit's lease would end before the apps' deadline, or no app is given.

app_of

app_of(file_type: type[NativeFile]) -> App

The app that reads a native type: its own, or else its nearest base's.

Raises:

Type Description
ValidationFailed

If no app reads it.

enabled async

enabled(
    ws: Workspace, file_type: type[NativeFile] | None = None
) -> bool

Whether the app of a native type is on in the workspace; or, without one, any app.

A type no app reads is off.

key_of

key_of(
    kind: str, data: Mapping[str, JsonValue]
) -> str | None

The integration that governs an artifact: its app's for a native file, else None.

It is the key_of of artifactr's integration_gate, so that while a file's app is off, an edit of the file is refused on every surface, REST and MCP too, not only here. A native type no app reads is governed by none, and no app edits it.

upload async

upload(
    ws: Workspace,
    file_type: type[F],
    name: str,
    data: AsyncIterable[bytes],
    *,
    media_type: str,
    artifact_id: ArtifactId | None = None,
    thread_id: ThreadId | None = None,
) -> Applied | Proposed

Create a native file from uploaded bytes, with a new id unless one is given.

Raises:

Type Description
NotEnabled

If the app's integration is off in the workspace.

ValidationFailed

If the type doesn't hold files of media_type, no app reads it, or its app reads the file as another format.

TooLarge

If the file is larger than the blob store's limit.

AppFailed

If the app can't read it.

check_out async

check_out(
    ws: Workspace, artifact_id: ArtifactId
) -> Checkout

Check a native file out to the workspace's actor, holding its lease.

The lease is taken before the head is read, so the version checked out is the one the lease holds the file at.

Raises:

Type Description
NotEnabled

If the app's integration is off in the workspace.

ArtifactBusy

If someone else has the file: a check-out, or an agent's edit.

NotFound

If there is no such native file, or it is hidden from the actor.

extend async

extend(ws: Workspace, checkout_id: str) -> Checkout

Extend a check-out by its whole length again, from now.

Raises:

Type Description
NotEnabled

If the app's integration is off in the workspace.

NotFound

If there is no such check-out.

Forbidden

If it is someone else's.

ArtifactBusy

If its lease ended and someone else has the file now.

InvalidState

If it was checked in.

check_in async

check_in(
    ws: Workspace,
    checkout_id: str,
    data: AsyncIterable[bytes],
    *,
    summary: str | None = None,
    media_type: str | None = None,
) -> Applied | Proposed | None

Check a file in: a revision from the version checked out, and the lease released.

The base is the check-out's, never the upload's. The lease is taken again first, which renews one that lapsed if no one else took it. A file whose fingerprint is the head's commits nothing, so a check-in retried after it succeeded does nothing. A check-in that is stale, because its lease lapsed and someone else took the file or committed meanwhile, is kept as a proposal against the head, which tests the head's digest and records the check-out's base as its provenance. Either way the check-out ends: its record is closed and its lease released.

The file must be in the format checked out: one that says it is another, by media_type, or that the app reads as another, is refused, and never converted.

Returns:

Type Description
Applied | Proposed | None

The revision's outcome, a proposal's if it was stale, or None if nothing changed.

Raises:

Type Description
NotEnabled

If the app's integration is off in the workspace.

NotFound

If there is no such check-out, or its file is hidden from the actor.

Forbidden

If it is someone else's.

FormatChanged

If the file is in another format than the one checked out.

InvalidState

If it was checked in already, and this file is another.

TooLarge

If the file is larger than the blob store's limit.

AppFailed

If the app can't read it.

edit async

edit(
    ws: Workspace,
    artifact_id: ArtifactId,
    *,
    base_version: int,
    commands: Sequence[AppCommand],
    summary: str,
    thread_id: ThreadId | None = None,
) -> Applied | Proposed | None

Edit a native file by a batch of its app's commands, holding its lease meanwhile.

For an agent, under the type's propose write policy, the revision is a proposal.

Returns:

Type Description
Applied | Proposed | None

The revision's outcome, or None if the batch changed nothing.

Raises:

Type Description
NotEnabled

If the app's integration is off in the workspace.

NotFound

If there is no such native file, or it is hidden from the actor.

VersionConflict

If base_version isn't the head's: the agent plans again.

ArtifactBusy

If someone else has the file, such as a person's check-out. The agent plans again after the check-in, or at once if the busy lease is None.

ContentUnavailable

If the head's bytes aren't in the blob store.

AppFailed

If the app can't run the batch.

read async

read(
    ws: Workspace,
    artifact_id: ArtifactId,
    *,
    version: int | None = None,
    part: Part = "content",
) -> bytes

Return a native file's bytes, its text or its review, at its head or a version.

The artifact is resolved through the workspace first, which asks authorize and hides restricted artifacts, and only then are the bytes its revision names read.

Raises:

Type Description
NotFound

If there is no such native file or version, or the file is hidden.

ContentUnavailable

If the revision names nothing for part, or bytes the blob store doesn't have.

render async

render(
    ws: Workspace,
    artifact_id: ArtifactId,
    page: int,
    *,
    version: int | None = None,
) -> bytes

Render one page of a native file, from 1, at its head or a version, as a PNG.

Raises:

Type Description
NotFound

If there is no such native file, or it is hidden.

ContentUnavailable

If the blob store doesn't have its bytes.

AppFailed

If the app can't render it, or there is no such page.

proposed async

proposed(
    ws: Workspace,
    proposal_id: ProposalId,
    *,
    part: Part = "content",
) -> bytes

Return what a proposal would make of a native file: its bytes, its text or its review.

A stale check-in is kept as a proposal, and its reviewer reads it beside the head. The proposal is read through the workspace, and then its file, as a read of either is, so a hidden one is answered as a missing one.

Raises:

Type Description
NotFound

If there is no such proposal, it is to no native file, or either is hidden.

ContentUnavailable

If the proposal names nothing for part, as one that doesn't edit the file's content, or bytes the blob store doesn't have.

render_proposed async

render_proposed(
    ws: Workspace, proposal_id: ProposalId, page: int
) -> bytes

Render one page of what a proposal would make of a native file, from 1, as a PNG.

Raises:

Type Description
NotFound

If there is no such proposal, it is to no native file, or either is hidden.

ContentUnavailable

If the proposal names no content, or the blob store hasn't it.

AppFailed

If the app can't render it, or there is no such page.

collect async

collect(ws: Workspace) -> int

Delete the workspace's blobs that nothing names, unless they were put lately.

Named means by a pending proposal, or by any revision of a native file, archived ones too; the proposals are read first, so that one accepted meanwhile is read as a revision. Bytes put within keep are kept whatever names them, for the commit that follows a put. The handle must see every thread and artifact, as an application's own actor does: a blob that only a hidden revision names would otherwise be lost.

Returns:

Type Description
int

How many blobs were deleted.

Raises:

Type Description
Forbidden

If the handle's actor can't see everything in the workspace.

artifactr.files.tools

The agent's tools for native files, in one toolset.

A pydantic-ai toolset beside artifactr's own, offered only where the app's integration is on: reading a file's words and its review, finding the app's commands, editing by a batch of them, and, if the application turns it on, looking at a page (lattice RFC-0003, phase 3).

REVIEW_ITEMS module-attribute

REVIEW_ITEMS: Final = 50

The most comments and tracked changes one read_review returns.

TEXT_CHARS module-attribute

TEXT_CHARS: Final = 8000

The most of a file's words one read_native returns.

native_file_tools

native_file_tools(
    files: NativeFiles, *, view: bool = False
) -> AbstractToolset[Session[Any]]

Build the toolset for native files, through files and its app.

Parameters:

Name Type Description Default
files NativeFiles

The native files, and the app that reads and edits them.

required
view bool

Whether to offer view_page, which shows the model one rendered page: off by default, since a page is an image's worth of tokens.

False

artifactr.files.fastapi

People's check-out and check-in over HTTP, with the fastapi extra.

files_router is the REST surface, mounted beside artifactr's own router: check out, extend, check in, download a version and fetch a page. editor_app serves an app's browser build, as released, on an origin of its own, under a policy that lets it reach nothing else (lattice RFC-0003, phase 4).

CheckedIn pydantic-model

Bases: BaseModel

What a check-in made: a revision, a proposal if it was stale, or nothing.

Fields:

  • outcome (Literal['applied', 'proposed', 'unchanged'])
  • version (int | None)
  • proposal_id (str | None)

FileStatus pydantic-model

Bases: BaseModel

A native file as a person about to check it out sees it.

Fields:

lease pydantic-field

lease: Lease | None

Who has it now, if anyone.

talk pydantic-field

talk: Talk

How much the app talks to the host while a person edits in it: at save, the person downloads the file, edits it in the app and uploads it again.

enabled pydantic-field

enabled: bool

Whether the app is on in the workspace: off, the file is read but not checked out.

limit pydantic-field

limit: int

The largest file, in bytes, that a check-in takes.

Lease pydantic-model

Bases: BaseModel

Who has a native file, until when.

Fields:

checked_out_by pydantic-field

checked_out_by: str | None

The participant whose check-out holds it, or None for an agent's edit, held for seconds.

checkout_id pydantic-field

checkout_id: str | None = None

The check-out's id, only when it is the caller's own: to extend it or check it in, from another tab, or after a reload.

editor_app

editor_app(
    build: Path,
    *,
    craft: Craft | None = None,
    portal: str | None = None,
    methods: Sequence[str] = (),
) -> ASGIApp

Serve a browser build's directory, as released, under its policy; and its shim, if asked.

With craft and portal, it also serves the shim at /.lattice/shim.html, which the portal frames to open the checked-out file in the build and hear its saves.

Parameters:

Name Type Description Default
build Path

The directory holding the build's index.html, such as what scripts/craft wordcraft web prints.

required
craft Craft | None

The app whose build it is, if its build has a host surface for the shim to reach.

None
portal str | None

The portal's origin, the only one that may frame the shim, and that it answers.

None
methods Sequence[str]

For a build with a JavaScript API, the methods of it the portal may call.

()

Raises:

Type Description
FileNotFoundError

If the directory has no index.html.

ValueError

If only one of craft and portal is given, or the shim can't be made for them, or the build has a file where the shim is served.

editor_policy

editor_policy(
    index: str, *, portal: str | None = None
) -> str

The Content-Security-Policy for a build whose page is index.

Its own files, and blob: for what the page makes itself, such as a saved file; its inline scripts by their SHA-256, and WebAssembly compiled from its own bytes, and no other code; styles; and no form, base or plugin. Nothing frames it; or, given the portal that frames the shim, only the shim, of its own origin, inside the portal: a frame's ancestors are each checked, the top one too.

files_router

files_router(
    workspaces: Workspaces,
    files: NativeFiles,
    *,
    resolve_actor: ResolveActor,
    limit: int = DEFAULT_LIMIT,
) -> APIRouter

Build the router for native files' check-out and check-in, downloads and pages.

Mount it beside artifactr_router, with the same resolve_actor and workspaces.

Parameters:

Name Type Description Default
workspaces Workspaces

Opens tenant-scoped workspaces, and asks their authorize hook.

required
files NativeFiles

The native files, their stores and their app.

required
resolve_actor ResolveActor

Authenticates each request, as for artifactr's router.

required
limit int

The largest check-in, in bytes, refused from its Content-Length before any of it is read: give it the blob store's limit, which is 25 MiB unless the store is given another. A body that grows past the store's own limit as it streams is refused there.

DEFAULT_LIMIT

artifactr.files.sql

Blobs and check-outs in SQL: PostgreSQL or SQLite, through SQLAlchemy 2's asyncio extension.

Install artifactr-ai[files,postgres] (asyncpg) or artifactr-ai[files,sqlite] (aiosqlite). Upgrade the database with migrate, then give NativeFiles the stores:

engine = create_async_engine("postgresql+asyncpg://localhost/app")
await migrate(engine)
files = NativeFiles(SqlBlobs(engine), SqlCheckouts(engine), app)

With row-level security, migrate as the tables' owner, granting a role that owns nothing the tables, and give the stores that role's engine with row_level_security=True, as artifactr's storage has it (artifactr ADR-0066). For SQLite, create the engine with create_sqlite_engine. create_schema creates the tables without migrations, for tests and prototypes.

SqlBlobs

SqlBlobs(
    engine: AsyncEngine,
    *,
    clock: Clock = utc_now,
    limit: int = DEFAULT_LIMIT,
    row_level_security: bool = False,
)

The Blobs port in a SQL database.

Create the tables with migrate first. The same code runs on PostgreSQL and on SQLite, whose engines must come from create_sqlite_engine.

Parameters:

Name Type Description Default
engine AsyncEngine

The database. The store does not dispose of it.

required
clock Clock

Returns the current time, which a put is stamped with.

utc_now
limit int

The largest blob, in bytes: 25 MiB unless given, and at most 100 MiB. The bytes are held in memory as they arrive, up to it.

DEFAULT_LIMIT
row_level_security bool

Whether each transaction names its tenant to PostgreSQL, whose policies, which the migrations create, then show it only that tenant's rows.

False

put async

put(
    scope: Scope,
    artifact_id: ArtifactId,
    data: AsyncIterable[bytes],
    media_type: str,
) -> Blob

Store bytes for an artifact, and name them, as Blobs.put does.

get async

get(
    scope: Scope, artifact_id: ArtifactId, sha256: str
) -> bytes | None

Return an artifact's bytes with this digest, or None if it has none.

collect async

collect(
    scope: Scope,
    referenced: Set[tuple[ArtifactId, str]],
    *,
    before: datetime,
) -> int

Delete the workspace's unreferenced blobs last put before before; return how many.

The blobs old enough are read, and those not referenced deleted in batches, each statement checking again that the blob is still old enough: a blob put again meanwhile, for a revision about to be committed, is kept.

SqlCheckouts

SqlCheckouts(
    engine: AsyncEngine, *, row_level_security: bool = False
)

The Checkouts port in a SQL database.

Parameters:

Name Type Description Default
engine AsyncEngine

The database. The store does not dispose of it.

required
row_level_security bool

Whether each transaction names its tenant to PostgreSQL.

False

save async

save(scope: Scope, checkout: Checkout) -> None

Keep a check-out, replacing one with its id.

get async

get(scope: Scope, checkout_id: str) -> Checkout | None

Return a check-out, or None.

create_schema async

create_schema(engine: AsyncEngine) -> None

Create the tables straight from the models, for tests and prototypes.

It skips Alembic, so the database records no schema version and cannot be upgraded with migrate later. Use migrate for databases that last.

migrate async

migrate(
    engine: AsyncEngine,
    *,
    application_role: str | None = None,
) -> None

Upgrade the database to the native files' latest schema, creating the tables if needed.

Run it when the application deploys or starts, as the tables' owner. It is safe to run on every start: a database that is already up to date is left alone.

Parameters:

Name Type Description Default
engine AsyncEngine

The database, as the tables' owner.

required
application_role str | None

A role that owns nothing, which the application connects as with row-level security on: it is granted the tables, on PostgreSQL. It must exist.

None

create_sqlite_engine

create_sqlite_engine(
    url: str | URL, **kwargs: Any
) -> AsyncEngine

Create an async SQLite engine whose transactions take turns.

Every transaction on the engine, reads included, holds the database's write lock, so transactions run one at a time, on the engine's one connection. A transaction waits for the connection for up to the engine's pool_timeout (30 seconds), and for another process's lock for up to the timeout connect argument of Python's sqlite3 (5 seconds unless connect_args says otherwise). Use a database file (or a shared-cache memory database).

Parameters:

Name Type Description Default
url str | URL

A SQLite URL for an async driver, such as sqlite+aiosqlite:///app.db.

required
**kwargs Any

Passed to SQLAlchemy's create_async_engine. Give poolclass (with its own arguments) to pool connections differently.

{}

Returns:

Type Description
AsyncEngine

The engine. Dispose of it when you are done.