# RFC-0001 — `mcp.json` v1.3 `CapabilityManifest` Contract

- **Status:** APPROVED 2026-04-21 (v1.3 amendment — opens the `Capability.kind` registry and lands the `system.echo` vocabulary gates G1/G2/G3 per RFC-0005).
- **Prior Status:** APPROVED 2026-04-21 (v1.2, off-by-one numeric fixes only); APPROVED 2026-04-20 (v1.1, supersedes v1.0 DRAFT).
- **Author:** 🧠 Agentic Architect
- **Sprint:** 1
- **Audience:** Cloud Agents (LLMs). Schema *is* the UX.
- **Scope:** Defines the device-published manifest, the MCP tool projection rule, the standardized error envelope, the `system.metrics` kind contract, and (as of v1.3) the `system.echo` kind contract.

---

## Changelog v1.2 → v1.3

- **v1.3 — 2026-04-21 (CTO-approved, gates G1/G2/G3):** Opens the `Capability.kind` enum to admit `system.echo` (RFC-0005). The enum remains a **closed, registered enum** governed by an **extensible registry; new entries require RFC amendment** — there is no free-form fallback. Specifically:
  - **G1 — enum addition.** `Capability.kind` now enumerates `["system.metrics", "system.echo"]`. Any future kind requires an amendment to this RFC adding a row here AND a `kind_short` row in §3.2 AND an `allOf` clamp under `$defs.Capability`.
  - **G2 — `kind_short` registry row.** §3.2 grows one row: `system.echo → sysecho`.
  - **G3 — `system.echo` clamp.** A new `allOf` branch under `$defs.Capability` clamps `kind = "system.echo"` to `safety_class = "read_only"`, `verbs = ["invoke"]`, `constraints.deadline_ms_default ≤ 5000`, and `constraints.rate_limit_rps ≤ 50`. The numeric ceilings are consistent with RFC-0005 §7 (5 s gateway-end-to-end budget) and RFC-0005 §9 (per-mock-device manifest entry of `rate_limit_rps: 10`, `deadline_ms_default: 2000`); the clamp deliberately leaves headroom above the §9 nominal so individual devices may tune within RFC-0005 limits without re-amending RFC-0001.
  No other normative changes; the projection rule, error envelope, `system.metrics` contract, and tool-name budget arithmetic are unchanged.

## Changelog v1.1 → v1.2

- **v1.2 — 2026-04-21:** Tool-name budget table corrected (off-by-one fixes in §3.1 worst-case Sprint-1 example and §5.5 example values). No schema changes; no behavioural changes. The `system.metrics.subscribe` worst-case projected name is exactly **51** chars (was incorrectly stated as 50); `snapshot` is exactly **50** chars (was 49). The 64-char hard ceiling and the §3.1 budget arithmetic (3 dots + 26 ULID + 9-char `subscribe` = 38 fixed; 26-char remainder split 8/18 between `kind_short`/`cap_id`) are unchanged and remain exactly satisfied.

## Changelog v1.0 → v1.1

All eight v1.0 Open Questions resolved by CTO on 2026-04-20 with "use your team's recommended defaults." Locked decisions and where they bake in:

