Contract Application Manifest (CAM) 1.1.0

Experimental draft. Document format versions: CAM 1.1.0, UI 1.0.0. Implementation baseline: 6a3c3c4. Documentation revisions do not change these format versions.

CAM model

A CAM application is advertised by a deployed contract, called the CAM host contract. To load the application, an agent or viewer needs the chain ID of the network hosting that contract and its deployed address. The agent or viewer calls camURI() and camHash() on that contract to obtain the manifest URI and expected manifest hash; ICamApp defines these discovery methods. The repository's EVM adapter validates the URI, asks its supplied resource loader for manifest bytes, and by default checks their hash before JSON parsing. The manifest declares ABI/UI resources, contract calls, and continuations. The agent or viewer uses the parsed routes and arguments to prepare requested contract calls.

The manifest joins each contracts.Name namespace to an ABI resource. This repository's EVM profile resolves Name to a destination address through the host contract's contractAddress(string) binding API. That API is a CamRoot extension, separate from ICamApp's URI/hash interface. The manifest's ui namespace selects a UI resource; its routes namespace selects routes declared in the same manifest. Mutable address bindings are outside the manifest hash.

Each route joins one contract call to a next invocation. A read exposes normalized results as $outputs while resolving its continuation arguments for a named UI node. A write prepares a transaction and names another route to follow after success. The agent or viewer prepares requests; called contracts enforce their business rules. An agent can inspect the resolved UI and action model without drawing an interface, while following the declared read-to-UI handoff.

A bundle author supplies the manifest and ABI/UI resource bytes under their declared URIs. For publication, the author makes those resources available through a chosen transport. The authorized CamRoot owner advertises the manifest URI/hash and configures address bindings through separate transactions; the owner need not be the bundle author. Editing resource bytes does not update CamRoot's stored hash.

Document rules govern the manifest/UI graph. EVM profile rules describe this repository's @cam/evm-viem adapter; UI resolution describes @cam/screen. Viewer policies describe the @cam/viewer session library and the web viewer that uses it. Conformance checks inspect supplied bundles for publication defects, without establishing live state, account authority, or transaction success.

Resolve and verify a bundle

The EVM adapter checks that its RPC client is on the selected chain. The selected chain identifier matches eip155:[1-9][0-9]*, with a number no greater than 9,007,199,254,740,991. The adapter requires supportsInterface(0x03625ff6) to return boolean true, then reads the advertised URI and hash. A failed interface read or any other return value blocks loading. The CAM interface ID excludes the inherited ERC-165 selector.

These bootstrap signatures are known before the advertised ABI resources are loaded:

function supportsInterface(bytes4 interfaceId) external view returns (bool);
function camURI() external view returns (string);
function camHash() external view returns (bytes32);
function contractAddress(string contractName) external view returns (address);

The first three methods identify and read ICamApp. The fourth is the binding extension used by this profile; an ICamApp support response does not attest to that extension. camURI() returns a string; the interface does not require external storage. CamRoot's URI-content check rejects only an empty string. camHash() must return a 0x-prefixed 32-byte hex value.

After URI validation, the adapter invokes the supplied resource loader and compares the returned manifest bytes' Keccak-256 digest with camHash() before JSON parsing. A mismatch rejects the load. A zero hash is rejected before calling the loader by default; explicit unsigned loading permits that zero value and skips the manifest commitment check. JSON decoding rejects invalid UTF-8 and duplicate object keys.

Contract ABI and UI declarations carry integrity as sha256:0x followed by exactly 64 lowercase hex digits. Resource loading checks SHA-256 of the original bytes before parsing. The nonzero manifest commitment therefore covers the declared resource digests, which cover the ABI/UI bytes. It does not cover address bindings or deployed contract code. Verified bytes do not grant permissions or prove an ABI matches deployed code. A successful document parse alone does not establish resource integrity.

