Publishing guide
Building an agent or tool that reads specs? See the API for agents.
Two ways to publish a spec
You can publish a spec on the website or by API/CLI — both run the exact same content validation, so either path produces an identical, verified spec.
On the website (easiest)
- Sign in and go to Submit a spec (the “Publish a spec” button on the home page).
- Paste your
.spec.md, or upload a file. The editor accepts a structured form or raw markdown — author the spec in your own editor and paste it in. - Click Preview. You’ll see any content errors inline (the same checks that run in CI). Fix them and re-preview.
- Click Publish. The spec goes live immediately: its page is
/specs/{publisher}/{name}, and agents fetch the raw markdown at/specs/{publisher}/{name}.md.
Each {name}@{version} is immutable — to release changes, bump the version field and publish again.
With the CLI
Create a token under Account → API tokens, then (Node 20+, nothing to install):
npx @canra/cli login # paste the token when asked
npx @canra/cli publish my-spec.spec.md
By API
POST the raw markdown (the request body is the spec, Content-Type: text/markdown) to /api/specs, authenticated with a token (create one under Account → API tokens) or via OIDC trusted publishing from CI:
curl -X POST https://canra.dev/api/specs \
-H "Authorization: Bearer $CANRA_TOKEN" \
-H "Content-Type: text/markdown" \
--data-binary @my-spec.spec.md
To validate without publishing, POST the same body to /api/specs/validate — it returns { ok, errors, name, version }.
Spec quality bar
A spec is ready to publish when it has:
- A clear description of what the spec covers and what is out of scope
- Enough structure (H2 sections) that an AI agent can implement from the spec alone
- Behavioral contracts or requirements using RFC 2119 language (MUST/SHOULD/MAY)
- At least 10 test vectors covering common inputs
- At least 5 test vectors covering error cases and edge cases
- Implementation notes covering common pitfalls
- References to governing standards (when they exist)
File format
Spec files use the .spec.md extension with TOML front-matter between +++ delimiters.
Front-matter schema
+++
# Required
name = "spec-name" # Lowercase, hyphens for multi-word
version = "1" # Incrementing integer version
title = "Human-Readable Title"
status = "draft" # draft | stable | deprecated
# Classification
keywords = ["keyword1", "keyword2"]
difficulty = "moderate" # trivial | easy | moderate | hard | expert
# Optional links
repository = "https://example.com/..." # Source repository URL
homepage = "https://..." # Project home page URL
# References - each is a label with an OPTIONAL http(s) url (rendered as a live
# link on the spec page). There are no rfc/standard categories.
[references]
items = [
{ label = "RFC XXXX", url = "https://www.rfc-editor.org/rfc/rfcXXXX.html" },
{ label = "Some standard" }, # url is optional
]
# Test vector locations
[test-vectors]
inline = true # true if vectors are in the .spec.md
external = [] # Paths relative to spec directory
# --- Library specs only (omit if not applicable) ---
# API surface
[api]
functions = ["parse", "serialize"] # Public function names (snake_case)
types = ["MyValue", "MyError"] # Public type names (PascalCase)
+++
Markdown body sections
Specs can use any H2 (##) sections that make sense for the subject. There is no required set of sections and no required order.
For code library specs, the following structure is recommended:
## Overview— What library this spec defines, its functions, and what is out of scope## Types— Type definitions for all types used in function signatures## Functions— One H3 subsection per function with signature, parameters, algorithm## Behavioral Contracts— Per-function MUST/SHOULD/MAY requirements## Test Vectors— Function-call input/output pairs in fencedtest-vectorsblocks## Edge Cases— Tricky situations as a bulleted list## Implementation Notes— Practical guidance, pitfalls, performance tips## References— Markdown links to authoritative sources
For other spec types (UI flows, protocols, data formats, etc.), use whatever sections best describe the subject. Examples:
- A UI flow spec might use:
## Overview,## Screens,## Navigation,## Validation Rules,## Accessibility,## Test Vectors,## References - A protocol spec might use:
## Overview,## Message Formats,## State Machine,## Error Codes,## Test Vectors,## References - A data format spec might use:
## Overview,## Schema,## Encoding Rules,## Validation,## Examples,## References
Pseudocode signature notation
All specs use this language-agnostic notation for function signatures:
function_name(param: Type, optional_param: Type = default) -> ReturnType | ErrorType
Conventions:
- Snake case for function and parameter names
- PascalCase for type names
->separates parameters from return type|separates success type from error type?suffix means optional:indent: Int?= valuemeans default value:indent: Int = 0
Primitive types: String, Int, Float, Bool, Null, Bytes
Collections: [T] for lists, Map<K, V> for maps
Type definitions use tagged union syntax:
type JsonValue =
| Null
| Bool(Bool)
| Number(Float)
| String(String)
| Array([JsonValue])
| Object(Map<String, JsonValue>)
Test vector format
Test vectors use JSONL (one JSON object per line), both inline and in external files. The format is function-call-oriented:
Success case:
{"id": "parse-001", "function": "parse", "input": {"args": {"input": "{}"}}, "expected": {"result": {}}, "tags": ["object"]}
Error case:
{"id": "parse-err-001", "function": "parse", "input": {"args": {"input": "{\"a\":1,}"}}, "expected": {"error": "SyntaxError"}, "tags": ["trailing-comma"]}
Assertion (for generative functions):
{"id": "gen4-001", "function": "generate_v4", "input": {"args": {}}, "expected": {"assertion": "result[14] == '4'"}, "repeat": 100, "tags": ["version-nibble"]}
Fields:
id— Unique identifier within the specfunction— Which function is being testedinput.args— Map of parameter names to valuesexpected.result— Expected return value (success case)expected.error— Expected error variant name (error case)expected.assertion— Pseudocode assertion over result (generative functions)expected.result_hex— Hex-encoded byte result (for binary data)repeat— Number of times to call the function (for statistical assertions)tags— Categories for filtering (optional)
For binary input/output, use _hex suffixed field names with hex encoding.
Naming conventions
- Spec names: lowercase, hyphens for multi-word (e.g.,
base64,iso-8601) - Use the common name, not the RFC number (e.g.,
jsonnotrfc8259) - Version is tracked in the
versionfield of the TOML front-matter - Each published version is immutable: the registry stores it under its
{version}and rejects re-publishing an existing{name}@{version}. To release a new version, bump theversionfield and publish again.
Content restrictions
Canra specs are consumed directly by AI agents. To prevent prompt injection attacks and hidden content, all files are subject to strict content restrictions. The website’s Preview button and the /api/specs/validate endpoint run these same checks, so you get identical feedback whether you submit on the web or via CI.
ASCII-only requirement
All .spec.md and .jsonl files must contain only ASCII bytes:
- Printable ASCII:
0x20-0x7E(space through tilde) - Whitespace: tab (
0x09), line feed (0x0A), carriage return (0x0D) - No other bytes are allowed. This eliminates all Unicode-based attacks (zero-width characters, direction overrides, homoglyphs, invisible tags, etc.) at the byte level.
Unicode data needed in test vectors (e.g., \u00e9, \uD83D\uDE00) must be expressed using JSON \u escape sequences, never as raw UTF-8 bytes.
Use ASCII alternatives for typography: a plain double-hyphen for an em-dash, a single hyphen for an en-dash, -> for an arrow, and straight quotes " ' instead of curly quotes.
Banned patterns
| Pattern | Reason |
|---|---|
| HTML comments | Can hide instructions invisible in rendered markdown |
| Executable/loading/reframing HTML tags (script, style, iframe, object, embed, link, meta, img, svg, math, base, frame/frameset, template, applet, marquee, foreignobject, annotation-xml) | Script execution, remote loads, document reframing, or content hidden from a reviewer but live to an agent |
| Dangerous link schemes in markdown links/images (javascript:, data:, vbscript:) | Smuggle script or arbitrary payloads into a followed link |
| Prompt-injection directives (e.g. “ignore previous instructions”, “system prompt:”, “you are now”, system-style role tags) | Instructions aimed at the consuming AI agent rather than the reader |
ASCII control characters (0x00-0x08, 0x0B, 0x0E-0x1F, 0x7F) | Non-printable, can carry hidden data |
Note: ordinary inline-code mentions of inert UI tags (e.g. an input element named in a UI spec) are allowed — only the executable/loading/reframing tags above are banned. Plain https:// links are always fine; only the dangerous schemes are rejected.
Size limits
| File type | Limit | Value |
|---|---|---|
.spec.md | Line length | 500 characters |
.jsonl | File size | 1 MB |
.jsonl | Line length | 10,000 characters |
.jsonl | Lines per file | 10,000 |
.jsonl | String value length | 10,000 characters |
Test vector field restrictions
id: lowercase alphanumeric and hyphens (^[a-z0-9][a-z0-9-]*$)function: snake_case (^[a-z_][a-z0-9_]*$)tags: lowercase alphanumeric and hyphens (^[a-z0-9][a-z0-9-]*$)expected.error: PascalCase identifier (^[A-Za-z][A-Za-z0-9]*$)expected.result_hex: lowercase hex digits onlyexpected.assertion: pseudocode characters only (alphanumeric, operators, parens, brackets, quotes)- Only allowed top-level keys:
id,function,input,expected,tags,repeat
Review
Specs published via the website or API go live immediately once they pass validation — there is no gated review for self-published specs (the content rules above are the automated gate). Quarantine and takedown handle abuse after the fact.