Status: Draft
Type: Standards Track
Category: SDK
Created: 2025-10-20
Revised: 2026-04-16 (rebuilt against the shipping implementation at node/pvm/Lib/cowboy_sdk/ v0.1.1 and node/cli/ v0.0.24)
Requires: CIP-1 (Actor Message Scheduler), CIP-2 (Off-Chain Compute), CIP-3 (Fee Model), CIP-5 (Timers)
1. Abstract
This proposal defines cowboy_sdk, the Python SDK actor authors import, and the normative runtime surface (cowboy_sdk.runtime) that the PVM provides. It specifies the actor model, call primitives, continuation semantics, type system, verification builder, error hierarchy, entitlement manifest, and the cowboy CLI surface used to build, deploy, and invoke actors.
This CIP is normative: any Cowboy node that runs Python actors MUST expose the module layout and syscalls defined here. Any CLI that names itself cowboy SHOULD implement the subcommands in §14 with compatible flag semantics.
Key properties:
- Deterministic by construction. Non-deterministic Python primitives (wall-clock time, hardware FPU, unseeded randomness, filesystem, network,
set(), pickle) are trapped or replaced by SDK equivalents.
- Two-dimensional metering. Every syscall is metered in Cycles (compute) or Cells (state IO) per CIP-3.
- Handler-mode purity. Every actor method declares whether it may issue async side effects (
@pure vs @deferred); the runtime enforces this distinction.
- Continuations compiled at decoration time.
async methods decorated with @runner.continuation or @actor.continuation are lowered to a finite-state machine at class-load time, not dynamically at runtime.
- Entitlements gate all external-effect syscalls. Entitlements are declared in a deploy-time manifest and enforced at the Host API boundary.
2. Motivation
Python is the surface that actor authors touch most often. For the protocol to be interoperable across implementations and for actors to remain portable across node versions, the SDK contract must be stable and normatively specified — not left as implementation-defined.
A prior revision of this CIP was drafted before significant SDK consolidation landed (handler-mode purity, runtime module, error-code hierarchy, CREATE2 address derivation, entitlement manifest format). This revision is a rebuild against the shipping code rather than a forward design, so that the spec tracks implementation reality.
Subsequent revisions will extend the SDK for CIP-9 volume mounts (currently runner-side only) and CIP-7 / CIP-17 streaming helpers. Both are noted as gaps in §17.
3. Definitions
- Actor: A Python class whose public methods are invocable handlers. Identified by a 20-byte address.
- Handler: A public method of an Actor. Accepts a CBOR-decoded payload; returns a CBOR-encodable value or raises.
- Handler mode: A per-method property, either
pure (default) or deferred. Pure handlers MUST NOT issue async side effects.
- Syscall: A call from the PVM into the Host API (implemented in Rust) to perform a privileged operation. The SDK wraps syscalls.
- Continuation: A compiled FSM representation of an
async def handler that awaits a runner job or an async actor call. State survives block boundaries.
- Manifest: A JSON document declaring the entitlements a deployed actor requests. Attached to the deploy transaction.
- PVM: The Python Virtual Machine executing actor bytecode under determinism constraints (CIP-3 §4).
4. SDK Module Layout
The canonical package is cowboy_sdk, shipped with the PVM at node/pvm/Lib/cowboy_sdk/. Version 0.1.1 targets Python ≥ 3.11.
Top-level exports (cowboy_sdk/__init__.py) — every actor-facing symbol MUST be importable from the package root:
| Category | Exports |
|---|
| Actor | actor, ActorRef |
| Handler modes | pure, deferred |
| Call primitives | call, send |
| Continuations | capture, Capture, bounded_loop |
| Guards | reentrancy_guard, storage_guard, GuardedValue, GuardSet |
| Runner integration | runner (module), Retry, TaskGroup |
| Types | Address, BlockHeight, SoftFloat, ordered_set, CowboyModel |
| Verification | Verify |
| Runtime surface | runtime (module) |
| Errors | See §13 |
Private submodules (cowboy_sdk.codec, cowboy_sdk.pvm_sys, cowboy_sdk.pvm_time, cowboy_sdk.pvm_random) are implementation details and SHOULD NOT be imported by actor code. They MAY be imported by SDK extensions that understand the PVM boundary.
Conformance: an SDK implementation MAY add symbols to this list (marked as extensions) but MUST NOT remove, rename, or change the semantics of any symbol specified in this CIP.
5. Actor Model
5.1 Declaration
An Actor is a Python class decorated with @actor:
from cowboy_sdk import actor
@actor
class Counter:
def __init__(self):
self.count = 0
def increment(self, amount: int = 1) -> int:
self.count += amount
return self.count
A bare class named Actor without the decorator is also accepted for compatibility; the decorator form is canonical.
The decorator:
- Injects
self.address (read-only Address, the actor’s own 20-byte address).
- Injects
self.storage (a dict-like proxy over actor private state — see §5.5).
- Wraps every public method (non-underscore-prefixed) in a handler-mode enforcer (default
@pure; see §6).
- Scans for
@runner.continuation and @actor.continuation methods and installs the corresponding __resume callbacks.
- Registers the class as the handler entry point for the deployed actor.
5.2 Address Derivation
Actor addresses are derived via Ethereum-style CREATE2:
address = keccak256(0xff || deployer_address || salt || sha256(code))[12:]
deployer_address: 20 bytes, the sender of the deploy transaction.
salt: 32 bytes, caller-supplied (zero-padded if shorter).
code: the UTF-8 bytes of the Python source module.
- Output: 20-byte
Address.
The CLI helper cowboy actor address computes this locally without submitting a transaction (§14).
5.3 Deployment
Actor deployment is a transaction carrying:
- The Python source code (UTF-8 bytes).
- Optional constructor arguments (CBOR-encoded; passed to
__init__ after injection of address and storage).
- An entitlement manifest (JSON; see §12.1).
- Resource limits (
cycles_limit, cells_limit).
- A
salt (for CREATE2 determinism).
The PVM compiles the source, instantiates the class, invokes __init__ if present, and persists the resulting state. The deployment receipt carries the derived address.
5.4 Message Handler Dispatch
A message to an actor carries a method_name (UTF-8 string) and payload (CBOR-encoded bytes). Dispatch:
- The PVM loads the actor class and state.
method_name is looked up on the class; unknown methods raise AttributeError — converted to ActorCallError.
payload is CBOR-decoded into positional and keyword arguments per the method signature.
- The handler runs under its declared handler mode (§6).
- Return value is CBOR-encoded and returned to the caller (for
call()) or discarded (for send()).
The reserved handler on_timer(msg) receives timer fires (CIP-5). Additional reserved handlers MAY be introduced by future CIPs.
5.5 Storage
Actor state is a private key-value store scoped to the actor’s address. Access is exclusively through self.storage:
self.storage[key] = value # CBOR-encode value, write
value = self.storage[key] # read, CBOR-decode
del self.storage[key] # delete
key in self.storage # membership test
Raw-bytes escape hatches are available when CBOR framing would be wrong (e.g., already-serialized content):
self.storage.set_raw(key, data) # data: bytes, stored verbatim
data = self.storage.get_raw(key) # returns bytes | None
Keys MUST be UTF-8 strings. Values of self.storage[key] MUST be CBOR-encodable per §9.3.
Storage writes and reads are metered in Cells per CIP-3. Cross-actor storage access is prohibited; actor B cannot read actor A’s storage directly — it must call a method on A.
6. Handler Modes
Every handler runs under one of two modes, declared via decorator:
| Mode | Decorator | Async side effects allowed? |
|---|
| Pure (default) | @pure (implicit) | No |
| Deferred | @deferred | Yes |
A pure handler MUST NOT issue any operation that produces an async effect: send(), runtime.schedule_timer(), runtime.submit_job(), emitting callbacks. Synchronous call() and local computation are allowed. Any prohibited operation inside a pure handler raises PurityViolationError.
A deferred handler MAY issue any SDK operation subject to entitlement gates.
from cowboy_sdk import actor, deferred, call, send
@actor
class Example:
def balance_of(self, addr: str) -> int: # @pure by default
return self.storage.get(f"bal:{addr}", 0)
@deferred
def transfer(self, to: str, amount: int) -> None:
self.storage[f"bal:sender"] -= amount
self.storage[f"bal:{to}"] = self.storage.get(f"bal:{to}", 0) + amount
send(to, {"kind": "received", "amount": amount}) # async side effect
The split exists because pure handlers are safe targets for read-only RPCs (CIP-14 query path, external introspection tools); the runtime can execute them without the overhead of a deferred effect queue. The enforcement is a runtime check at the Host API boundary, not a static check.
7. Call Primitives
Three primitives for inter-actor and off-chain interaction:
| Primitive | Timing | Return value | Atomic with caller? | Typical use |
|---|
call() | same transaction (T+0) | yes | yes (shared rollback) | atomic cross-actor operations, reads |
send() | next block (T+1) | no | no (fire-and-forget) | notifications, triggering downstream work |
await runner.<op> | T+K, after off-chain execution | yes (resume value) | no | LLM, HTTP, MCP tool calls |
await ActorRef.async_* | T+K, after target actor responds | yes | no | actor-to-actor async request/response |
7.1 call() — Synchronous Cross-Actor Call
from cowboy_sdk import call
result = call(
target="0x1111...",
method="get_balance",
args={"user": "0xABCD..."},
cycles_limit=5000,
)
Arguments:
target: 20-byte address, as Address, hex string, or raw bytes.
method: UTF-8 handler name on the target.
args: CBOR-encodable dict of keyword arguments, or list of positional arguments.
cycles_limit: explicit cycle budget for the callee. MUST be passed; there is no implicit limit.
Semantics:
- Executes in the same transaction. Shared read-write set with the caller.
- Callee exceptions propagate — uncaught, they roll back the entire transaction.
- Call depth is capped at 32. Exceeding this raises
CallDepthExceeded.
- Return value is CBOR-decoded and returned to the caller.
The ActorRef wrapper provides syntactic sugar:
from cowboy_sdk import ActorRef
oracle = ActorRef("0x4444...", cycles_limit=5000)
price = oracle.get_price("ETH") # lowers to call("0x4444...", "get_price", ["ETH"])
7.2 send() — Fire-and-Forget Message
from cowboy_sdk import send
send(
target="0x3333...",
payload={"event": "order_created", "order_id": "abc"},
)
Semantics:
- Enqueues a message for delivery at the start of the next block.
- No return value;
send() returns immediately.
- Irrevocable: once a transaction commits, its sent messages are delivered even if later logic would want to cancel them.
- Message ID:
keccak256(sender ‖ nonce ‖ target ‖ keccak256(payload_cbor)).
- Allowed only from
@deferred handlers.
Authors SHOULD structure handlers so that all fallible synchronous calls complete before issuing send(). Raising after a send() does not unsend the message.
7.3 await runner.<op> — Runner Continuation
Decorated async def methods can await off-chain operations:
from cowboy_sdk import runner, capture
@runner.continuation
async def analyze(self, msg):
ctx = capture()
ctx.summary = await runner.llm(
prompt=f"Summarize: {msg['text']}",
max_tokens=200,
)
self.storage["last_summary"] = ctx.summary
The decorator lowers the function into an FSM at class-load time (§8). Available awaitables under runner:
| Awaitable | Purpose | Required entitlement |
|---|
runner.llm(prompt, ...) | LLM inference | oracle.llm |
runner.http(url, method, ...) | HTTP request | http.fetch |
runner.mcp(server, tool, args) | MCP tool call | varies by tool |
Each awaitable produces an opaque _RunnerAwaitable instance at authoring time; it is consumed by the FSM compiler and never observed at runtime.
7.4 await ActorRef.async_* — Actor Continuation
Actor-to-actor async calls use the @actor.continuation decorator:
from cowboy_sdk import actor, ActorRef, capture
@actor
class Aggregator:
@actor.continuation(timeout_blocks=100)
async def collect(self, assets: list[str]) -> dict:
ctx = capture()
ctx.results = {}
for asset in assets[:5]:
ctx.results[asset] = await ActorRef("0x4444...").async_get_price(asset)
return ctx.results
Semantics:
- Each
await ActorRef.async_<method>(...) is lowered to send(target, {...}) plus a state save.
- The target’s handler is invoked in a future block; when it completes, it delivers a callback message that resumes the caller’s continuation.
- Requires both caller and callee to be
@deferred.
8. Continuations
8.1 FSM Compilation
Both @runner.continuation and @actor.continuation compile at class-load time into a pair of functions:
<name>: the initial handler; starts the FSM, persists state 0, issues the first async effect, returns.
<name>__resume: the resume handler; called by the runtime on callback delivery, loads state, advances the FSM, issues the next async effect (or returns final value).
The generated code is never observed by actor authors. Its shape is non-normative.
8.2 capture() — Explicit Local State
Local variables that must survive an await MUST be attached to a capture() object:
ctx = capture()
ctx.first = await runner.llm("...") # first-await state
ctx.second = await runner.http("...") # second-await state
return ctx.first + ctx.second
Captured values MUST be of CBOR-encodable types (see §9.3). Attempting to capture a closure, generator, file handle, thread, or custom object raises CaptureTypeError at the await point.
Continuation state is stored at key __continuation:<correlation_id> within the actor’s own storage. Limits:
| Constant | Value | Meaning |
|---|
CONTINUATION_MAX_SIZE | 64 KiB | per-continuation serialized state |
CONTINUATION_MAX_COUNT | 100 | active continuations per actor |
Exceeding either limit raises ContinuationSizeLimitError or ContinuationCountLimitError.
8.3 Guards
Continuations can assert that specific storage keys were not modified during the async wait:
Method A — decorator-level:
@runner.continuation(guard_unchanged=["price", "config"])
async def act_on_price(self, msg):
decision = await runner.llm(f"Given price {self.storage['price']}, ...")
# On resume: if storage["price"] or storage["config"] was modified by any
# intervening transaction, raise StateConflictError BEFORE running the body.
...
Method B — object-level via GuardedValue:
balance = self.storage.guard("balance") # GuardedValue
result = await runner.llm("...")
new_balance = balance.value - 100 # raises StateConflictError if changed
self.storage["balance"] = new_balance
Guard fingerprints are keccak256(cbor(value_at_snapshot)). The fingerprint is stored in the continuation state and re-checked on resume.
8.4 Bounded Loops
await inside a Python loop requires an explicit upper bound:
from cowboy_sdk import bounded_loop, capture
@runner.continuation
async def fan_out(self, items: list):
ctx = capture()
ctx.results = []
@bounded_loop(max_iterations=10)
async def run():
for item in items[:10]:
ctx.results.append(await runner.process(item))
await run()
A bounded_loop with max_iterations = N generates N FSM states at compile time. Exceeding N at runtime raises LoopBoundExceeded. Unbounded iteration with await is rejected at class-load time.
8.5 Sequential Await Limit
A single continuation function MAY contain at most 8 sequential await points (not counting awaits inside bounded_loop). This is a compile-time limit; violations are rejected at class-load. The cap reflects the practical state-space ceiling of the FSM compiler; authors needing more should split into multiple continuations.
9. Type System
The SDK replaces or constrains Python built-ins whose semantics are non-deterministic or ambiguous across platforms.
9.1 Address
20-byte Ethereum-compatible address.
from cowboy_sdk import Address
a = Address.from_hex("0x7a3B6E...F92E")
b = Address(raw_bytes_20)
a.to_hex() # checksummed (EIP-55)
a.to_bytes() # 20-byte payload
Address.ZERO # sentinel
Address.JOB_DISPATCHER # 0x0000…0002
9.2 BlockHeight
Semantic int wrapper for block heights. Identity-equal to int at the bytecode level; useful for type annotations.
9.3 SoftFloat, ordered_set
SoftFloat is a software-floating-point type. Native Python float depends on the hardware FPU and is non-deterministic across platforms; authors MUST use SoftFloat instead. The SDK may alias SoftFloat = float when running under a PVM that enforces softfloat at the instruction layer; authors SHOULD still annotate with SoftFloat for clarity.
ordered_set is a dict-backed set with deterministic insertion-order iteration. Python’s built-in set() is prohibited in actor code.
9.4 CowboyModel
A dataclass-like base class for structured data. Feature set:
- Deterministic serialization via
to_cbor() and from_cbor(bytes).
- JSON Schema export via
schema() (for use with Verify).
- Field validation on construction.
- Forbids
set / frozenset fields (non-deterministic iteration).
- Forbids
float fields (requires SoftFloat).
from cowboy_sdk import CowboyModel, SoftFloat
class Trade(CowboyModel):
asset: str
size: int
price: SoftFloat
9.5 CBOR Codec
The SDK uses Canonical CBOR (RFC 8949 §4.2) everywhere encoding crosses a determinism boundary: storage values, message payloads, continuation state, call arguments, return values. Requirements:
- Map keys sorted by byte-lexicographic order.
- No duplicate keys.
- Floats encoded as IEEE 754 double (when unavoidable).
- Integers encoded in the shortest form.
cowboy_sdk.codec.encode(v) -> bytes and .decode(b) -> value are the canonical entry points.
10. Runtime Module
cowboy_sdk.runtime exposes the Host API. Actor code SHOULD prefer the higher-level primitives in cowboy_sdk (call, send, storage proxy); runtime is for cases where fine-grained control is needed (e.g., system actors, upgrade flows).
10.1 Context
| Function | Returns | Notes |
|---|
runtime.get_sender() | bytes (20) | Caller of the current handler |
runtime.get_actor_address() | bytes (20) | This actor’s address |
runtime.get_block_height() | int | Current block height |
runtime.get_timestamp_ms() | int | Block timestamp, milliseconds since epoch |
These are the ONLY authorized sources of time and identity. time.time(), datetime.now(), and similar are trapped.
10.2 State
Lower-level state access paralleling self.storage:
runtime.get_state(key: bytes) -> bytes | None
runtime.set_state(key: bytes, value: bytes) -> None
runtime.delete_state(key: bytes) -> None
10.3 Events
runtime.emit_event(name: str, payload: dict | bytes) -> None
Emits a log entry visible in the transaction receipt. Metered in Cells per CIP-3.
10.4 Tokens (CIP-20)
The standard CIP-20 interface is exposed as runtime operations:
runtime.token_create(name, symbol, decimals, initial_supply, max_supply) -> token_id
runtime.token_transfer(token_id, to, amount)
runtime.token_approve(token_id, spender, amount)
runtime.token_mint(token_id, to, amount)
runtime.token_burn(token_id, amount)
runtime.token_balance(token_id, account) -> int
runtime.token_allowance(token_id, owner, spender) -> int
Each requires the corresponding entitlement (token.create, token.transfer, etc.).
10.5 Timers (CIP-5)
runtime.schedule_timer(fire_at_block: int, payload: bytes) -> timer_id: bytes
runtime.cancel_timer(timer_id: bytes) -> None
At fire_at_block, the scheduler delivers payload to the actor’s on_timer handler. Requires timer.schedule entitlement.
10.6 Jobs (CIP-2)
runtime.submit_job(job_spec: dict) -> job_id: bytes
Low-level entry used by @runner.continuation; actor authors SHOULD prefer the continuation form.
10.7 Upgrades
runtime.upgrade_self(new_code: bytes, new_manifest: dict | None = None) -> None
Replaces the running actor’s code and optionally its entitlement manifest. The new manifest MUST be a subset of the current manifest (no privilege escalation); violation raises PurityViolationError at the Host boundary. Requires sys.upgrade entitlement.
10.8 Crypto
runtime.keccak256(data: bytes) -> bytes # 32-byte hash
runtime.randomness(domain: bytes) -> bytes # deterministic VRF output
randomness() produces deterministic per-block VRF output keyed by domain. This is the only authorized source of randomness; random.random() and secrets are trapped.
10.9 Metering
runtime.charge_gas(amount: int) -> None
Explicitly consumes Cycles. Useful for implementations that want to front-load gas accounting for complex operations. Most actors do not need this; metering happens automatically at syscall boundaries.
11. Verification Builder
Verify produces CIP-2 verification configurations using a fluent chain:
from cowboy_sdk import Verify, runner
await runner.llm(
prompt="Analyze market...",
response_model=MarketAnalysis,
verification=Verify.builder()
.mode("structured_match")
.runners(5)
.threshold(3)
.check(Verify.json_schema_valid(MarketAnalysis.schema()))
.check(Verify.numeric_tolerance("score", SoftFloat("0.05")))
.check(Verify.no_prompt_leak())
.build(),
)
Modes: none, economic_bond, majority_vote, structured_match, deterministic, semantic_similarity.
Built-in checkers:
| Checker | Purpose |
|---|
Verify.exact_match() | Byte-for-byte equality across runners |
Verify.json_schema_valid(schema) | Output conforms to JSON Schema |
Verify.structured_match(fields) | Named fields match across runners |
Verify.majority_vote(field) | Field value agrees on > 50% |
Verify.numeric_tolerance(field, tolerance) | Field within ±tolerance |
Verify.numeric_range(field, min, max) | Field within bounds |
Verify.set_equality(field) | Unordered set equality |
Verify.contains_all(substrings) | Output contains required strings |
Verify.contains_none(substrings) | Output excludes strings |
Verify.regex_match(pattern) | Regex match |
Verify.length_bounds(min, max) | Output length within bounds |
Verify.semantic_similarity(threshold) | Embedding cosine similarity ≥ threshold |
Verify.no_prompt_leak() | Output does not echo the system prompt |
Verify.entropy_check(min_entropy) | Output is not degenerate/repetitive |
Verify.custom(actor, method) | Custom on-chain validator actor |
The builder MUST produce a deterministic CBOR encoding: regardless of the order in which .check() is called, the resulting job spec hashes identically.
12. Entitlements
A manifest accompanies every deploy and upgrade. JSON shape:
{
"entitlements": [
{"id": "econ.hold_balance"},
{"id": "econ.transfer", "params": {"max_amount": "1000000000", "max_per_block": "10000"}},
{"id": "http.fetch", "params": {"allowlist_domains": ["api.example.com"], "max_requests": 100}},
{"id": "oracle.llm", "params": {"max_tokens": 4096, "max_requests": 50}}
]
}
Rules:
id values MUST appear in the Entitlement Registry (types/src/registry.rs).
- The
entitlements array MUST be lexicographically sorted by id; a chain MUST reject deploy transactions with an unsorted manifest.
params contents are entitlement-specific and validated at deploy time against the registry’s ParamSchema.
- An upgrade’s manifest MUST be a subset (in both ids and param-bound strength) of the prior manifest.
12.2 Runtime Enforcement
Entitlements are enforced at the Host API boundary, not in SDK Python:
- Actor invokes an SDK function.
- SDK issues the corresponding syscall.
- Host checks the actor’s manifest for the required entitlement.
- If absent: Host returns
HostError::MissingEntitlement → SDK raises the corresponding error.
- If present but quota-exceeded or param-restricted: Host returns the appropriate error.
Actor authors do not check entitlements in Python; they rely on the runtime to enforce.
A partial list of currently-registered entitlements, each with its gated syscall:
| ID | Gates | Params |
|---|
econ.hold_balance | CBY balance storage | — |
econ.transfer | runtime.transfer | max_amount, max_per_block |
exec.spawn | child actor deploy | max_children |
http.fetch | runner.http() | allowlist_domains, max_requests |
oracle.llm | runner.llm() | max_tokens, max_requests |
storage.kv | self.storage.set_raw beyond quota | max_bytes |
sys.upgrade | runtime.upgrade_self() | — |
timer.schedule | runtime.schedule_timer() | — |
token.create, token.transfer, token.mint, token.burn | corresponding runtime.token_* | varies |
bridge.asset, bridge.subscribe_event | CIP-19 bridge primitives | varies |
accel.gpu | GPU runner selection | min_vram_gb |
sec.data_residency | geofenced execution | (attested) |
CIP-2 §7 is the normative source.
13. Error Hierarchy
All SDK exceptions derive from cowboy_sdk.CowboyError. Each exception exposes:
HOST_ERROR_CODE: int — Host API error code (1–8)
ERROR_SLUG: str — short identifier, format E1xxx
.why: str — human explanation
.fix: str — suggested remediation
Categories:
| Class | Slug range | When |
|---|
CycleLimitExceeded | E1001 | Cycles budget exhausted |
CallDepthExceeded | E1002 | Call stack depth > 32 |
LoopBoundExceeded | E1003 | bounded_loop iterated past declared max |
ContinuationNotFoundError | E1101 | Resume with unknown correlation id |
ContinuationCorruptedError | E1102 | State integrity check failed |
ContinuationSizeLimitError | E1103 | Serialized state > 64 KiB |
ContinuationCountLimitError | E1104 | > 100 active continuations |
DeterminismError | E1201 | Non-deterministic operation attempted |
StateConflictError | E1202 | Guard snapshot mismatch on resume |
ReentrancyError | E1203 | @reentrancy_guard tripped |
PurityViolationError | E1204 | Async side effect from @pure handler |
CaptureTypeError | E1205 | Captured value is not CBOR-encodable |
RunnerTimeoutError | E1301 | timeout_blocks expired |
RunnerValidationError | E1302 | Runner result failed Verify chain |
ActorCallError | E1401 | Callee raised or returned invalid payload |
ActorNotFoundError | E1402 | Target address is not a deployed actor |
CodecError | E1501 | CBOR encode/decode failure |
AddressError | E1502 | Invalid Address construction |
Exceptions are CBOR-encoded into the transaction receipt; clients use ERROR_SLUG for programmatic handling.
14. CLI
The canonical CLI is cowboy, implemented in node/cli/. Current binary: v0.0.24. Commands relevant to actor development:
14.1 Project bootstrap
cowboy init <network> # network: local | dev | summit
cowboy wallet create [--output FILE]
cowboy wallet create-mnemonic
Persists config at .cowboy/config.json (RPC URL, sender key, nonce cache).
14.2 Actor lifecycle
cowboy actor new <name> # scaffold actors/<name>/main.py
cowboy actor deploy
--code <file.py>
--salt <hex>
[--manifest-json <file.json>]
[--cycles-limit N] [--cells-limit N]
cowboy actor address
--code <file.py>
--creator <addr>
--salt <hex> # compute CREATE2 address locally
cowboy actor execute
--actor <addr>
--handler <name>
[--payload <hex | @file>]
cowboy actor get --address <addr>
cowboy actor logs --address <addr>
cowboy actor fund --actor <addr> --amount <cby>
cowboy actor upgrade
--actor <addr>
--code <new.py>
[--manifest-json <new.json>] # requires sys.upgrade
14.3 Ecosystem
cowboy account balance [--address <addr>]
cowboy token create --name N --symbol S --decimals D --initial-supply X [--max-supply Y]
cowboy token transfer --token-id T --to <addr> --amount A
cowboy runner list
cowboy runner register --stake <amount>
cowboy job submit --actor <addr> --input <json>
cowboy watchtower init
cowboy watchtower new feed --name N [--description D]
cowboy watchtower feed <id> publish --data <json>
Any implementation MAY extend the CLI with additional commands. Renaming any command in this section is a breaking change and requires a CIP revision.
15. PVM Determinism Rules
Actor code MUST conform to the following rules. Violations are detected at class-load time where statically decidable; otherwise they raise DeterminismError at runtime.
| # | Rule | Replacement |
|---|
| 1 | No import time, datetime.now() | runtime.get_block_height(), runtime.get_timestamp_ms() |
| 2 | No import random, no secrets | runtime.randomness(domain) |
| 3 | No hardware float in actor state, message payloads, or CBOR | SoftFloat |
| 4 | No set() / frozenset() | ordered_set |
| 5 | No pickle | CBOR (cowboy_sdk.codec) |
| 6 | Call depth ≤ 32; every call() MUST pass cycles_limit | — |
| 7 | ≤ 8 sequential awaits per continuation | split into multiple continuations |
| 8 | await in loops requires @bounded_loop(max_iterations=N) | — |
| 9 | capture() required for locals spanning an await | — |
| 10 | send() and other async side effects prohibited from @pure handlers | mark handler @deferred |
| 11 | No filesystem, network, or subprocess access | runner.http, runtime.*, entitlement-gated |
16. Reference Implementation
Canonical implementation:
- SDK:
/node/pvm/Lib/cowboy_sdk/ (version 0.1.1)
- Host API:
/node/execution/src/pvm_host.rs
- CLI:
/node/cli/ (binary cowboy, version 0.0.24)
- Examples:
/cowboy/examples/01-tokens, /cowboy/examples/08-audio-transcription, /cowboy/examples/09-image-generation, /cowboy/examples/13-dao-copilot, /cowboy/examples/14-compliance, /cowboy/examples/18-ring-demo
A Lasso wrapper (Node.js) provides convenience tooling around the CLI but is not part of the normative surface.
17. Open Questions / Gaps
- CIP-9 volume mounts from the SDK. Today, CBFS volumes are a runner-side concern. An actor-side API for
mount(volume_id, access_mode) is not yet defined. Needed for first-class storage-attached actor workflows.
- CIP-7 / CIP-17 stream helpers.
cowboy watchtower exists at the CLI level and as a system-actor pattern, but there is no actor-side @on_stream(stream_id) decorator or stream_publish() helper. Authors currently manage this via raw call() / send().
- Checkpoint mode deprecation.
runtime.is_checkpoint_mode() exists for local development but is marked for removal once FSM mode is production-stable across all targets.
- Reentrancy semantics on
call() cycles. @reentrancy_guard is defined, but its key derivation (keccak256(method_name ‖ caller_addr)) may over-collide in contract-factory patterns. A CIP-6.1 revision may add an explicit key argument.
- Address scheme forward compatibility.
Address is fixed at 20 bytes (Ethereum-compatible). A future migration to a larger scheme would break the type; no migration path is specified here.
- Static loop-bound detection. The compiler rejects unbounded
await-in-loop at class-load, but edge cases (awaits inside comprehensions, awaits in helper functions called from loops) are under-specified. A future revision should enumerate accepted and rejected shapes.
- Entitlement manifest upgrade semantics. “Subset” is defined informally; the registry needs a
is_tighter_than operator per entitlement, currently implemented ad-hoc.