URI siteAccepted form
Repository publication checks: manifest URIAbsolute https://… or supported ipfs://CID/….
EVM adapter's manifest URI gateAlso permits http://…. HTTP(S) rejects credentials, raw controls/DEL, and backslashes; query and fragment components are permitted.
Parsed CAM ABI/UI declaration./… relative to the manifest, or supported ipfs://CID/…. No query, fragment, percent escape, backslash, empty segment, or ./.. segment.

Current URI policy: the EVM adapter rejects data: URIs and raw JSON strings before calling the supplied loader. Replacing that callback does not bypass the URI gate. For permitted URI keys, the callback can return fetched, cached, in-memory, or contract-derived bytes. URI acceptance does not require physical external storage or provide a transport; the shared HTTP loader performs a network fetch.

Supported IPFS forms are CIDv0 base58btc or lowercase CIDv1 base32 with raw or dag-pb codec; both require a SHA-256 multihash with a 32-byte digest. IPFS manifest and resource URIs share the path restrictions above; bare ipfs://CID is allowed, while credentials, trailing slashes, queries, and fragments are rejected. Accepting an IPFS URI does not provide a transport.

Relative resources resolve from the directory of the original camURI, dropping its query and fragment. For example, https://example.org/app/main.json?rev=2#section plus ./ui/main.json resolves to https://example.org/app/ui/main.json.

Loader policy: the shared HTTP loader pins an allowed origin, rejects redirects, and bounds decoded responses to 2 MiB inclusive. The adapter also checks returned byte size. Custom loaders own their transport, access, and timeout policies; URI resolution is not an access grant.

Address bindings and their lifetime

A contracts.Registry namespace resolves through contractAddress("Registry"), using the bare suffix. Nonstring, invalid, and zero addresses are rejected. Bindings may change independently of the manifest commitment. Loading does not promise an atomic snapshot of URI, hash, and bindings.

Viewer session policy: the repository's viewer session resolves bindings and ABIs at load and reuses them during navigation and preparation. A prepared write carries the resolved destination; simulation and submission use that destination without rereading the mapping. Later binding updates do not retarget that call. Reloading the session resolves bindings again. Other agents and viewers choose their lookup policy.

Interpret the manifest graph

The top-level manifest has exactly cam, entry, namespaces, and routes. Supported cam values are "1.1.0" and "1.0.0". entry names an existing route. Unknown structural fields are rejected at the manifest, namespace, route, and invocation boundaries.

NamespaceDeclarationInvocation target
contracts.Nametype: "contract", abiURI, integrity. Name matches [A-Za-z_][A-Za-z0-9_]*.A function in the declared ABI, at the host-bound address.
uitype: "ui", uri, integrity.A named node in that UI document.
routestype: "routes"; no resource fields.A route declared in this manifest.

Invocations have exactly {"namespace":"…","function":"…","args":{…}}. The namespace must be declared with an allowed type for that edge. In CAM routes, function is a nonempty literal string; argument values may contain expressions recursively. Route names and invocation argument keys need only be nonempty strings, unlike input identifiers.

A route has kind, inputs, call, and then. inputs is its exact callable interface: an array of unique names matching the expression identifier grammar. Resolving either invocation rejects missing or extra inputs, including unused inputs.

Route kindcall edgethen edge
readA contract namespace, targeting a view/pure function.The UI namespace, targeting a named UI node.
writeA contract namespace, targeting a nonpayable/payable function.The routes namespace, targeting another route.

The namespace joins in this table are manifest-parser rules for route.call and route.then. ABI mutability is checked separately by the EVM adapter against the loaded ABI. Parsing the manifest alone does not inspect an ABI or deployed code.

then is the continuation descriptor. During a read, the repository's viewer session starts the call with empty outputs, then resolves then.args against the normalized results. The author selects which $outputs values reach the UI node. The session's write preparation resolves both descriptors with empty outputs. The @cam/core route helpers instead resolve the context supplied by their caller.

