Authoring
Status: implemented. Local authoring (
tfp init/lint/test) and authoring over MCP (the toolkit tool, theauthorandcustom_verifiercapabilities, uploads, validation with reconciliation, durable private drafts and explicit sharing) are implemented and tested. Custom checker images are registered, provisioned and executed on TfP; the base imagetokensforprogress/verifier-python:0.1.0is public on Docker Hub, see Custom sandbox checkers. This page is the contract for the authoring package (the files, what each becomes, how they refer to one another, what the linter reports) and for the MCP surface (capabilities, uploads, validation, private drafts and publication). The author-facing guide is Authoring with Your Agent; the TfP Authoring Skill (skills/authoring/SKILL.md, embedded on that page) teaches the same convention to an agent.
An authoring package is a directory of ordinary files. Each file corresponds to one existing TfP object; the linter validates every file with the same canonical code the platform runs, and prints what the package would publish. Publication consumes exactly these files and adds nothing.
Files
A Problem package is a directory containing:
| File | Becomes | Required |
|---|---|---|
problem.json |
Problem (identity) and the non-prose fields of its first or next ProblemRevision; on a later revision revision_note becomes ProblemRevision.changelog_md |
yes |
statement.md |
ProblemRevision.statement_md |
yes |
skills/<name>/… |
one SkillVersion per directory, pinned required by the revision |
no |
reference/<key>.<ext> |
one reference-input binding per file: slot <key> → these exact bytes; mounted in the sandbox as /input/reference/<key>/<key>.<ext> |
no |
reference/<key>.link.json |
one reference-input binding per file: slot <key> → content at a public https:// URL, with the hash and size you state; TfP stores no bytes and sandbox stages do not see it (reference/) |
no |
verifier-contract.json |
VerifierContract (a Verifier is created or reused for it at publication); each sandbox stage's inline implementation becomes one VerifierImplementation |
no |
checker.py (any .py a stage names as ./…) |
the program of a script checker's VerifierImplementation: stored content-addressed, run on TfP's tfp-runtime/python:1 (Script checkers) |
with a stage naming it |
fixtures/<filename> |
bytes of the Contract's conformance fixtures (its checker test cases) | with sandbox stages |
review-protocol.json |
ReviewProtocol | with review stages |
verifier-implementation.json |
VerifierImplementation for one sandbox stage, only when the stage does not name its checker inline (kept for compatibility; prefer inline) | no |
ProblemRevision.context_md is always null: there is no context.md.
There is no changelog.md and authors do not maintain a changelog; the
one optional sentence explaining a later revision is
problem.json.revision_note. Both file names are reported as notes
and never published.
A Campaign folder is a directory containing campaign.json and a
problems/ directory whose subdirectories are Problem packages.
Shared mechanisms may sit beside campaign.json: skills/,
review-protocol.json, verifier-contract.json (with its inline
implementations) and fixtures/. A Problem that has no mechanism of
that kind inherits the Campaign's; a Problem with its own uses its own.
The plan lists every mechanism once, keyed by content hash, and each
Problem references the hashes it uses, so a thousand Problems sharing
one Contract publish one Contract.
Anything else in the directory is reported as a note and is not
published. A .tfp/ directory is ignored (the CLI keeps upload state
there).
problem.json
{
"slug": "extract-arxiv-2607-15495",
"title": "Extract structured findings from arXiv:2607.15495",
"summary": "Produce a JSON extraction of the paper; a reviewer checks fidelity.",
"structure": "SOLVE",
"priority_weight": 0.5,
"campaign": "translate-chinarxiv",
"verifier_contract": "vct_…",
"skills": ["skv_…"],
"benchmark": null,
"revision_note": "Schema v2: added the limitations field."
}slug, title, summary are required and validated by the Problem
schema. structure defaults to SOLVE; BENCHMARK requires a
benchmark block (the ProblemRevision schema's shape); DISCOVERY
takes no benchmark. priority_weight defaults to 0.5. campaign names
an existing Campaign by slug; inside a Campaign folder it is implied and
must not disagree. verifier_contract pins a published Contract instead
of authoring verifier-contract.json (never both). skills pins
published SkillVersions in addition to any under skills/.
revision_note is optional metadata for a later revision only: a
string of at most 5000 characters saying why this revision replaces
the previous one, stored as the new ProblemRevision.changelog_md
when reconciliation makes the Problem a revise. On a first
publication, and on a noop, it has no effect and changelog_md of a
first revision is null. It is not a changelog: nothing
accumulates across revisions, and each revision carries at most its
own note.
sealing is required exactly when a reference file binds a slot the
Contract declares sealed. It declares sealed_until, optional
submission_closes_at, score_release ({mode:"immediate",release_at:null}
or {mode:"final_only",release_at:"<RFC 3339 UTC>"}), and
submission_limit: {max_submissions:n,window_hours:h}, and optional
entries: "collaborative" | "independent" (default collaborative). Independent
entries require a submission cutoff. Publication requires
future release dates and bounded integer quotas. The public revision adds
derived evaluation_id, unsealed_at and nullable release_event (the manager, time and public
reason for explicit early release); do not author these fields. Revisions with
the same complete sealed dataset and owners share one evaluation, immutable
policy, quota and final-entry choice. Partial overlaps or different owners are
refused. Early release requires preparing and acknowledging its exact scope.
Materializing a sealed package with tfp draft commits its release dates, quota
and scoring rules even while private. Obtain the owner's authorization for that
commitment; rehearse using invented labels in separate fixtures or dummy work.
The proposal preparation helpers refuse sealed packages. See
Sealed reference inputs.
campaign.json
{ "slug": "translate-chinarxiv", "title": "Translate ChinArxiv into English", "summary": "…", "description_md": "…", "status": "draft" }slug, title, summary required; description_md defaults to
empty; status to draft.
reference/
The file name before its extension is the binding key and must match
^[a-z0-9]+(_[a-z0-9]+)*$. The media type is taken from the extension
(.json, .md, .txt, .html, .csv, .pdf, …; anything unknown is
application/octet-stream). Size and SHA-256 are computed. The
statement refers to inputs by key; the linter notes a key the statement
never mentions. A file bound to a sealed slot is uploaded and stored
like any other; only who can read it after publication differs.
Binding a slot to a link
When the content already lives at a public URL and attaching the bytes is
impractical (a campaign of thousands of scanned papers hosted by their
publisher under an open licence), bind the slot to the link instead:
reference/<key>.link.json holds
{ "url": "https://journals.example.org/archive/1974/paper-0512.pdf",
"content_hash": "sha256:…", "size_bytes": 1548211,
"media_type": "application/pdf", "filename": "paper-0512.pdf" }urlis an absolutehttps://URL of at most 2048 characters.content_hash(sha256:<64 hex>) andsize_bytesare your statement about the content at that URL. TfP does not fetch the URL and never checks them; the revision records them as stated, and solvers and reviewers compare what they download against them. State them from the exact bytes the URL serves.media_typeis written in canonical form (lower-casetype/subtype, no parameters) and must be one the slot'smedia_typesallows;size_bytesmust not exceed the slot'smax_bytes.filenameis an optional label.- A slot is bound by bytes or by link, not both.
A linked slot behaves like a stored one everywhere people and agents look
for inputs: the Problem page, get_problem and start_attempt
(reference_resources[].url), the submission page, the review casefile
and the revision's package all carry the URL beside the key, media type,
hash and size, and GET /api/problem-revisions/{id}/reference/{key}
answers with a redirect to the URL (the stated hash rides in
x-tfp-content-hash). It differs in one way: sandbox stages do not
mount a linked slot. /input/reference/<key>/ is absent in the run,
so a checker that reads the slot must have the bytes bound as a file.
Lint says so with a note on every link binding under a Contract with
sandbox stages, and names the checker when a builtin is known to read
that slot. Conformance fixtures always bind bytes: a fixture whose
filename names a .link.json is refused. tfp lint --fix never
rewrites a link binding's hash, and tfp verify skips linked slots with
a note instead of downloading them.
Changing a link binding's url, hash, size, media type or filename
changes the revision's identity, so the next publication revises the
Problem, as replacing the bytes of a stored binding would.
skills/<name>/
An Agent Skill package: SKILL.md at its root with name and
description frontmatter, any other files beside it, within the
registry's limits. Validated by the same rules as the Skills registry;
the content hash is the registry's tree digest.
verifier-contract.json
The canonical VerifierContract document (see Verification), with these local conveniences, each resolved by the linter before the canonical validator runs:
- a
sandboxstage carries"implementation": "<checker>", where<checker>is a builtin (tfp-builtin/json-schema-check:1,tfp-builtin/sat-assignment-check:1), a Python file in the Contract's directory (./checker.py,./checkers/structure.py: letters, digits,_and-; one UTF-8 file of at most 256 KiB, run on TfP'stfp-runtime/python:1), or a public OCI image pinned by digest,registry/repository@sha256:<64 hex>(for exampledocker.io/alice/tfp-translation-checker@sha256:…). A tag (:latest,:1.2) is refused withunsupported_runtime: a digest is the only immutable name; a missing script file isnot_found, a non-Python oneunsupported_runtime. The linter derives the canonical VerifierImplementation (TfP fixes a builtin's entrypoint, limits and source; a script checker's runtime, entrypoint andprogramblock; a custom image's are fixed by the run protocol) and, for a builtin, checks that the Contract declares the artifact role and reference slots it reads. The plan marks a custom imagecustomand a script checker with itsprogram(file, hash, size); both conform on the server's runner, not locally; - the stages are any finite ordered sequence of
sandbox,reviewandexternalin any order and multiplicity; sequential means only that stage N+1 runs after stage N passes; - a
reviewstage may set"review_protocol_id": "./review-protocol.json"; submission_requirements.accepts_text_onlydefaults tofalse,answer_format_mdtonull;scoretonullunlessresult_kindisscore; a fixture'sexpected_scoreandinputs.summary_texttonull,inputs.referenceto{}andinputs.artifactsto[].
Conformance fixtures reference files by filename, found under
fixtures/ or, when a fixture uses a real bound input, under
reference/; --fix writes their content_hash and size_bytes.
Field by field:
Top level
| Field | Required | Meaning |
|---|---|---|
title |
yes | one line |
statement_md |
yes | one sentence: what "verified" means |
reference_inputs |
yes (may be []) |
the slots a revision binds files to |
stages |
yes | ordered checks; stage N+1 runs after stage N passes |
submission_requirements |
yes | what a solver must submit |
result_kind |
yes | pass_fail or score |
score |
with score |
{ "unit": "…", "description": "…" } plus optional "direction": "higher_is_better" | "lower_is_better"; lint fills null for pass_fail. Leave direction out and the Problem's benchmark.direction decides how results rank, so one scored Contract serves minimised and maximised Problems |
conformance_fixtures |
yes (may be []) |
checker test cases; at least one verified and one refuted per sandbox stage is the norm |
reference_inputs[]
{ "key": "document_schema", "required": true, "media_types": ["application/json"], "max_bytes": 262144, "description": "The JSON Schema the translation must satisfy." }key matches ^[a-z0-9]+(_[a-z0-9]+)*$ and names the file
reference/<key>.<ext> (or the link descriptor reference/<key>.link.json)
in the package. A required: true slot must be bound by every Problem
using the Contract, and by fixtures of stages selecting that slot
(fixtures bind bytes, never a link). sealed (optional, default false)
holds the bound bytes back from everyone but selecting sandbox checks and
the Problem's managers until the revision unseals
(Sealed reference inputs); a sealed slot cannot be
bound by link, since TfP would hold no bytes to hold back.
stages[]
{ "key": "structure_check", "kind": "sandbox", "title": "Structural check",
"description_md": "Parses and satisfies the schema.",
"evidence_schema": { "type": "object", "required": ["valid"], "properties": { "valid": { "type": "boolean" } } },
"review_protocol_id": null, "rule_owner": null, "acceptance": null, "external_system": null,
"implementation": "tfp-builtin/json-schema-check:1" }| Field | Meaning |
|---|---|
key |
unique, ^[a-z0-9]+(_[a-z0-9]+)*$ |
kind |
sandbox (a checker runs), review (people judge) or external (a partner system attests) |
title, description_md |
for readers |
reference_input_slots |
sandbox only: selected reference keys; required on every sandbox stage if the Contract declares any sealed slot. [] mounts no references. Otherwise omission preserves the legacy all-slots behavior |
result_kind |
sandbox only: pass_fail or score, defaulting to the Contract's result kind. A scored Contract can begin with a public pass/fail format check before a scored evaluation |
limits |
sandbox stages naming an implementation only; {memory_mb, vcpu, disk_mb, timeout_seconds}, each optional; the checker's requested execution envelope (an Implementation detail, stripped from the Contract; see custom checkers) |
evidence_schema |
JSON Schema the checker's evidence/summary.json must satisfy, or null |
review_protocol_id |
for review: "./review-protocol.json" or a published rpt_…; otherwise null |
rule_owner, acceptance, external_system |
for external only; otherwise null |
implementation |
for sandbox only: a builtin, a Python file in this directory (./checker.py), or a public image pinned by digest (registry/repo@sha256:<64 hex>; a tag is refused) |
Builtins:
| Builtin | Checks | Reads |
|---|---|---|
tfp-builtin/json-schema-check:1 |
the artifact in role document parses as JSON and satisfies the schema |
reference input document_schema |
tfp-builtin/sat-assignment-check:1 |
the artifact in role assignment satisfies the CNF |
reference input instance |
A builtin fixes the artifact role and the reference key it reads; declare both, or lint tells you which is missing.
submission_requirements
{ "required_artifacts": [ { "role": "document", "description": "The English translation as JSON.", "media_types": ["application/json"], "max_bytes": 1048576 } ] }accepts_text_only (default false) allows a submission that is only
its summary text; answer_format_md (default null) tells the solver
the expected answer shape. A solver binds each uploaded Artifact to a
role explicitly (create_submission { artifacts: [{ role, artifact_id }] }).
conformance_fixtures[]
{ "key": "valid_document", "stage_key": "structure_check", "description": "A complete translation.",
"inputs": {
"reference": { "document_schema": { "filename": "document_schema.json", "content_hash": "sha256:…", "size_bytes": 420 } },
"artifacts": [ { "role": "document", "filename": "valid.json", "content_hash": "sha256:…", "size_bytes": 114 } ]
},
"expected_result": "verified" }inputs.referenceis an object keyed by slot;inputs.artifactsis an array in role order;inputs.summary_text(defaultnull) is the submission text foraccepts_text_onlyContracts.- Reference keys must be selected by the fixture's stage; all selected required slots must be present. Sealed slots use public stand-ins, never the revision's actual protected bytes under any role or filename.
- Files are found under
fixtures/<filename>, or underreference/when a fixture uses the real bound input.tfp lint --fixwritescontent_hashandsize_bytes. expected_resultisverified,refutedorinconclusive;expected_score(defaultnull) only with the stage's effectiveresult_kind: score.- Fixtures are inspectable specification material: every solver can read their hashes through the published Contract. Use synthetic cases that exercise the check, never the actual answer to a Problem whose answer is short enough to recognize by hash.
| Builtin | Checks | Reads |
|---|---|---|
tfp-builtin/json-schema-check:1 |
the artifact in role document parses as JSON and satisfies the schema |
reference input document_schema |
tfp-builtin/sat-assignment-check:1 |
the artifact in role assignment satisfies the CNF |
reference input instance |
review-protocol.json
The canonical tfp.review/v0.1 document (see
Reviews), with one local convenience: a node's
skills[].skill_version_id may be "./skills/<name>".
Lint
tfp lint <dir> [--fix] [--json] in the toolkit bundle, and the same
code as pnpm tfp-lint <dir> in the repository. Runs, in this order, on
the Campaign root's shared mechanisms and then on every Problem package
found:
problem.jsonagainst the Problem schema;campaign.jsonagainst the Campaign schema (ids, owner and timestamps are placeholders).- Each
skills/<name>/with the Skill-package rules. review-protocol.jsonwith the ReviewProtocol validator, rules 1–16, on the production schema engine; local Skill references resolved.verifier-contract.jsonwith the Contract validator; each stage'simplementationresolved to an Implementation and validated against the Contract; fixture bytes hashed; the local protocol reference resolved.- Reference files against the Contract's slots (required slots bound, media types and sizes within limits); sealing policy and date ordering; bounded integer quotas; explicit stage selectors for sealed Contracts; no sealed bytes duplicated elsewhere in the package, including public bindings, fixtures, Skill files and checker source. Publication additionally checks historical hashes of public references, fixtures, Artifacts and checker programs and shared release promises against the registry, which local lint cannot inspect.
- The assembled ProblemRevision against its schema.
Every failure is one record:
{ "file": "verifier-contract.json", "path": "/stages/1/review_protocol_id", "code": "not_found", "message": "review-protocol.json is not in this directory" }file is relative to the package root (problems/<slug>/… inside a
Campaign folder). code is one of missing_file, invalid_json,
schema_violation, rule_violation (a rule beyond the schema;
protocol rules carry their number), not_found (a reference to
something not in the package), binding_error, unsupported_runtime,
hash_mismatch, unknown_file. Notes carry the same addressing and
never fail the run. --json prints {ok, errors, notes, plan, plan_hash}.
The plan
plan is what the package would publish, computed from the files:
{
"campaign": { "slug": "…", "title": "…", "summary": "…", "description_md": "…", "status": "draft" } | null,
"skills": { "<content_hash>": { "dir": "…", "name": "…", "content_hash": "…", "files": 3 } },
"review_protocols": { "<content_hash>": { "file": "…", "title": "…", "content_hash": "…", "skill_dirs": ["…"] } },
"verifier_contracts": { "<content_hash>": { "file": "…", "title": "…", "stages": ["schema:sandbox", "fidelity:review"], "content_hash": "…", "local_review_protocol": "<hash>" | null, "fixture_files": ["…"] } },
"verifier_implementations": { "<content_hash>": { "file": "…", "stage_key": "…", "image_ref": "…", "image_digest": "sha256:…", "custom": false, "content_hash": "…", "program": { "file": "checker.py", "content_hash": "sha256:…", "size_bytes": 1420 } } },
"problems": [ { "dir": "…", "slug": "…", "structure": "SOLVE", "title": "…", "summary": "…", "priority_weight": 0.5, "campaign_slug": "…" | null,
"statement_md": "…", "benchmark": null, "revision_note": null,
"skills": ["<hash>"], "published_skills": ["skv_…"],
"reference_inputs": [ { "key": "…", "file": "…", "media_type": "…", "size_bytes": 0, "content_hash": "sha256:…" } ],
"verifier_contract": "<hash>" | null, "published_verifier_contract": "vct_…" | null,
"verifier_implementations": ["<hash>"], "review_protocol": "<hash>" | null } ]
}plan_hash is sha256: of the canonical JSON of the plan. It
identifies a publication exactly: the same files produce the same hash
on the author's machine and on the server, and every server-side step
below is keyed by it.
The plan is state-independent: it describes the files. verifier_contract
is the hash of a Contract authored in the package and
published_verifier_contract is a vct_… id the package pins in
problem.json; a package that authors its Contract always shows the
first and null for the second, whatever the server later reuses.
Which existing objects a publication reuses (the same Contract by its
canonical document, the same SkillVersion by content hash) is the
reconciliation's and the publication result's vocabulary, never the
plan's.
Published ids (vct_…, rpt_…, skv_…) are accepted by shape and
noted; whether they exist is checked by the server.
One engine
The linter contains no rule of its own. It calls
@tokensforprogress/verification-contracts (Contract, Implementation
and binding validation, the JSON-Schema subset engine, canonical JSON
and hashing), @tokensforprogress/skill-package (package rules and
tree digest), and @tokensforprogress/review-protocol (rules 1–16 on
the subset engine), plus the canonical JSON Schemas under schemas/.
It reads a package as a file tree (path → bytes), so the same function
runs from the author's disk and from an uploaded archive on the
server. The Worker imports the same packages; the operator seeds use
them. A package that lints clean is one the platform would accept, and
a rule the platform enforces is one the linter reports.
Stage 8B: authoring over MCP
The surfaces below implement the authoring contract. A package save and an explicit visibility transition are separate operations.
Capabilities
| Scope | Kind | Grants |
|---|---|---|
read |
scope | everything read-only, including the Skill Resource and get_sandbox_image_status |
contribute |
scope | doing work, reviewing and preparing private proposals as the connected User |
author |
beta capability | materialize ordinary private packages and explicitly open participation |
custom_verifier |
beta capability | name public, digest-pinned OCI images as sandbox checkers; images are registered under this capability |
Capabilities are granted by an operator to allowlisted Users and
recorded with who granted them and when. Signed-in contributors can save
incomplete proposals and validate and test them privately (prepare_work_draft)
without author access. Uploading and validating are also available to contributors;
direct package materialization requires author or administrator access. Unlisted work is not
automatically included in discovery.
Trusted authors can explicitly list their own work. Platform administrators can
publish proposals on behalf of their owners and manage access through the website
or MCP. See Drafts, publication and discovery.
Existing author grants are kept in user_capabilities
(migration 0020). A token's scope is the requested scope intersected
with read/contribute plus the capabilities the User holds at grant
time; a token never carries a capability its User lacks. The consent
screen for author states that publications are immutable. The website groups current abilities into contributor, author, trusted-author
and administrator roles. Trusted listing and administration are checked live,
independently of OAuth capability claims.
Surface
| Surface | Scope | Purpose | Status |
|---|---|---|---|
pnpm tfp-lint <dir> / tfp lint <dir> |
none (local) | canonical local validation and the plan | implemented |
| website: Propose a campaign or problem, Proposal status and Edit | signed-in account, live role checked per action | save incomplete proposals, edit text, validate/test, request publication; technical package editing stays in agent tooling | implemented |
Resource tfp://authoring/skill/latest |
read |
the canonical TfP Authoring Skill (skills/authoring/SKILL.md) |
implemented |
get_authoring_toolkit |
read |
a short-lived download URL for the tfp bundle; open to every connected User, since the bundle is also the solver's preflight (tfp verify) |
implemented |
request_authoring_upload |
contribute |
a signed PUT slot for one package archive | implemented |
validate_authoring_upload |
contribute |
canonical validation plus server checks; the plan, its reconciliation and its hash; registers any custom image | implemented |
publish_authoring_upload |
author or live administrator |
execute exactly the validated reconciliation; new work starts private | implemented |
save_work_draft, prepare_work_draft |
contribute |
save incomplete proposals; materialize valid new work as ordinary private Campaigns/Problems | implemented |
publish_work_draft, set_work_visibility |
live role checked | explicitly open participation or list the same work | implemented |
get_sandbox_image_status |
read |
provisioning and conformance state of one registered custom image, or of a script runtime (tfp-runtime/python:1) |
implemented |
authorize_cli |
contribute |
approve the code tfp login --mcp displays; the CLI receives its own credential |
implemented |
tfp login [--mcp], tfp whoami, tfp logout |
(CLI) | the CLI's credential: browser OAuth + PKCE, or a code approved over MCP; stored in ~/.config/tfp/credentials.json, revocable in Settings |
implemented |
Resources tfp://authoring/skill/latest/ + …/<path> |
read |
the Skill package's manifest and files: reference/*.md, examples/** (also /docs/authoring/skill/<path>) |
implemented |
tfp init/test/upload/draft/publish |
(CLI over the tools above) | the author-side loop; draft and its compatibility name publish shard a large Campaign, retry transient failures and resume from .tfp/publish-state.json |
implemented |
tfp verifier build <checker-dir> |
(CLI over docker buildx) |
build a custom checker for linux/amd64, push it, confirm it is publicly pullable, pin the digest into the Contract |
implemented |
tfp verifier status <ref-or-digest> |
(CLI over get_sandbox_image_status) |
where a custom image stands | implemented |
tfp verify <problem> --artifact <role>=<path>… |
(CLI over get_problem and the API) |
the solver's preflight: the Contract's sandbox stages on local files, the Worker's way (Verification) | implemented |
| custom image execution | custom_verifier |
run a digest-pinned image as a sandbox stage on a runner per runtime digest | implemented; provisioning verified live 2026-09-04 (status) |
| script checker execution | author |
run one Python file (./checker.py) on TfP's tfp-runtime/python:1, the same runner data plane; the runtime bound once per deployment by an operator (POST /admin/sandbox-runtimes) |
implemented (status) |
GET /api/verifier-implementations/{id}/program |
read (API) |
the exact program bytes of a script Implementation, with x-tfp-content-hash; also a revision file checkers/<stage_key>/<filename> |
implemented |
Tools
Every result is an MCP text content holding a JSON envelope; errors
are MCP tool errors with { "error": … }.
get_authoring_toolkit {} →
{ version, content_hash, platforms, download_url, expires_at }.
download_url is a short-lived (15 minutes) URL for the single-file
bundle (tfp-<version>.mjs); content_hash is sha256: of its bytes;
platforms lists what it runs on (node>=20). The bundle is TfP's
private authoring CLI: internal package name
@tokensforprogress/authoring, never published to npm.
request_authoring_upload { size_bytes, content_hash } →
{ upload_id, upload_url, expires_at, max_bytes }. size_bytes and
content_hash describe the archive the client is about to PUT;
upload_url is a signed PUT URL valid until expires_at (15
minutes); upload_id (aup_…) names the upload from here on. The
server verifies size and hash on receipt and records the upload as
uploaded.
validate_authoring_upload { upload_id } →
{ valid, errors: [{file, path, code, message}], notes, plan, plan_hash, conformance: [{stage_key, image_ref, fixture, expected, actual, passed}], images: [{image_ref, image_digest, state}] }.
Unpacks the archive into the same file tree the linter reads, runs the
linter unchanged, then the checks only the server can make:
- ownership and current state: every included Problem is reconciled
with current state as
create,noop,update,reviseorconflict(see Reconciliation); an existing slug must belong to the caller unless the caller is a live platform administrator; the same rule applies to thecampaignslug. Administrator maintenance preserves existing ownership and records the actual publishing administrator. An archived Problem takes no new revision; - existence of published ids (
vct_,rpt_,skv_), not yanked; - bytes and hashes recomputed from the uploaded bytes (stored reference inputs, fixtures, Skill trees); a link binding's hash and size are recorded as the author stated them, never fetched;
custom_verifierheld by the caller for any custom image;- custom image registration: every custom image the package names is
registered under the caller's
custom_verifiercapability if it is not already known (a digest already registered by anyone is reused, never registered twice) and its provisioning is enqueued; the result reports each image'sstate(see Custom sandbox checkers); - production conformance: every builtin checker's fixtures are run on
the server's own runtime; a custom image's fixtures are run once the
image is
availableand otherwise reportedpending; - slug constraints: the
pv-prefix is reserved; uniqueness is checked among live objects.
Validation never mutates author content. The upload is recorded as
validated with plan_json, plan_hash and the reconciliation it
computed (including the current state it was computed against), or
invalid with the errors. Validating the same upload again recomputes
the reconciliation against current state.
publish_authoring_upload { upload_id, plan_hash } →
{ campaign_id?, problems: [{slug, problem_id, problem_revision_id, revision_number, visibility, listed_in_explore}], campaign?, skill_version_ids, verifier_contract_ids, verifier_implementation_ids, review_protocol_ids, counts }. Requires
a validated upload whose plan_hash matches; a stale hash is refused
with stale_plan_hash: validate again. Executes the stored
reconciliation; before mutating, it verifies that every affected
Campaign and Problem still has the owner and current revision the
validator observed, and otherwise refuses with stale_server_state: validate again. Creates, in dependency order:
SkillVersions, ReviewProtocol, Verifier (created or reused) and
VerifierContract (local protocol reference replaced by the published
id), VerifierImplementations, Campaign (owner = caller, initially active and
unpublished), then each
Problem and its ProblemRevision with pins and bindings, opened. New standalone
Problems are private too; Campaign children inherit the Campaign visibility unless
an individual exception restricts it. The result reports effective visibility
and listed_in_explore. Existing visibility is preserved, including on retries.
Idempotent: publishing an upload already published returns the
recorded result and creates nothing. For a Campaign folder the CLI may
send several uploads (publication batches); that is transport, and the
shared mechanisms are reused by hash across batches. Problems whose
action is noop create nothing and are returned with their current
ids. A package naming
a custom image publishes whether or not the image is available; a
run reaching that stage waits for the image and ends error if
provisioning fails (an execution fault, never a finding; a limit
exhausted while running is inconclusive).
A newly created ordinary Campaign is Unpublished after this package step.
Its owner and platform administrators can inspect it and test its open Problems
privately. New standalone Problems also start private. When ready, use the
management visibility controls or set_work_visibility to share as unlisted,
then list when authorized. Materializing a Problem or revision within a Campaign keeps
the parent's visibility, and updating or retrying an existing Campaign package
does not reset its publication setting or individual Problem exceptions.
get_sandbox_image_status { image } → { image_ref, image_digest, runtime_digest, state, state_reason, registered_at, implementations: [{verifier_implementation_id, contract_id, stage_key, conformance}] }.
image is the author-supplied reference, a bare sha256: digest, or a
script runtime name (tfp-runtime/python:1). state is registered,
provisioning, available or provisioning_failed (with
state_reason), or unbound for a script runtime whose pinned image has not been enabled on
on this deployment yet; runtime_digest is the
linux/amd64 image that actually executes (equal to image_digest
for a single-platform image); each Implementation's conformance is
pending, passed or failed. Semantics in
Custom sandbox checkers.
tfp verifier build: a checker image, pinned
tfp verifier build <checker-dir> is a thin wrapper over the Docker
Build Cloud flow in
Custom sandbox checkers:
no new runtime concept, only the commands an author would type. It
requires a Dockerfile in <checker-dir>, then picks a builder in
order: a builder docker buildx ls lists with the cloud driver; one
created from DOCKER_ACCESS_TOKEN + DOCKER_ACCOUNT +
CLOUD_BUILDER_NAME (or --builder <account>/<name>) with docker login --password-stdin (the token goes over stdin, never argv or
output) and docker buildx create --driver cloud --use, which needs
Buildx ≥ 0.37; the local daemon when docker info answers; otherwise
it fails naming the docs section and the three variables. It runs
docker buildx build --platform linux/amd64 --push --tag <repository>:<tag> --metadata-file …, with --repository defaulting
to tokensforprogress/tfp-verifiers (DOCKER_REPOSITORY) and --tag
to <dir basename>-<YYYYMMDDHHMMSS>, reads containerimage.digest
from the metadata, and confirms the image is anonymously pullable the
way the Worker pulls it (an anonymous token from auth.docker.io,
then a manifest HEAD at registry-1.docker.io); a private
repository is an error. Inside a package (a verifier-contract.json
in the directory's parent, or --contract <path>) it writes
docker.io/<repository>@sha256:… into the sandbox stage's
implementation (--stage <key> when there is more than one) and,
with --limits memory_mb=…,vcpu=…,timeout_seconds=…, the limits
beside it, keeping 2-space JSON; otherwise it just prints the
reference. --json prints { image_ref, digest, repository, tag, builder, contract_file?, stage_key? }; --dry-run prints the
commands it would run.
Uploads: transport only
The archive is a ZIP of the package directory, produced by the CLI for transport and never shown, versioned or published. Rules:
- at most 64 MiB per archive and 5000 files;
- entries are relative POSIX paths under the package root; absolute
paths,
..segments, symlinks, and entries outside the root are refused (unsafe_path); .tfp/and dot-files are dropped on unpacking;- the server stores the exact bytes it received (
archive_r2_key,archive_content_hash) for provenance; uploads that were never validated or published expire after 7 days.
The CLI's <dir>/.tfp/upload.json records {upload_id, plan_hash, content_hash, uploaded_at, valid} so draft and publish need no
arguments; <dir>/.tfp/publish-state.json records the shard plan of
the last tfp draft or tfp publish and which shards completed (below).
tfp draft: shards, retry, resume
tfp draft <dir> [--json] [--fresh] [--verify] [--shard-problems <n>] [--shard-bytes <n>] owns transport for a package of any size. tfp publish
is a compatibility name with identical behavior. Neither command changes the
visibility of existing work; new Campaigns and standalone Problems start private:
- The whole package is linted once locally; errors stop it.
- The package is cut into shards at Problem boundaries. Every shard is
a sub-tree holding
campaign.json, every root-level shared mechanism (skills/,verifier-contract.json,verifier-implementation.json,review-protocol.json,fixtures/) and a subset ofproblems/<slug>/directories, in slug order. A shard closes when the next Problem would exceed any ceiling: 300 Problems (--shard-problems), 3000 files, 24 MiB of zipped bytes (--shard-bytes, estimated by deflating each file) or 100 MiB unpacked; all well under the server's 5000 files, 64 MiB and 256 MiB. A single Problem or a small Campaign is one shard and behaves as before; whentfp uploadalready sent exactly those bytes, that validated upload is what gets materialized. - Each shard goes through
request_authoring_upload→ PUT →validate_authoring_upload→publish_authoring_upload, with up to 5 attempts on transient failures (a dropped connection, a call that passes the client's deadline of 180 s, 300 s for the PUT, an HTTP 5xx page, an empty or unparseable answer), waiting 2, 4, 8, 16 and 30 s plus jitter. A retry re-sends the whole shard: if the lost answer was to a publish the server had committed, the replay reconciles those Problems asnoop, so nothing is duplicated.stale_plan_hashandstale_server_stateare answered with exactly one fresh upload + validate + publish of the same shard; validation errors and conflicts stop the run and are printed like lint errors. <dir>/.tfp/publish-state.jsonrecords the plan (shard ids, their slugs, a content hash of each shard's files) and, per shard, the status, attempts, upload id, plan hash, counts and time. A rerun skips shards that are published and whose hash is unchanged, and sends the rest; a crashed or interrupted run is resumed by running the same command again.--verifyreplays every shard (the server answersnoopfor each unchanged Problem);--freshignores the state file. Changing a root file changes every shard's hash, so all are re-sent (and reconcile asnoopunless a Problem's effective task changed).
Progress is one line per shard; --json prints one final document
{ok, campaign, problems_total, planned_shards, published_shards, skipped_shards, counts, problems: [{slug, action, problem_id, problem_revision_id, revision_number}], …_ids, shards, published_at}
with progress on stderr.
Durable private drafts
tfp draft executes the validated plan under the real Campaign and Problem slugs.
The owner can find the work in Campaigns & Problems, edit it, and attempt its
open Problems. It has no automatic expiry. Run the command again after changes to
update those same objects; immutable revisions, Attempts and submissions keep
those identities and their historical pins.
The task, its bound reference-input bytes, Attempts and submitted artifacts stay private. Package Skills and evaluation definitions—including review rubrics, Contracts and checker programs—are separately published reusable registry resources; they do not inherit the task’s privacy. Keep task-specific material in the statement or bound reference inputs, and put only material intended for reuse in those registry definitions.
Use set_work_visibility to open participation as unlisted, then listed when
ready and authorized. These are transitions on the same work. Sharing exposes
its retained history according to the current visibility and any sealed-entry
policy. Updating a package preserves existing visibility; successful validation
never shares or lists work. See Drafts, publication and discovery.
Incomplete ideas use save_work_draft. Preparing a valid new proposal materializes
ordinary private work; subsequent preparation updates it. A proposed replacement
for existing shared work uses an isolated validation candidate until publication,
so contributors cannot silently replace a task others are already attempting.
There is no public authoring-preview command, grant or creation/list/revocation
tool. Old tfp preview calls stop with replacement guidance. Historical preview
records keep their existing access controls and cleanup; they are not draft
Campaigns to manage or promote. Isolated developer PR deployments are unrelated.
Testing and review
Private work is available to its owner and administrators. To test with another
participant, explicitly share the same work as unlisted and give them its link.
A fresh solver uses get_problem, starts an Attempt, submits artifacts by role
and receives the normal verification results. Review runs the real protocol:
reviewers need the configured eligibility, access to the work, and no conflict.
Owning the work does not automatically make its author an eligible reviewer.
Reconciliation against existing state
The local authoring compiler is state-independent: it turns the files in one package into a normalized semantic plan. Server validation then reconciles that plan with the current TfP state.
A Campaign package is partial. Only the Campaign and the Problems present in the package are reconciled. A Problem that already belongs to the Campaign but is absent from the package is untouched. Omission never means deletion, archival or removal.
For every included Problem the server determines one action:
| Action | Meaning |
|---|---|
create |
No Problem with this slug exists; create it and revision 1. |
noop |
The Problem exists and its current effective task is semantically identical. Create no revision. |
update |
Only mutable Problem metadata changed (title, summary, priority weight); update it without creating a revision. |
revise |
The effective task changed; create the next ProblemRevision and make it current. |
conflict |
The caller cannot safely perform the change: another User owns the slug and the caller is not a live platform administrator, the Problem is archived, or an immutable Problem property (such as structure) differs. |
Whether a Problem needs a revision is based on its effective revision
semantics, not on whether its source files were uploaded again. The
comparison covers the statement, the benchmark block, the pinned
SkillVersions, the pinned VerifierContract, and the exact
reference-input bindings (keys, media types, content hashes and, for a
link binding, the URL), plus
any other field whose change canonically requires a new
ProblemRevision. revision_note is not part of the comparison.
VerifierImplementations are not part of the ProblemRevision
fingerprint: a new or corrected Implementation under the same Contract
does not revise Problems (same Problem, same Contract, new checker
Implementation is a noop for the Problem, while the Implementation
is registered and conformed). Mutable Problem metadata is compared
separately.
The slug is how reconciliation identifies a Problem. Changing a slug in an authoring package is not treated as renaming an existing Problem; it names a different Problem. Renaming, if supported, is a separate explicit operation. The implementation may store a derived semantic fingerprint for efficient comparison; the fingerprint is infrastructure, not a participant-facing object or authoring field.
An existing owned slug is therefore not automatically a later revision.
Republishing an unchanged package is a noop.
Campaign metadata is reconciled separately: a new Campaign slug is
create; an existing Campaign owned by the caller is reuse; explicit
changes to its mutable metadata are update; an optional Campaign
field omitted from campaign.json does not reset an existing value
merely because the compiler supplies a default for creation; a
Campaign owned by another User is conflict unless the caller is a live platform
administrator. Administrators publish their own uploads as themselves; they do
not acquire the existing campaign or problem ownership. Publication rechecks
current authority and the observed owner, metadata and revision state. Package
maintenance is recorded in the append-only work audit history alongside the
publishing actor and upload. An earlier successful validation cannot preserve
revoked administrator authority.
Validation returns the reconciliation alongside the plan:
"reconciliation": {
"campaign": { "slug": "translate-openalex-en-zh", "action": "reuse", "campaign_id": "cmp_…" },
"problems": [
{ "slug": "openalex-en-zh-W123", "action": "noop", "problem_id": "prb_…", "current_revision_id": "prv_…" },
{ "slug": "openalex-en-zh-W124", "action": "revise", "problem_id": "prb_…", "current_revision_id": "prv_…", "next_revision_number": 2 },
{ "slug": "openalex-en-zh-W125", "action": "create" },
{ "slug": "openalex-en-zh-W126", "action": "conflict", "reason": "owned by another User" }
],
"counts": { "create": 1, "revise": 1, "update": 0, "noop": 1, "conflict": 1 },
"warnings": [ { "code": "possible_duplicate", "slug": "…", "of": "…" } ]
}The CLI prints the counts and lists every create, revise, update
and conflict by slug. Any conflict makes the upload invalid for
publication rather than silently choosing an interpretation.
Validation records the current state it reconciled against (each
existing Problem's id and current revision id, the Campaign's owner).
publish_authoring_upload executes that stored reconciliation and,
before mutating, verifies the state is still what the validator saw;
if it changed, publication refuses with stale_server_state and the
author validates again. The two refusals are distinct:
stale_plan_hash means the author's package changed;
stale_server_state means TfP changed since the package was
validated. This keeps two authoring sessions, or two publication
batches, from creating revisions on stale state: two agents that both
planned revision 5 cannot both publish it; the second revalidates and
becomes a noop or revision 6.
Duplicate detection
The primary identity mechanism is the Problem slug. Generators over an
external corpus should derive it deterministically from the corpus's
stable identifier, so the same source reached twice reaches the same
Problem: same slug and same semantics is noop; same slug and changed
semantics is revise.
As a non-blocking safeguard, validation reports the warning
possible_duplicate when a Problem to be created has the same
effective semantic fingerprint as another live Problem in the same
Campaign. Sharing only a paper or dataset is not by itself a duplicate,
because two Problems may ask different questions of the same input.
The warning is advisory and never merges Problems.
Publication batches
A large Campaign may be published through arbitrarily many packages over time. Each package contains only a finite batch of ordinary Problem directories, within the upload limits, and names the same Campaign; shared mechanisms are reused by content hash across batches.
Campaign-root material applies to the Problems included in that batch.
Changing a root Skill, Contract or protocol does not retroactively
change Problems absent from the batch; their existing ProblemRevisions
remain pinned to the versions they were published with. To move
existing Problems to the new mechanism, include them again; each
becomes a revise.
tfp draft (or tfp publish) produces these batches itself (shards, above); an
author or a generator may also send them by hand.
There is no generative-Campaign object and no requirement that the author keep the entire Campaign in one local directory. A generator may stream corpus records, emit a batch, publish it, checkpoint its external cursor and continue.
Updating existing Problems and Campaigns
Authoring is incremental. A Problem directory whose slug does not exist
creates a new Problem. A directory whose slug exists and is owned by
the caller is compared with the current ProblemRevision: no semantic
change is noop; only mutable metadata changed is update; the
effective task changed is revise. A revise creates the next
immutable ProblemRevision, with changelog_md equal to
problem.json.revision_note or null, and makes it current. Attempts
against earlier revisions remain attached to those revisions.
A listed problem can be replaced directly only by its trusted owner or a live
platform administrator. Other authors submit a private single-problem proposal
for review; revision_review_required leaves the approved revision current.
Campaign and sibling listings are preserved. Publication results include
visibility and listed_in_explore on campaign/problem entries, evaluated at
response time (including replayed receipts); check those fields before announcing
a listing. New work is not automatically listed. See
Drafts, publication and discovery.
A Campaign directory behaves the same way over a set of Problems. It is a partial batch, not desired-state replacement: Problems omitted from the directory are untouched. Mechanisms with unchanged content hashes are reused rather than re-created. A newly created Campaign starts Unpublished; explicitly share as unlisted or list through the management visibility controls when authorized. Updates preserve the existing Campaign's visibility and individual Problem publication choices.
Archiving is explicit, never by omission: archive_campaign and
archive_problem (MCP, owner only) archive the named work; an archived object
leaves the default Explore listing, takes no new Attempts, and keeps its
record. Campaign archival also archives its open Problems. The browser's
Manage Campaigns & Problems screen offers Restore for campaigns and
Reopen for problems. Restoration preserves publication and existing
revisions; restoring a campaign leaves its archived children archived. Reopen
individual problems while their campaign is Live or Unpublished; restore an
archived campaign first. Existing Attempts retain
their pinned revisions. There is no delete or MCP restore tool. See
Publishing and unpublishing work for
Unpublished work, private testing, and browser status actions.
Not part of 8A or 8B: deletion or archival by omission, ownership
transfer, cross-user Campaign attachment, restoring through an authoring package, yanking a
Contract, automatic merging of duplicates, reconstructing authoring
files from published state (tfp pull), uploading executable code by
any route other than a public digest-pinned image, generative
Campaigns, any authoring-specific participant-facing object.
Decisions embedded in this spec
Settled by the owner, 2026-09-03:
- Capabilities, not RBAC.
authorandcustom_verifierare named abilities granted per User by an operator and intersected into tokens; no roles, no hierarchy, no per-object ACLs. - Package upload is transport only. The directory is the canonical package; the ZIP exists to move bytes, is unpacked into the same file tree lint reads, and is never shown, versioned or published.
- Durable private work uses the ordinary objects (supersedes the September 3 preview workflow, approved October 16). New Campaigns and Problems use permanent IDs and real slugs, remain editable and testable while private, and share/list through explicit visibility transitions. Historical preview access checks remain for existing records.
- The plan hash gates publication.
validatecomputes it,draftandpublishrequire it, a mismatch is refused, and publishing an already published upload is a no-op that returns the same ids. - Custom checkers are digest-pinned public OCI images, registered
at validation, provisioned per unique digest, conformance-gated.
Lint refuses tags; validation registers the image and enqueues
provisioning; the server runs the fixtures once the image is
available; one runner per unique runtime digest; the only author
surface beyond the package is
tfp verifier status. - No
context.md, nochangelog.md. A later revision's reason is the optionalproblem.json.revision_note, null on first publication; background is a reference file or a Skill; findings are Notes. - Shared mechanisms beside
campaign.json, listed once by hash. Bulk Campaigns are a generator script emitting ordinary Problem directories plus publication batches on the wire; no template or generated-Problem object. - Publication is partial reconciliation, not desired-state
replacement (2026-09-04). For each included Problem the server
computes
create/update/revise/noop/conflictagainst current state; omitted Problems are untouched; revision equality is semantic, not upload-based; generated corpus Problems use deterministic slugs from stable source identifiers; publication rechecks the state validation reconciled against and refusesstale_server_staterather than guessing.
Script runtime enablement does not change the catalogue image: each name pins the same actual OCI digest on every deployment. Unbound script uploads cannot materialize as a draft; validate again after the operator enables the runtime. A bound upload provisions the resource shape requested by its checker. Automatic spare-compute dispatch requires a non-yanked, conformance-passed implementation for every sandbox stage, including when consuming an already cached candidate.