| # | Decision | Justification | Schema/section impact |
|---|----------|---------------|-----------------------|
| 1 | **Signing key lifecycle:** TEST root CA (managed by 🛡️ devex-protocol-sec under `tracking/work/devex-protocol-sec/test-ca/`) issues per-node Ed25519 leaf certs; `kid` = SHA-256 leaf cert thumbprint (hex, 64 chars). | Chains to a tenant-rotatable CA without per-node key ceremony; thumbprint is deterministic, matches existing `^[0-9a-f]{64}$` pattern, and lets the broker resolve `kid → cert → tenant` in one lookup. Test CA scope keeps Sprint 1 unblocked while RFC-0003 specifies prod issuance. | §2 `node_attestation.kid` description tightened. |
| 2 | **Discovery via retained `$devices/{node_id}/announce` only.** Direct HTTPS GET on the node is REMOVED for Sprint 1. | Nodes are NEVER required to be cloud-ingressable; eliminates inbound-firewall / NAT / dyndns concerns and a whole class of node-side attack surface. The retained MQTT message gives late-joining cloud agents the same discovery semantics as a polled URL. | §1 rewritten; §1's `GET /.well-known/mcp.json` reframed as the on-node *self-publish source*, not a cloud-reachable endpoint. |
| 3 | **`node_id_short` REMOVED — projection uses the full 26-char ULID.** CTO directive: "full chars for easy read." | Eliminates registry-side `_2/_3` collision suffixing entirely (no more silent rename surprises for agents). Tool names remain tokenizer-stable. Cost: tightens the `kind_short` and `cap_id` budgets — see new §3.1 *Tool name budget*. | §3 projection table; §3.1 added; §5.5 example updated. |
| 4 | **`system.metrics.subscribe` `safety_class = read_only`** for Sprint 1. A `streaming` safety *dimension* (orthogonal to `safety_class`) is RESERVED for Sprint N+ to account for fanout cost (likely Flink-style broker-side multiplexing). | Subscribe reads no extra capability beyond `snapshot` from the node's perspective; fanout cost is a *broker* concern, not a *device safety* concern, so it belongs on a separate axis. | §2 `allOf` clamp on `system.metrics` retained; §5.1 note added. |
| 5 | **Schema registry served by the Cloudflare Worker** at `GET /schemas/{kind}@{semver}` (content-addressable, immutable). Agents pin via `schema_ref`. | Centralizes schema distribution, lets the Worker cache aggressively at the edge, and means nodes do NOT have to ship the schema bytes — they ship only a `mcp://schemas/...@x.y.z` reference. Resolution is a pure GET; no auth required for read (schemas are public contracts). | §2 `schema_ref` description; §8 Handoff names `☁️ cloudflare-native-edge` for the Worker route. |
| 6 | **`expires_at_ms` REQUIRED, max TTL 24h, refreshed on heartbeat.** | Forces periodic re-attestation so a stolen-then-replayed manifest goes stale within a day. Refresh-on-heartbeat means a healthy node never serves an expired manifest; an unhealthy/silent node fails closed at the broker. | §2: `expires_at_ms` added to `required`; new constraint `expires_at_ms - issued_at_ms ≤ 86_400_000`. |
| 7 | **Error `message` ASCII-only,** regex `^[\x20-\x7E]*$`. | Log pipelines, terminal dumps, and many LLM tokenizers misbehave on stray UTF-8 (esp. RTL, zero-width, homoglyph). LLMs do not need non-ASCII for `message`; localization belongs in a future `message_i18n` field if ever needed. | §4 error schema `message.pattern`. |
| 8 | **Sysinfo source: `sysinfo` Rust crate field set is canonical;** raw `/proc` parsing is FALLBACK only (when the crate yields `None` for a field on a given platform). | Cross-platform consistency (the crate already abstracts Linux/macOS/BSD), avoids ad-hoc `/proc` parsing bugs, and the §5.4 `MetricsSample` shape was authored against this crate's API. Fallback path documented for `🦀 edge-kubelet-engineer`. | §5.4 note added; field set unchanged. |

---

## 1. Discovery (v1.1)

Discovery is **MQTT-only**. Nodes are NEVER required to be ingressable from the cloud.