Repository conformance checks diagnose $outputs references in route.call.args, route.value, and write-route then.args; read-route then.args may select them. Accepting expression syntax does not establish that a reference is usable at that phase.

Document parsing and conformance permit a write-to-write continuation. The viewer session does not reject that destination during first-write preparation. If the first transaction succeeds, the web viewer then requests navigation, which rejects the next write route; the web viewer reports transaction success with a view-refresh failure. That failure does not roll back the first transaction. Supporting another write requires an agent or viewer to prepare and authorize it; no declaration automatically submits a second transaction.

Bind EVM calls and results

The EVM adapter matches the resolved contract invocation against the verified ABI. A bare function name must be unique; a full signature selects an overload. Signature matching uses the supplied ABI's type spellings without whitespace, expands tuple components as (uint256,address), and retains array suffixes. It does not rewrite supplied uint to uint256 during matching.

Arguments must have exactly the ABI input names, which must be unique and nonempty. The adapter orders them by the ABI and validates values recursively. Decimal strings preserve large integers; accepted numeric inputs are limited to safe integers.

ABI typeAccepted value
uintN, intNN is 8–256 in steps of 8; bare aliases mean 256. Decimal strings match -?[0-9]+, or numbers are safe integers. Unsigned range is 0 through 2N−1; signed range is −2N−1 through 2N−1−1. Leading zeros and negative zero are accepted where in range.
address0x plus 40 hex digits; normalized to lowercase.
bytes, bytesN0x plus whole hex byte pairs; dynamic bytes permits empty 0x. N is 1–32, with exact byte length.
bool, stringBoolean or string respectively.
Dynamic array, named tupleAn array of recursively checked values, or an object with exactly the tuple component names. Fixed-size arrays are unsupported.

The entire ABI resource is validated, including unused functions. Unsupported parameter types, malformed function declarations, duplicate signatures, and unnamed or duplicate tuple component names reject the ABI. Tuple names are required for both inputs and outputs. Top-level outputs may be unnamed because their positions identify them. Nonfunction entries receive only object/type validation.

Read caller and outputs

A read route must target a view or pure function. A supplied account.address is also the RPC caller, independently of whether any expression references it. Caller-dependent reads may therefore differ across accounts. Without an account, the adapter leaves the caller unspecified. These reads require no wallet signature.

The result is always an outer positional list: no ABI outputs gives []; one output gives [value], even when value is itself an array or tuple. Integers become canonical decimal strings, addresses become lowercase strings, bytes retain hex letter case, and tuples become named objects. Counts, shapes, and ranges that disagree with the ABI are rejected. Use $outputs.0 or $outputs.0.component, not top-level ABI output names.

Native transaction value

CAM 1.1.0 write routes may declare value, separate from call.args. It resolves as a literal or expression to a decimal uint256 value in native base units. Payable targets require an explicit value, including "0". Nonpayable targets reject any declared value, including zero. Read routes and CAM 1.0.0 reject the field.

No unit conversion or token transfer is implied. An amount passed as an ABI argument remains an argument; transferring tokens remains a contract operation. Writes have no modeled ABI return values in route context.

Resolve expressions and handoffs

Expression strings select supplied context values. They occupy the whole string and resolve recursively within argument arrays and objects, without changing keys. A single leading $ starts a reference; $$ escapes one dollar sign. Other strings remain literal. There is no arithmetic, interpolation, operator-object DSL, JavaScript evaluation, or resource fetch in this mechanism.

reference  = "$" identifier ("." segment)*
identifier = [A-Za-z][A-Za-z0-9_]*
segment    = identifier | arrayIndex
arrayIndex = "0" | [1-9][0-9]*  (at most 9,007,199,254,740,991)

Indexes are canonical nonnegative decimals. Brackets, empty segments, minus signs, and leading-zero indexes are invalid. Arrays are selected by index and objects by own fields. Missing roots or segments fail resolution; an existing null leaf is valid, but traversal through null fails. A selected string is not reinterpreted as another expression.

