Status: repository contract for the Forge Assets platform work. The schema and migration behavior described here are source contracts; using them in a live Forge deployment still requires a separate Forge release and verification. Creating an asset migration does not deploy a site, change DNS, or retire an existing host.
Implemented and locally tested in this source snapshot: standard Git LFS Batch/basic transfer, account and repository reachability, strict config parsing, exact-SHA build hydration, unchanged-public-asset publication, and an administrator-only adoption API/UI that proposes ordinary-blob conversion and opens a migration pull request after every selected object is verified. These changes are not deployed until Forge is separately released and live verified.
Forge Assets combines two layers:
Git remains authoritative for which logical asset path belongs to an exact commit. Forge's control plane records which account and repository may reach a digest. Immutable object storage holds the bytes. Repository-authored files refer to logical SHA-256 digests; they never contain bucket names, object keys, credentials, or another provider's physical locator.
git-lfs binary?#No, not for Forge builds of already-managed pointers, the Sites adoption flow, or an agent that calls the standard LFS HTTP endpoints directly. The adoption flow uses the existing standard Forge LFS HTTP API for missing bytes; it does not invoke a local clean/smudge filter. API callers may implement Batch/basic requests directly and do not need the local helper binary.
Install git-lfs when a developer wants the familiar local Git LFS workflow:
git lfs install
git clone https://forge.smol.ai/<owner>/<repository>.gitWith the helper installed, checkout hydrates pointer files and push uploads missing objects through the standard Git LFS protocol. Without it, Git still clones and pushes the small pointer files; the working tree shows pointers instead of the large bytes. Forge builds are unaffected because hydration is a server-side responsibility.
| Workflow | Local git-lfs required? |
|---|---|
forgeBuild.ts migration assistant |
No; asset adoption is a separate Sites action |
| Agent using the standard LFS HTTP API directly | No |
| Forge exact-SHA build | No |
| Clone/push pointers with ordinary Git | No |
| Automatically hydrate and clean LFS files in a local checkout | Yes |
| Sites/API asset adoption and migration PR | No |
Forge accepts the standard Git LFS pointer and Batch/basic transfer contracts. This compatibility boundary is intentionally not a Forge-specific pointer format.
.forge/assets.json#A repository may add one strict configuration file at its root:
.forge/assets.json. The file declares reviewed asset intent; it does not
contain storage locations and it does not grant deployment or DNS authority.
{
"$schema": "https://forge.smol.ai/spec/assets/v1/schema.json",
"version": 1,
"mode": "hybrid",
"entries": [
{
"path": "apps/main/public",
"kind": "tree"
},
{
"path": "media/keynote-source.mp4",
"kind": "file"
}
]
}The public schema is
packages/cli/schema/assets-v1.json.
| Field | Contract |
|---|---|
$schema |
Optional editor hint. When present, it must name the Forge Assets v1 schema. |
version |
Required and exactly 1. |
mode |
Optional; defaults to and may only be "hybrid" in v1. Git continues to hold source and pointers while Forge manages digest bytes and build hydration. |
entries |
Required list of reviewed file or tree declarations. |
entries[].path |
A literal, normalized, repository-relative path. Globs, absolute paths, backslashes, NUL bytes, . segments, and .. traversal are invalid. |
entries[].kind |
"file" names one regular file. "tree" recursively names regular files below one directory. |
The file is limited to 64 KiB and 1,000 entries; each path is limited to 1,024 UTF-8 bytes. Paths must be unique and non-overlapping after normalization. The build path validates the strict source shape and discovers regular LFS pointer files. Adoption additionally rejects missing paths, symlinks, special files, and submodules before it writes a migration commit.
The file is optional. Without it, Forge still understands standard Git LFS pointers, but it does not silently convert ordinary Git blobs or infer that a directory should become a managed asset tree.
Forge resolves each build from its exact Git commit and then chooses the smallest conservative materialization. For an OpenNext build, Forge can keep a pointer out of the build sandbox only when all of these conditions hold:
.forge/assets.json explicitly covers the pointer;public/** tree; andThe deferred extensions in v1 are:
.7z .aac .avif .avi .bmp .bz2 .eot .flac .gif .gz .heic .heif .ico
.jpeg .jpg .jxl .m4a .m4v .mkv .mov .mp3 .mp4 .oga .ogg .opus .otf
.pdf .png .rar .svg .tar .tif .tiff .ttf .wav .webm .webp .woff
.woff2 .xz .zipEverything else is hydrated before the build. This includes text and data
files such as JSON, YAML, CSV, Markdown, and GraphQL; extensionless control
files; unknown extensions; pointers outside the reviewed public/** tree; and
all pointers for runtimes without this OpenNext publication path. That default
preserves framework behavior such as prerender imports and fs.readFile() of
files below public/.
The decision is deterministic: it uses the exact committed path, selected project root, reviewed configuration, runtime, and extension. An AI model does not decide build-time hydration, and repository code never receives object-store credentials.
V1 does not statically infer arbitrary file reads performed by repository build
code. If build code opens or transforms a binary/media file, do not cover that
file with the reviewed public/** declaration; it will then hydrate before the
build.
Hydration work is batched so a repository with thousands of pointers does not incur one sandbox or control-plane round trip per path:
The output scan is not the final trust check. Immediately before any connected
provider publication call, Forge loads each selected object from immutable
storage, applies the provider's per-file limit, and recomputes both the byte
count and SHA-256 digest. A size or digest mismatch fails before publication.
This keeps the fast path credential-free without trusting stale metadata or
performing a redundant per-object storage HEAD during the build.
An unchanged digest can be reused without re-upload inside an account after repository reachability is established. Cross-account callers must independently upload or prove the bytes; the current protocol deliberately does not reveal that another account already stored the digest. Physical storage may deduplicate after verification, while authorization, logical quota, metadata, and reachability remain account scoped.
The high-level adoption flow never rewrites already-pushed commits. It creates a new, reviewable migration branch and pull request from a frozen source commit:
.forge/assets.json, .gitattributes, and standard Git
LFS pointers to the migration branch.The pull request can be closed with no history rewrite. Merging it changes only future commits descended from that merge. Forge suppresses automatic Deploy previews for this machine-authored migration pull request. Creating it does not advance the production branch, deploy a release, attach a domain, alter DNS, or retire the previous host.
The assistant's default proposal is deliberately small:
public/** tree only when its selected regular files total
more than 50 MiB; andBoth comparisons are strict > comparisons. Exactly 50 MiB and exactly 5 MiB
are not selected by default. The current v1 endpoint reports these fixed policy
values with every inspection; administrators customize the result by selecting
or deselecting proposed entries. Threshold overrides are not part of the
callable v1 request.
These thresholds affect migration suggestions only. They are not build limits, Git LFS protocol limits, or automatic conversion rules.
The authenticated migration surface is designed so an agent does not need to clone the entire repository, run a clean/smudge filter, or possess object-store credentials:
uploaded: false through Forge's standard
authorized LFS Batch/basic API;The repository-administrator endpoints are:
GET /api/repos/:owner/:repo/sites/migration/assets?sha=<40-hex-source-sha>
POST /api/repos/:owner/:repo/sites/migration/assetsGET is read-only. It returns the primary branch and exact source/head SHA,
policy defaults, existing config, literal entry suggestions, each source blob
SHA, SHA-256 LFS OID, size, upload status, and warnings. Inspection streams
ordinary blobs into SHA-256 with bounded concurrency and does not execute
repository code.
The v1 Sites UI reports which selected objects are missing, but it does not
copy ordinary source blobs into Forge Asset storage. Before Apply can succeed,
a user, agent, or standard LFS client must upload those exact bytes through the
LFS transfer API and inspect again. API callers do not need a local git-lfs
binary.
POST requires version: 1, an 8–128 character idempotency_key, matching
source_sha and expected_head_sha, the inspected primary target_branch,
reviewed entries, and the exact proposed object identities. Forge revalidates
the head, source, selection, repository authorization, verified LFS reachability,
and byte equality before committing .forge/assets.json, .gitattributes, and
standard pointer files. A successful response returns the derived migration
branch, commit, pull request, immutable receipt, and
preview_behavior: "suppressed". Repeating the same idempotency key and body
returns the same receipt; a changed head or changed input fails closed.
{
"version": 1,
"idempotency_key": "assets-01JEXAMPLE",
"source_sha": "<40-hex-commit>",
"expected_head_sha": "<same-40-hex-commit>",
"target_branch": "main",
"entries": [{ "path": "apps/main/public", "kind": "tree" }],
"objects": [{
"path": "apps/main/public/hero.mp4",
"source_blob_sha": "<40-hex-git-blob>",
"oid": "<64-hex-sha256>",
"size": 12345678
}]
}Forge Assets v1 has a 1 GiB single-object safety ceiling. The effective limit is the narrowest account entitlement, repository policy, Forge safety ceiling, and transport/provider ceiling. Logical bytes count against the account that can reach the object even when the physical digest is already present globally.
The initial Worker-mediated basic upload path is capped at 100 MiB per request, within Cloudflare's current single-request body limit for the deployed Worker plan. Forge rejects larger objects before promising an upload action. Large-object resumable or multipart transfer is a separate transport capability. The 1 GiB value is therefore an identity safety ceiling, not a claim that every current account can upload a 1 GiB object in one HTTP request.
One Batch request accepts at most 100 objects and 256 KiB of request metadata. Batch authorization itself remains separate from byte transfer. Short-lived signed upload and verification tickets share a dedicated 12,000 operations per hour transfer budget rather than consuming ordinary Git mutation capacity. A typical missing object uses one signed upload request and one signed verification request. If a ticket expires or a bounded batch is interrupted, submit the same OIDs to Batch again: already-verified objects need no new bytes and remaining work receives fresh actions. Rebatching is the v1 resume/idempotency mechanism; it is not multipart upload for one oversized object.
Release publication has separate provider limits. For example, connected Cloudflare static assets retain Cloudflare's 25 MiB per-file ceiling even when their source object was accepted by Forge Assets. See Deploy limits.
For unchanged public binaries selected by the OpenNext preset, Forge plans the provider manifest from the exact LFS SHA-256 and the normalized file extension. It does not hydrate or buffer every object during the build. If Cloudflare asks for bytes that are not already present, the runner verifies the R2 upload checksum and size on that exact object and streams the provider-requested multipart bucket with bounded base64 chunks. Provider asset hashes, byte counts, and MIME identity remain deterministic, while aggregate release size does not become aggregate runner memory use.
See Failure diagnostics for operator-facing failure categories.