- The node MUST publish its current `CapabilityManifest` (§2) as a **retained** message on `$devices/{node_id}/announce` (RFC-0002 §1, §2).
- The on-node kubelet MAY also expose the same JSON locally at `GET http://127.0.0.1:{port}/.well-known/mcp.json` for operator debugging — this loopback endpoint is OUT OF SCOPE for cloud discovery and MUST NOT be assumed reachable.
- Cloud agents discover nodes by subscribing to `$devices/+/announce` (subject to RBAC per RFC-0002 §4).
- The retained payload's content-hash (RFC 8785 JCS → blake3-256) functions as the ETag and is reported in `pubsub.heartbeat.manifest_etag` (RFC-0002 §3).
- Manifest freshness is enforced via `expires_at_ms` (§2, locked decision #6) — agents MUST treat expired manifests as `E_MANIFEST_INVALID`.

---

## 2. `CapabilityManifest` JSON Schema (Draft 2020-12)

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "mcp://schemas/manifest@1.0.0",
  "title": "CapabilityManifest",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "manifest_version",
    "node_id",
    "hw_fingerprint",
    "node_attestation",
    "issued_at_ms",
    "expires_at_ms",
    "capabilities"
  ],
  "properties": {
    "manifest_version": {
      "type": "string",
      "const": "1.1.0"
    },
    "node_id": {
      "type": "string",
      "description": "Stable node identifier. ULID lowercased.",
      "pattern": "^[0-9a-hjkmnp-tv-z]{26}$"
    },
    "hw_fingerprint": {
      "type": "object",
      "additionalProperties": false,
      "required": ["algo", "value", "sources"],
      "properties": {
        "algo": { "type": "string", "enum": ["blake3-256"] },
        "value": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
        "sources": {
          "type": "array",
          "minItems": 1,
          "uniqueItems": true,
          "items": {
            "type": "string",
            "enum": [
              "cpu_serial",
              "soc_uid",
              "machine_id",
              "tpm_ek_pub",
              "mac_primary"
            ]
          }
        }
      }
    },
    "node_attestation": {
      "type": "object",
      "additionalProperties": false,
      "required": ["alg", "kid", "sig", "payload_hash"],
      "properties": {
        "alg": { "type": "string", "enum": ["Ed25519"] },
        "kid": {
          "type": "string",
          "description": "SHA-256 thumbprint (hex, lowercase) of the per-node Ed25519 LEAF certificate issued by the test root CA at tracking/work/devex-protocol-sec/test-ca/. Broker resolves kid → leaf cert → tenant_id.",
          "pattern": "^[0-9a-f]{64}$"
        },
        "sig": {
          "type": "string",
          "description": "Base64url Ed25519 signature over JCS-canonicalized manifest with node_attestation.sig=\"\".",
          "pattern": "^[A-Za-z0-9_-]{86}$"
        },
        "payload_hash": {
          "type": "string",
          "description": "blake3-256 hex of the canonicalized payload that was signed.",
          "pattern": "^[0-9a-f]{64}$"
        }
      }
    },
    "issued_at_ms": {
      "type": "integer",
      "minimum": 1700000000000
    },
    "expires_at_ms": {
      "type": "integer",
      "minimum": 1700000000000,
      "description": "REQUIRED (locked decision #6). MUST satisfy 0 < (expires_at_ms - issued_at_ms) <= 86400000 (24h). Refreshed on every heartbeat (RFC-0002 §3)."
    },
    "capabilities": {
      "type": "array",
      "minItems": 1,
      "maxItems": 256,
      "items": { "$ref": "#/$defs/Capability" }
    }
  },
  "$defs": {
    "Capability": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "cap_id",
        "kind",
        "schema_ref",
        "verbs",
        "safety_class",
        "constraints"
      ],
      "properties": {
        "cap_id": {
          "type": "string",
          "description": "Node-local capability id; tokenizer-stable. Max 18 chars to fit the 64-char tool-name budget when projected with the full 26-char ULID node_id (see §3.1).",
          "pattern": "^[a-z][a-z0-9_]{0,17}$"
        },
        "kind": {
          "type": "string",
          "description": "Closed, registered enum (extensible registry; new entries require RFC amendment). v1.3 admits system.metrics and system.echo. There is no free-form fallback; unknown values are rejected at the manifest boundary with E_KIND_UNSUPPORTED.",
          "enum": ["system.metrics", "system.echo"]
        },
        "schema_ref": {
          "type": "string",
          "description": "Content-addressable schema reference. Resolved by the Cloudflare Worker schema registry at GET /schemas/{kind}@{semver} (locked decision #5). Immutable per (kind, semver).",
          "pattern": "^mcp://schemas/[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*@\\d+\\.\\d+\\.\\d+$"
        },
        "verbs": {
          "type": "array",
          "minItems": 1,
          "uniqueItems": true,
          "items": {
            "type": "string",
            "enum": ["snapshot", "subscribe", "get", "set", "invoke", "stream"]
          }
        },
        "safety_class": {
          "type": "string",
          "description": "Sprint 1 enum FROZEN. The orthogonal `streaming` cost-dimension (locked decision #4) is reserved for a future minor version and will NOT be added as a fourth enum value here.",
          "enum": ["read_only", "reversible", "physical_actuation"]
        },
        "constraints": {
          "type": "object",
          "additionalProperties": false,
          "required": ["rate_limit_rps"],
          "properties": {
            "rate_limit_rps": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 1000
            },
            "max_concurrency": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "deadline_ms_default": {
              "type": "integer",
              "minimum": 50,
              "maximum": 30000,
              "default": 2000
            }
          }
        }
      },
      "allOf": [
        {
          "if": { "properties": { "kind": { "const": "system.metrics" } } },
          "then": {
            "properties": {
              "safety_class": { "const": "read_only" },
              "verbs": {
                "items": { "enum": ["snapshot", "subscribe"] }
              }
            }
          }
        },
        {
          "if": { "properties": { "kind": { "const": "system.echo" } } },
          "then": {
            "properties": {
              "safety_class": { "const": "read_only" },
              "verbs": {
                "items": { "enum": ["invoke"] }
              },
              "constraints": {
                "properties": {
                  "deadline_ms_default": { "maximum": 5000 },
                  "rate_limit_rps":      { "maximum": 50   }
                }
              }
            }
          }
        }
      ]
    }
  }
}
```

---

## 3. MCP Tool Projection Rule (v1.1)

Each `Capability × verb` pair projects to exactly one MCP tool name:

```
{kind_short}.{node_id}.{cap_id}.{verb}
```

| Token         | Derivation                                                          | Constraint                          |
|---------------|---------------------------------------------------------------------|-------------------------------------|
| `kind_short`  | Per-kind static mapping (see §3.2). NOT derived from `kind` string. | `^[a-z][a-z0-9_]{0,7}$` (max 8)     |
| `node_id`     | Manifest `node_id` **VERBATIM** — full 26-char ULID (locked decision #3). No truncation, no registry suffixing. | `^[0-9a-hjkmnp-tv-z]{26}$` |
| `cap_id`      | Manifest `cap_id`                                                   | `^[a-z][a-z0-9_]{0,17}$` (max 18)   |
| `verb`        | Manifest `verbs[i]`                                                 | enum (see schema)                   |
| **full name** | dot-joined                                                          | total length ≤ **64 chars**, `^[a-z0-9_.]+$` |

**Tool `description` field:** rendered server-side from a static template keyed off `kind` + `verb`. Device-supplied strings are NEVER interpolated into descriptions (USB descriptor / EDID injection mitigation).

**`safety_class` propagation:** the projected tool's MCP annotation MUST include `x-safety-class` mirroring the capability; downstream RBAC and the two-phase-commit gate read this field, not the description.

### 3.1 Tool name budget (worst-case length analysis)

Fixed costs:
- 26 chars for full ULID `node_id` (locked decision #3).
- 3 separator dots.
- 9 chars for the longest Sprint-1 verb (`subscribe`).
- **Subtotal fixed: 38 chars.**

Remaining budget: `64 − 38 = 26 chars`, split between `kind_short` and `cap_id`:
- `kind_short` ≤ 8 chars (regex `^[a-z][a-z0-9_]{0,7}$`).
- `cap_id` ≤ 18 chars (regex `^[a-z][a-z0-9_]{0,17}$`, schema-enforced).
- 8 + 18 = 26 → exact fit; budget is tight by design to deter sprawl.

**Worst-case projected name (Sprint 1):**
```
sys.01hzx9k3m4p7q8r9s0t1v2w3xy.sysmetrics.subscribe   (51 chars)
```
(3 + 1 + 26 + 1 + 10 + 1 + 9 = 51.)

**Worst-case projection at the schema limit (any future kind):**
```
abcdefgh.01hzx9k3m4p7q8r9s0t1v2w3xy.abcdefghijklmnopqr.subscribe   (64 chars) ✓
```

If a future kind needs `kind_short` > 8 or `cap_id` > 18 chars, the projection rule MUST be revised in a new RFC — the schema's `pattern` constraints will reject it at the manifest boundary before any tool name is ever computed.

### 3.2 `kind_short` registry (Sprint 1)

| `kind`           | `kind_short` |
|------------------|--------------|
| `system.metrics` | `sys`        |
| `system.echo`    | `sysecho`    |

New kinds MUST add a row here in the same RFC that introduces the `kind`. The registry is extensible; every new entry requires an RFC amendment that lands the row, the `Capability.kind` enum addition (§2), and a matching `allOf` clamp.

---

## 4. Standardized Error Envelope (per SOP #2)

All tool failures (manifest fetch, projection, invocation) MUST return:

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "mcp://schemas/error@1.0.0",
  "type": "object",
  "additionalProperties": false,
  "required": ["code", "message", "suggested_fix"],
  "properties": {
    "code": {
      "type": "string",
      "enum": [
        "E_MANIFEST_NOT_FOUND",
        "E_MANIFEST_INVALID",
        "E_ATTESTATION_FAILED",
        "E_KIND_UNSUPPORTED",
        "E_VERB_UNSUPPORTED",
        "E_RATE_LIMITED",
        "E_DEADLINE_EXCEEDED",
        "E_NODE_OFFLINE",
        "E_SAFETY_DENIED",
        "E_INTERNAL"
      ]
    },
    "message": {
      "type": "string",
      "maxLength": 512,
      "pattern": "^[\\x20-\\x7E]*$",
      "description": "Human-readable, but consumed by LLM. ASCII printable only (locked decision #7). No PII, no device-supplied strings."
    },
    "suggested_fix": {
      "type": "string",
      "maxLength": 512,
      "pattern": "^[\\x20-\\x7E]*$",
      "description": "Actionable next step the calling agent can take (e.g. 'retry after 1500ms', 'request fresh manifest', 'escalate to operator'). ASCII printable only."
    },
    "retry_after_ms": {
      "type": "integer",
      "minimum": 0
    },
    "correlation_id": {
      "type": "string",
      "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
    }
  }
}
```