Resolved values are inert: JSON scalars with finite numbers, arrays, and plain objects. Undefined, functions, bigint values, and cycles are rejected. EVM integer outputs have already become decimal strings.

Expression rootSupplied value
$hostSelected CAM host contract's chainId and address.
$accountOptional account with address. Absent accounts cannot resolve references. Also see the independent RPC caller behavior.
$inputsCurrent route's exact inputs.
$outputsCall results supplied in context; the viewer session starts calls and write preparation with an empty array.
$stateUI-local editable values, available during UI resolution.
$viewThe value supplied as a named UI node's view argument; any inert value. It is separate from the resolved UI tree and editable state.
"$inputs.itemId"    → the supplied value, preserving its type
"$outputs.0.name"   → a named field in the first result
"$$inputs.itemId"   → the literal "$inputs.itemId"
"ID: $inputs.id"    → literal text, without interpolation

Do not manufacture write outputs from events, receipts, or simulation results.

Interpret UI 1.0.0

The UI resource is another declared graph, joined to routes by named node invocations. The repository's UI runtime resolves a selected node into a structured interface tree with action descriptions. An agent can inspect that tree without drawing it, and a viewer can render it; read continuations still target the UI namespace.

The document has exactly ui: "1.0.0" and a nonempty nodes object. Node names are nonempty strings. Named nodes declare requires as [] or ["view"], and receive exactly those argument names. Inline children cannot declare requires. A supplied view becomes $view during resolution; it is the handoff value, not the UI tree. Node arguments cannot shadow runtime roots.

Elements have closed structures and props. String props may contain references but must resolve to strings. There are no arbitrary HTML, style, script, or event-handler fields.

ElementRequired structure and propsOther rules and behavior
Screenprops.title, childrenView container.
FragmentchildrenGroups children without a title.
Textprops.textText display.
Addressprops.label, props.addressAddress resolves to 0x plus 40 hex digits.
Statusprops.label, props.valueprops.format: literal bytes, durationSeconds, or stringList.
Nftprops.contractAddress, props.tokenIdContract address has the address shape. Token ID may resolve to any inert JSON value; this node does not enforce a uint256 token ID or perform a contract call.
TextFieldprops.label, stateenabledWhen; stores editable text.
DocumentHashFieldprops.label, literal props.algorithm: "sha256", stateprops.sourceURI, enabledWhen; editable value, with digest calculation offered by the web viewer.
AmountFieldprops.label, literal props.unit, stateenabledWhen; stores a base-unit integer string.
NonNegativeIntegerFieldprops.label, stateprops.min, props.max, enabledWhen; inclusive decimal bounds.
Includecall into uiComposes selected named nodes.
Buttonprops.label, call into routesenabledWhen; describes a route action for an agent or viewer to invoke.

Status with stringList requires an array of strings. bytes/durationSeconds require unsigned decimal integer strings or safe nonnegative integer numbers. Unformatted value presentation belongs to the viewer.

Publication policy: conformance follows statically known reference types through route/UI handoffs. For Nft.tokenId, known references must supply an integer or string; boolean, tuple, or array references receive a diagnostic. This check does not restrict literals at that site. The UI runtime accepts any inert token ID, with no contract call implied.

Includes and route actions

An Include invokes ui; its function resolves to one node name or an ordered array of distinct names. Empty arrays include nothing. Targets must exist, receive their exact required arguments, and form no Include cycle. Selected nodes share runtime/state and replace the Include in selection order. A Button invokes routes; its function resolves to one nonempty route name and its arguments supply that route's inputs.

State initialization and current resolution

Fields declare state.key and state.defaultValue as expression-capable values. The key resolves to an expression identifier, the default to a string. Use the UI runtime's initial resolution when constructing a new view's field state: that operation collects defaults into empty state, then resolves the UI and actions. Duplicate keys reject initialization even if defaults agree. Defaults and Includes selecting fields cannot depend on editable $state; Includes selecting only actions may use state once initialized.

