Skip to main content
This page is normative for the Nautilius TypeScript extension. Base-server tests pin the observable wire behavior; handler tests pin the product operations.
The operation identifiers remain LAND_AUCTION/{CREATE_AUCTION,SUBMIT_BID,CLEAR_AUCTION}. The public product rename does not alter this deployed wire contract.

1. Process topology

A prebuilt tee-node ./cmd/extension binary runs beside the TypeScript runtime, joined so the container dies if either child dies:
Use /bin/bash, not /bin/sh. Debian’s /bin/sh is dash, which does not implement wait -n, and without it the container will not exit when a child dies. Neither process is privileged.

2. HTTP surface the extension MUST serve

On $EXTENSION_PORT, bound on all interfaces inside the container.

POST /action

Request body is an Action (§4.1). Response is an ActionResult (§4.3) as JSON with status 200 whenever the action was routed to a handler — including when the handler fails. Handler failure is signalled by ActionResult.status, not by the HTTP status. The 501 body is a human-readable diagnostic, not a fixed string — the status code is what is contractual. The Go implementation additionally names the received and expected identifiers, which is worth imitating.

GET /state

No request body. Returns a StateResponse (§4.4) with status 200.

Routing rules


3. HTTP surface the extension MAY call

tee-node exposes a signing/crypto API on http://localhost:$SIGN_PORT. The extension calls it; it is never exposed outside the container.

POST /decrypt

Decrypts a payload that was encrypted to the TEE’s public key. Because tee-node is Go and Go marshals []byte as base64 in JSON, the wire encoding here is base64, not hex — this is the single most common porting mistake. Request:
Response:
The scaffold wraps this in each language’s base/node.* module so extension authors never hand-roll it.

4. Wire format

Every field below is derived from the Go types that tee-node actually serializes. The Go type is given because it determines the JSON encoding, and getting the encoding wrong is silent — the node accepts the request and the result fails verification later. Encoding rules for the Go types involved: bytes32 identifiers (opType, opCommand) are UTF-8 strings right-padded with zero bytes to 32 bytes, then hex-encoded. "LAND_AUCTION" becomes 12 content bytes followed by 20 zero bytes. The empty string is 32 zero bytes, and it is meaningful: see §5.

4.1 Action — request body of POST /action

Source: tee-node/pkg/types/actions.go.

4.2 ActionData — the data field

Note the double encoding on message: hex-decode it, then parse the resulting bytes as JSON.

4.3 DataFixed — decoded from ActionData.message

Source: go-flare-common/pkg/tee/instruction/instruction.go. originalMessage is what a handler receives. Its interpretation is extension-specific. Nautilius expects ECIES ciphertext at the instruction boundary; after /decrypt, the TypeScript handler strictly validates the operation-specific JSON.

4.4 ActionResult — response body of POST /action

Source: tee-node/pkg/types/actions.go.
Every field is always present. The Go struct carries no omitempty tags, so data and additionalResultStatus marshal as "0x" rather than being omitted, and log is always a string. An implementation that drops empty fields produces a different JSON shape from the reference Go image.
data must be byte-exact across implementations. ActionResult.Hash() computes keccak256(data) and that hash is signed, so serialization details matter: emit compact JSON with no whitespace, preserving field declaration order. Go’s encoding/json, Python dicts and TypeScript object literals all do this naturally; the conformance fixtures compare the resulting hex exactly.
version is a plain string, not bytes32. The Go declaration is Version string (tee-node/pkg/types/actions.go:57). Send "0.1.0", not "0x302e312e30000...". This is easy to get wrong because StateResponse.stateVersion is bytes32 (§4.5) — the two are genuinely asymmetric. Nautilius pins the official Go ActionResult.Hash() behavior in its TypeScript and Solidity golden-vector tests; removed upstream greeting fixtures are not part of its conformance claim.

4.5 StateResponse — response body of GET /state

4.6 status and log

data is only meaningful for status == 1.

5. Handler dispatch

An extension registers handlers against (opType, opCommand) pairs, both compared as bytes32. Lookup order: exact (opType, opCommand) match first; then (opType, <empty bytes32>) as a wildcard. Registering a handler with an empty opCommand makes it the default for every command under that op-type. No match at all is a 501. Concurrency: handler invocations are serialized — at most one runs at a time. GET /state is serialized against handlers too, so a state read never observes a half-applied mutation. Implementations may use a mutex (Go, Python) or a promise chain (TypeScript); what matters is the observable guarantee.

6. Container requirements

Environment variables consumed

An extension implementation itself only needs to read EXTENSION_PORT and SIGN_PORT; the rest are consumed by tee-node. All of them must still be settable on the container.

Ports

Launch policy label — required

Without this label, a GCP Confidential Space VM rejects operator env overrides at attestation time and whatever was baked into the image at build time is final. Every language image must carry an identical list — a mismatch produces a deployment that cannot be reconfigured, and the failure appears at attestation rather than at build.

User

Matches tee-node. The TEE itself is the isolation boundary, not in-container user separation.

7. Reproducibility

The container’s code hash is what gets registered on-chain, so build determinism is a security property, not a nicety. Every language image must:
  • accept a SOURCE_DATE_EPOCH build arg and propagate it,
  • pin apt to snapshot.debian.org keyed on SOURCE_DATE_EPOCH,
  • install dependencies from a committed lockfile (go.sum, package-lock.json, pinned requirements.txt),
  • normalize mtimes as the final build step: RUN find /app -exec touch -h -d @${SOURCE_DATE_EPOCH} {} +
Determinism is not equal across languages. Go on distroless is bit-for-bit reproducible across machines. Python wheels and node_modules trees embed build-host paths and npm-version-dependent layout, so those images target same-machine determinism only. See local reproducibility for the honest per-language caveats. tee-node version pinning. Non-Go images build the tee-node ./cmd/extension binary from source. The version must match the Go module pin in go/go.mod, or the node and the proxy will disagree on signature formats and surface confusing verification failures. That pin is frequently a Go pseudo-version (v0.0.21-0.20260619120252-31fc839ae6d2), whose last segment is an abbreviated commit SHA. Two obvious approaches both fail on it: git clone --branch <sha> resolves tags and branches only, and git fetch --depth 1 origin <sha> requires a full 40-char SHA plus server-side uploadpack.allowAnySHA1InWant, which GitHub rejects with couldn't find remote ref. Use a blobless partial clone, which fetches all refs cheaply and lets an abbreviated SHA resolve locally:
scripts/lib/versions.sh derives TEE_NODE_REF from go/go.mod, and scripts/check-versions.sh fails the build if the pins drift apart.

8. Verifying the implementation

  1. Preserve the framework layer (base/) wire types, dispatch and serialization.
  2. Keep operation identifiers identical to the sender contract: LAND_AUCTION/{CREATE_AUCTION,SUBMIT_BID,CLEAR_AUCTION}.
  3. Run cd typescript && npm test && npm run typecheck; no chain, registration or proxy is required for these wire/product tests.