---

## 5. `system.metrics` Kind Contract (Sprint 1)

`schema_ref`: `mcp://schemas/system.metrics@1.0.0`

### 5.1 Verbs

| Verb        | Safety        | Semantics                                                          |
|-------------|---------------|--------------------------------------------------------------------|
| `snapshot`  | `read_only`   | Returns one `MetricsSample` synchronously.                         |
| `subscribe` | `read_only`   | Cloud-side stream; broker-fanned via `$devices/{node_id}/telemetry` (see RFC-0002). Input declares `interval_ms`. |

**Note on `subscribe.safety_class` (locked decision #4):** Sprint 1 fixes this to `read_only`. Fanout-cost accounting will be expressed via a future orthogonal `streaming` dimension (Sprint N+, candidate design: Flink-style broker-side multiplexing with per-tenant fanout budget). The `safety_class` enum stays at three values.

### 5.2 `snapshot` — Input

```json
{
  "$id": "mcp://schemas/system.metrics.snapshot.input@1.0.0",
  "type": "object",
  "additionalProperties": false,
  "required": [],
  "properties": {
    "include": {
      "type": "array",
      "uniqueItems": true,
      "items": {
        "type": "string",
        "enum": ["cpu", "mem", "load", "uptime", "disk"]
      },
      "default": ["cpu", "mem", "load", "uptime", "disk"]
    }
  }
}
```

### 5.3 `subscribe` — Input

```json
{
  "$id": "mcp://schemas/system.metrics.subscribe.input@1.0.0",
  "type": "object",
  "additionalProperties": false,
  "required": ["interval_ms"],
  "properties": {
    "interval_ms": {
      "type": "integer",
      "minimum": 1000,
      "maximum": 60000
    },
    "include": {
      "type": "array",
      "uniqueItems": true,
      "items": {
        "type": "string",
        "enum": ["cpu", "mem", "load", "uptime", "disk"]
      },
      "default": ["cpu", "mem", "load", "uptime", "disk"]
    }
  }
}
```

### 5.4 Output — `MetricsSample` (shared by both verbs)

**Source-of-truth (locked decision #8):** the Rust [`sysinfo`](https://crates.io/crates/sysinfo) crate's field set is canonical for all numeric values below. Raw `/proc/{stat,meminfo,loadavg,mounts}` parsing is permitted ONLY as a fallback when the crate yields `None` for a field on a given platform; fallback paths MUST produce identical JSON shape and units (bytes for memory/disk, integer seconds for `uptime_s`, percent 0–100 for `usage_pct`).


```json
{
  "$id": "mcp://schemas/system.metrics.sample@1.0.0",
  "type": "object",
  "additionalProperties": false,
  "required": ["ts_ms", "node_id", "uptime_s"],
  "properties": {
    "ts_ms": { "type": "integer", "minimum": 1700000000000 },
    "node_id": { "type": "string", "pattern": "^[0-9a-hjkmnp-tv-z]{26}$" },
    "uptime_s": { "type": "integer", "minimum": 0 },
    "cpu": {
      "type": "object",
      "additionalProperties": false,
      "required": ["cores", "usage_pct"],
      "properties": {
        "cores": { "type": "integer", "minimum": 1, "maximum": 4096 },
        "usage_pct": { "type": "number", "minimum": 0, "maximum": 100 },
        "per_core_pct": {
          "type": "array",
          "items": { "type": "number", "minimum": 0, "maximum": 100 },
          "maxItems": 4096
        }
      }
    },
    "mem": {
      "type": "object",
      "additionalProperties": false,
      "required": ["total_bytes", "available_bytes"],
      "properties": {
        "total_bytes":     { "type": "integer", "minimum": 0 },
        "available_bytes": { "type": "integer", "minimum": 0 },
        "used_bytes":      { "type": "integer", "minimum": 0 },
        "swap_total_bytes":{ "type": "integer", "minimum": 0 },
        "swap_used_bytes": { "type": "integer", "minimum": 0 }
      }
    },
    "load": {
      "type": "object",
      "additionalProperties": false,
      "required": ["one", "five", "fifteen"],
      "properties": {
        "one":     { "type": "number", "minimum": 0 },
        "five":    { "type": "number", "minimum": 0 },
        "fifteen": { "type": "number", "minimum": 0 }
      }
    },
    "disk": {
      "type": "array",
      "maxItems": 64,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["mount", "fs_type", "total_bytes", "available_bytes"],
        "properties": {
          "mount":           { "type": "string", "maxLength": 256 },
          "fs_type":         { "type": "string", "maxLength": 32  },
          "total_bytes":     { "type": "integer", "minimum": 0 },
          "available_bytes": { "type": "integer", "minimum": 0 }
        }
      }
    }
  }
}
```

### 5.5 Projected Tool Names (example, v1.1)

For `node_id = 01hzx9k3m4p7q8r9s0t1v2w3xy`, `cap_id = sysmetrics`, with `kind_short(system.metrics) = sys`:

- `sys.01hzx9k3m4p7q8r9s0t1v2w3xy.sysmetrics.snapshot`  (50 chars)  ; 3+1+26+1+10+1+8
- `sys.01hzx9k3m4p7q8r9s0t1v2w3xy.sysmetrics.subscribe` (51 chars)  ; 3+1+26+1+10+1+9

Both ≤ 64 chars, snake/dot only, tokenizer-stable, full ULID preserved for human readability.

---

## 6. "Why this shape" — Justification Table

| Field / Decision                                   | Why                                                                                                                  |
|----------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| `additionalProperties: false` everywhere           | LLMs hallucinate fields. Reject at boundary, surface `E_MANIFEST_INVALID` with `suggested_fix`.                      |
| `kind` as closed enum (single value Sprint 1)      | Forces explicit RFC + version bump to add a kind. Prevents silent capability sprawl.                                 |
| `schema_ref` content-addressable + semver pinned   | Older agents keep working; new agents opt-in by referencing newer `@x.y.z`. No silent breakage.                      |
| `manifest_version: const "1.0.0"`                  | Document version separate from per-kind schema version. Lets us evolve the *envelope* independently of *kinds*.      |
| `node_id` = ULID lowercased                        | Sortable, fixed-width, tokenizer-stable, no hyphens (unlike UUID), Crockford alphabet avoids `0/O 1/I/L` confusion.  |
| `hw_fingerprint.sources` enum + `algo: blake3-256` | Pins the entropy sources used; prevents devices declaring fingerprints over user-controlled inputs.                  |
| `node_attestation` Ed25519 + JCS canonicalization  | Deterministic signing; no JSON-stringify ambiguity attacks. Ed25519 is small enough for constrained nodes.           |
| `safety_class` enum (3 values)                     | Drives RBAC and two-phase-commit. Sprint 1 only `read_only` is reachable; gates physical kinds at the type level.    |
| `constraints.rate_limit_rps` mandatory             | LLM agents will stampede. Self-declared ceiling enforced by broker; prevents node DoS.                               |
| Tool name ≤ 64 chars, snake/dot only               | Fits within MCP client tool-name limits and stays tokenizer-stable across BPE/SentencePiece.                         |
| `node_id_short` = first 10 chars + registry suffix | Keeps tool names short while preserving uniqueness; collision handling lives in the registry, not the manifest.       |
| Server-rendered tool `description`                 | USB / EDID / I²C descriptor injection is a real attack surface. Device strings never reach the LLM context.          |
| Error envelope `{code, message, suggested_fix}`    | Per SOP #2; `suggested_fix` lets the calling agent self-recover without escalating to operator.                      |
| `metrics.subscribe` input requires `interval_ms`   | No defaulted streams — agent must declare cadence so broker can budget fanout.                                       |
| `MetricsSample.cpu.usage_pct` 0–100 (not 0–1)      | Matches `/proc/stat`-derived sysinfo conventions; one less unit-conversion bug surface.                              |
| `disk` is an array, not a map                      | JSON Schema validates arrays of objects more cleanly than dynamic-keyed maps; preserves mount ordering.              |

---

## 7. Resolved — see Changelog

All v1.0 Open Questions were resolved by the CTO on 2026-04-20. See the **Changelog v1.0 → v1.1** section at the top of this document for each decision, its justification, and where it is baked into the schema.

---

## 8. Handoff

- **Status:** APPROVED. Implementation cleared.
- **Next persona:** `🦀 edge-kubelet-engineer` — produce the manifest emitter + `system.metrics` provider design doc that *consumes* this contract. MUST cite `mcp://schemas/manifest@1.1.0` and `mcp://schemas/system.metrics@1.0.0` verbatim, source sysinfo via the `sysinfo` crate (decision #8), and use the leaf-cert thumbprint from `tracking/work/devex-protocol-sec/test-ca/` as `kid` (decision #1).
- **Parallel persona — `☁️ cloudflare-native-edge`:** ship (a) the broker ACL + manifest-mirror per RFC-0002, and (b) the schema registry route `GET /schemas/{kind}@{semver}` (decision #5).
- **Parallel persona — `🛡️ devex-protocol-sec`:** stand up the test root CA under `tracking/work/devex-protocol-sec/test-ca/` and document the leaf-cert issuance procedure (decision #1). Coordinate with RFC-0003 (RBAC/token format) for the production issuance path.

## v2 Cross-Reference (additive 2026-05-03)

RFC-0012, RFC-0013, RFC-0014 (v1.0, 2026-05-03): supersede portions of this RFC. v3.0 amendment scheduled end-of-sprint-3-extended (2026-05-30).