Use current resolution to resolve the interface again with state supplied by the agent or viewer, such as an edited draft. This UI operation does not merge defaults, prune old keys, or initialize newly selected fields. Draft editing alone does not submit a transaction or change contract storage. The repository's viewer session starts fresh state for ordinary route navigation; Back may restore compatible saved drafts after rereading the prior route. Other agents and viewers choose persistence, account-change, focus, wallet, and confirmation behavior; UI JSON has no fields that prescribe these policies.

Typed drafts

Numeric fields expose canonical decimal strings for valid drafts, including "0". Empty, incomplete, invalid, or input-method-composing drafts expose null. Editing is bounded to 2,048 UTF-16 code units and 1,024 canonical significant digits; these limits do not replace ABI ranges or business constraints.

For every numeric field, state.defaultValue must resolve to a digit string or an empty string. An AmountField default always uses base units, regardless of its interactive editing mode. With unit.decimals: 2, default "125" initializes canonical state "125", corresponding to display value 1.25. Default "1.25" is invalid and rejects initialization. A user may instead type "1.25" during display-unit editing, which produces canonical state "125". Empty defaults initialize null; invalid or out-of-bounds defaults reject initialization rather than becoming null.

Integer/base-unit editing permits only ASCII digits; leading zeros are removed canonically. Integer bounds resolve with empty editable state to nonnegative decimal strings, are inclusive, and require min ≤ max when both are supplied. There is no implicit uint256 editor maximum. Display amount editing additionally permits one decimal point: .5 and 1. can be valid, while . is incomplete. Signs, whitespace, separators, hexadecimal notation, and exponents are invalid.

AmountField.props.unit contains exactly the literal nonempty strings assetId, baseLabel, displayLabel, and integer decimals from 0 to 255. Scaling is exact: excess fractional precision is accepted only for zero digits, without rounding. The unit declaration does not verify an asset or authorize spending.

DocumentHashField declares algorithm sha256, a state binding, and optional sourceURI. On request, the repository's web viewer can calculate a 0x-prefixed 32-byte digest from selected file bytes or bytes fetched from that URI. Resolving the UI node itself does not fetch or hash a document. Manual/default strings are not digest-validated by the UI runtime; the receiving ABI or contract owns acceptance. This requested calculation is separate from ABI/UI resource integrity checking.

Availability

"enabledWhen": {
  "member": "edit",
  "in": "$view.availableActions"
}

Only Buttons and fields accept enabledWhen. It has exactly a nonempty literal member and in, either a reference resolving to an array of nonempty strings or a literal such array. Membership is case-sensitive; absence disables but retains the control. Disabled controls still resolve props, arguments, and defaults. This is not an authorization check.

Literal membership strings reject single-dollar references and do not unescape doubled dollars: "$$edit" compares with those two dollar signs.

Execute and report outcomes

The repository's viewer session starts a read with the selected CAM host contract identity, optional account, exact inputs, and empty outputs. Through the core and EVM helpers, the session resolves and ABI-checks the call, requests RPC execution without committed transaction effects, normalizes the result, then resolves then.args with those outputs. Failed calls and invalid results cannot produce an ordinary successful UI view.

The session's write preparation resolves the contract call and continuation with empty outputs. An agent or viewer may request simulation as an RPC trial against current node state, without committed effects. Wallet submission returns a transaction hash; successful included execution can apply contract changes. Reverted execution does not apply the attempted contract changes. Neither a simulation nor a hash establishes successful confirmation.

The repository's web viewer waits for a successful receipt, then requests session navigation to the route named by the prepared write continuation. Successful read navigation refreshes the UI. The web viewer reports a rejected write-route destination or failed refresh separately from the already successful transaction; that UI failure does not roll back transaction effects.

Agents and viewers choose confirmation, timeout, replacement, reorg, and stale-interaction policy. An application also decides when an account change requires rereading caller-dependent results and whether drafts or prepared calls remain usable. CAM does not prescribe these policies. A timeout or uncertain submission is not proof of failure; resolve wallet/chain status before retrying a consequential write.

Worked bundle

Assume a CamRoot host binds Registry with labelOf(uint256 itemId) view returns (string) and setLabel(uint256 itemId, string label) nonpayable. The manifest joins that ABI to an editor UI and routes show/save. The registry contract owns edit authorization.

The digests below match the exact UTF-8 text in the resource code blocks, without an added trailing newline. Changed resource whitespace requires new digests.

{
  "cam": "1.1.0",
  "entry": "show",
  "namespaces": {
    "contracts.Registry": {
      "type": "contract", "abiURI": "./abi/Registry.json",
      "integrity": "sha256:0xe11b67b27adf4e4ff5a34bb0b2c91afcc61cfae6a90aa33223e3d954063dc8e3"
    },
    "routes": { "type": "routes" },
    "ui": {
      "type": "ui", "uri": "./ui/main.json",
      "integrity": "sha256:0x78d72558471330294d9bccf3d53a7f3f1e66a83fde3fa4c7665e43374f7c2da1"
    }
  },
  "routes": {
    "show": {
      "kind": "read", "inputs": ["itemId"],
      "call": {
        "namespace": "contracts.Registry", "function": "labelOf",
        "args": { "itemId": "$inputs.itemId" }
      },
      "then": {
        "namespace": "ui", "function": "Editor",
        "args": { "view": { "itemId": "$inputs.itemId", "label": "$outputs.0" } }
      }
    },
    "save": {
      "kind": "write", "inputs": ["itemId", "label"],
      "call": {
        "namespace": "contracts.Registry", "function": "setLabel",
        "args": { "itemId": "$inputs.itemId", "label": "$inputs.label" }
      },
      "then": {
        "namespace": "routes", "function": "show",
        "args": { "itemId": "$inputs.itemId" }
      }
    }
  }
}

show requires {"itemId":"7"} or another explicit item ID. An entry invocation does not invent inputs; an agent or viewer starting an entry without inputs needs a separate input-free entry design.

{
  "ui": "1.0.0",
  "nodes": {
    "Editor": {
      "requires": ["view"], "element": "Screen",
      "props": { "title": "Edit label" },
      "children": [
        {
          "element": "TextField", "props": { "label": "Label" },
          "state": { "key": "labelDraft", "defaultValue": "$view.label" }
        },
        {
          "element": "Button", "props": { "label": "Save" },
          "call": {
            "namespace": "routes", "function": "save",
            "args": { "itemId": "$view.itemId", "label": "$state.labelDraft" }
          }
        }
      ]
    }
  }
}

Registry ABI

[
  {
    "type": "function", "name": "labelOf", "stateMutability": "view",
    "inputs": [{ "name": "itemId", "type": "uint256" }],
    "outputs": [{ "name": "", "type": "string" }]
  },
  {
    "type": "function", "name": "setLabel", "stateMutability": "nonpayable",
    "inputs": [{ "name": "itemId", "type": "uint256" }, { "name": "label", "type": "string" }],
    "outputs": []
  }
]

The agent or viewer requests labelOf(7), obtaining one normalized string at $outputs.0. The continuation constructs view: {itemId, label} for Editor. Initial UI resolution sets labelDraft from that label; editing changes only the draft. Invoking Save captures itemId and labelDraft, prepares the nonpayable setLabel call without native value, and prepares show as its next route.

After successful submission and confirmation, the repository's web viewer requests show again and resolves the updated interface. These examples establish document and handoff shapes, not a deployment or live transaction.

Evidence and limits

The pinned implementation baseline is 6a3c3c4. The sources below follow that exact revision, not a moving branch. The reference's own revision is linked from the version index.

The examples and repository tests do not establish interoperability with independently implemented agents or viewers.