{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://netshow.ai/schema/nsv-agent-1.schema.json",
  "title": "NetShow Alive — Agent Definition (nsv-agent-1), spec 1.0 and 1.1",
  "description": "SPEC 1.1 CANDIDATE (lane staged-patches/opus5-format-bump1-20260805, O25). This file accepts BOTH minors and says which rules belong to which.\n\nWHAT 1.1 CHANGED, and only this:\n  · THE BODY PIN IS NATIVE (OWED-J1, decided). ext.join.body.packageSha256 / .fileSha256 / .name become avatar.ref.integrity.payload / .file and avatar.name. The two pins, their laws, and their refusal strings are unchanged; one indirection is gone. `source` and `type` are not carried at all in 1.1 — the gate DERIVED both in 1.0 anyway (:181, :182), so storing them was storing a second copy of a fact that already had to agree.\n  · ext.join IS ABSORBED WHOLE. contract -> `spec`; body -> avatar (above); persona -> mind.persona; integrity -> a top-level `integrity` block. `ext` returns to its drafted meaning: provider-namespaced entries a consumer MUST ignore. A first-party, load-bearing block living in ignore-by-contract space was the whole of OWED-J1.\n  · `core` — a RESERVED EXTENSION POINT, first-party, seat-allocated, seal-covered, and closed to unreserved names. See $defs.core.\n\nA 1.0 document is held to exactly the rules it was published under; nothing about it is retro-validated. The two profiles are selected by `spec` in the root allOf, and a MAJOR-1 minor this file does not know is refused BY NAME (join-bad-spec-minor) rather than loaded silently under 1.0 rules, which is what the shipped 1.0 gate did.\n\nTHE CONTRACT for the portable agent definition: one JSON document describing a NetShow Alive agent — who she is, which body she wears, which mind thinks for her, which voice speaks, and every capability as an ON/OFF/DEFAULT pick.\n\nLINEAGE — THIS FILE SUPERSEDES BY EXTENSION, NOT BY REPLACEMENT.\nA staged draft already existed and was already machine-run once:\n  · opus5-agent-builder-20260803/schema/agent-schema-v1.json   md5 d3abfb74ea06d80506ebfc86793820a6\n  · opus5-parity-close-20260803/PRE/agent-schema-v1.json       md5 94bd3d4a8938a4f3d0f9cc8f46586eef  (ancestor)\n  · opus5-parity-close-20260803/POST/agent-schema-v1.json      md5 d3abfb74ea06d80506ebfc86793820a6  (== the agent-builder copy)\nPRE -> POST added the $defs.noHostPath guard; POST was written back into the agent-builder lane, which is why two of the three copies are byte-identical. POST is therefore the newest and the authority, and it is NOT deprecated by this file — it is CARRIED FORWARD. It was verified, not assumed: re-run 2026-08-05 against the 31-vector parity fixture (gate-vectors-v1.json), the four preset joined documents and the worked example — 31/31 vectors correct, 5/5 documents accepted, ZERO mismatches. Its rules are sound and are reproduced here.\n  The AGENT-JOIN intake machine-ran it once against a single emitted document (s45-intake-f-20260804/logs-schema-crosscheck.log: 'CROSS-CHECK: PASS'). That run also recorded the gap in one line — `ext typing: {\"type\": \"object\", ...}`.\n\nTHAT GAP IS WHAT THIS FILE CLOSES. In the draft, `ext` is an OPEN object with no subschema. So a joined document whose ext.join is complete nonsense — wrong contract string, missing body pins, a persona of the wrong shape — validates against the draft exactly as cleanly as a correct one. The draft was never wrong; it was SILENT. nsv-agent-join.mjs shipped afterwards and made ext.join load-bearing. This file models it.\n\nTWO PROFILES, ONE FILE, SELECTED BY THE DOCUMENT ITSELF:\n  · NO ext.join present  -> the BUILDER profile. The draft's rules, carried forward verbatim in substance. This is what the Agent Builder (P1) emits and what the 31 parity vectors exercise.\n  · ext.join present     -> the BUILDER profile PLUS the JOINED profile ($defs.joinedProfile), which is nsv-agent-join.mjs validateJoin() expressed as schema. This is what emitJoin() emits and what the four preset agents carry.\nThe switch is `if: {required:['ext'], properties:{ext:{required:['join']}}}` in allOf below. A document is never held to join rules it does not opt into by carrying a join block.\n\nDERIVED FROM THE IMPLEMENTATION. The joined half is traceable line-by-line to staged-patches/fable5-agentjoin1-20260804/nsv-agent-join.mjs — THE structure gate, THE seal and THE load-time body re-check, consumed by both the browser page and the suite so there is one canon and zero drift. Its 29 stable refusal reason strings are mapped field-by-field in $defs.refusalReasonIndex.\n\nWHAT THIS SCHEMA CANNOT EXPRESS — enforced by nsv-validate.mjs, never by JSON Schema:\n  1. THE SEAL. ext.join.integrity.doc must equal sha-256(canonJson(document-with-integrity-removed)). Shape is checkable here; value needs a recompute. verifySeal() :234-242.\n  2. THE BODY PINS. ext.join.body.packageSha256 must equal the sealed body's OWN payload hash, and (ref mode) fileSha256 must equal the sha-256 of the package file's exact bytes as fetched. These are CROSS-FILE facts — the body lives at another URL. verifyBody() :248-277. This is the security property of the whole format: it is what stops a valid mind being re-pointed at an attacker's body, and no schema can carry it.\n  3. CANONICALISATION. canonJson :53-73 mirrors alive-package.js byte-for-byte — sorted keys, no whitespace, undefined skipped, non-finite throws. Reimplement it exactly or every seal will disagree.\n\nSTRICTER-THAN-THE-GATE POINTS, NAMED HONESTLY. Three rules below are inherited from the proven draft and are NOT enforced by validateJoin(). They are kept because they are correct producer discipline and because they cost nothing on the real corpus — but a document can be GATE-VALID and fail them, so nsv-validate.mjs reports that case as SCHEMA-STRICTER-THAN-GATE and not as a bare failure. They are: mind.additionalProperties:false (validateJoin accepts unknown keys inside mind — confirmed by intake probe X4b); identity.additionalProperties:false; and the noHostPath guards on provenance strings. See $defs.divergences.",
  "type": "object",
  "required": [
    "fmt",
    "spec",
    "uid",
    "identity",
    "avatar"
  ],
  "$comment": "The five the gate refuses without. validateJoin: fmt :111 · spec :112 · uid :116 · identity :120-122 · avatar :132. Identical to the draft's required list — the two implementations agreed without coordination, which is the strongest evidence available that this set is right. NOT required (the gate never looks for them): product, kind, createdAt, updatedAt, generator, mind, voice, capabilities, embed, integration, provenance, ext.",
  "additionalProperties": false,
  "$comment#top": "GATE-ENFORCED, unusually. validateJoin :113-115 walks every top-level key and returns 'join-unknown-key' for anything outside AGENT_TOP_KEYS (:36-37) — a CLOSED whitelist of exactly the seventeen properties below, in this order. This is stricter than alive-package-1, which silently drops unknown top-level keys. The draft independently arrived at the same seventeen. Do not add a property here without adding it to AGENT_TOP_KEYS in the same change.",
  "properties": {
    "fmt": {
      "const": "nsv-agent-1",
      "description": "Format discriminator. validateJoin :111 — 'join-bad-fmt'. Declared AGENT_FMT :28. A consumer that does not recognise it MUST reject whole rather than guess (the alive-package-1 precedent)."
    },
    "spec": {
      "type": "string",
      "pattern": "^1(\\.(0|1)(\\..*)?)?$",
      "description": "Spec version. MAJOR must be 1; the MINOR selects the profile. nsv-agent-join.mjs specMinor() returns 0 for \"1\", \"1.0\", \"1.0.0\" (the shipped 1.0 gate accepted all three, and this file still reproduces it exactly), 1 for \"1.1\", and -2 for any other MAJOR-1 minor -> refusal join-bad-spec-minor.\n\nTIGHTENED FROM THE 1.0 FILE, and this is the one place this candidate is stricter than its predecessor on purpose. The 1.0 pattern ^1(\\..*)?$ accepted \"1.2\"; the v1.1 gate refuses it by name. A schema that accepts what the gate refuses is a FALSE PASS, which nsv-validate.mjs classifies as GATE-STRICTER-THAN-SCHEMA and reports as a hard failure. Producers should emit MAJOR.MINOR: $defs.producerProfile pins the strict form."
    },
    "product": {
      "const": "NetShow Alive",
      "description": "Branding self-description. NOT CHECKED by validateJoin at all — it is inside AGENT_TOP_KEYS so its PRESENCE is legal, but no line reads its value. `const` states the only legal value for a producer (emitJoin :312 hard-codes it); it is not a gate rule. Never an authorization claim."
    },
    "kind": {
      "const": "nsv-agent",
      "description": "Document kind. Same status as product: unread by validateJoin, hard-coded by emitJoin :313. Producer truth, not gate truth."
    },
    "uid": {
      "type": "string",
      "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
      "maxLength": 80,
      "description": "Stable identity, kebab-case. validateJoin :116 applies `/^[a-z0-9]+(?:-[a-z0-9]+)*$/` — 'join-bad-uid'. Lowercase alphanumeric segments joined by single hyphens; no leading/trailing/doubled hyphen, no underscore, no uppercase, no dot, never empty.\n\nmaxLength 80 is the DRAFT's addition and is NOT gate-enforced (validateJoin caps nothing here). Kept as producer discipline; a longer uid is gate-valid, so nsv-validate.mjs reports it as SCHEMA-STRICTER-THAN-GATE. This is the value share.agentUid carries and the key an embed references."
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "description": "Unread by validateJoin. emitJoin :315 REQUIRES the caller to supply it and refuses to read a clock itself (:288) — determinism: same inputs -> byte-identical document -> identical seal. `format` is annotation-only in draft 2020-12 unless a validator opts in; nsv-validate.mjs does assert it, because a producer that writes a non-date here has a bug even though the gate cannot see it."
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time",
      "description": "Unread by validateJoin. emitJoin :316 sets it equal to createdAt at emit time."
    },
    "generator": {
      "type": "object",
      "additionalProperties": false,
      "description": "Provenance of the emitting surface — informational, never an authorization claim. Unread by validateJoin (its PRESENCE is legal via AGENT_TOP_KEYS; its CONTENT is never inspected). emitJoin :317 emits {surface:'nsv-agent-join', version:'1', host:''}.",
      "properties": {
        "surface": {
          "type": "string",
          "maxLength": 60
        },
        "version": {
          "type": "string",
          "maxLength": 20
        },
        "host": {
          "type": "string",
          "maxLength": 120,
          "$ref": "#/$defs/noHostPath",
          "description": "MUST NOT carry a filesystem path, port, username or credential — the AGENTS-ENDPOINT §3.3.2 rule applied to every field a page may print verbatim."
        }
      }
    },
    "identity": {
      "$ref": "#/$defs/identity"
    },
    "avatar": {
      "$ref": "#/$defs/avatar"
    },
    "mind": {
      "$ref": "#/$defs/mind"
    },
    "voice": {
      "$ref": "#/$defs/voice"
    },
    "capabilities": {
      "$ref": "#/$defs/capabilities"
    },
    "embed": {
      "$ref": "#/$defs/embed"
    },
    "integration": {
      "$ref": "#/$defs/integration"
    },
    "provenance": {
      "$ref": "#/$defs/provenance"
    },
    "ext": {
      "type": "object",
      "description": "Extension seat, namespaced by convention ('openai/…', 'anthropic/…'); consumers MUST ignore unknown entries. Deliberately open EXCEPT for the reserved key `join`.\n\n`ext.join` IS NOT AN EXTENSION IN THE ORDINARY SENSE. It is the load-bearing join contract — the block that pins the body and seals the joined whole. When it is present, $defs.joinedProfile applies in full and the entire nsv-agent-join.mjs ruleset is in force. When it is absent the document is a builder-profile agent and this seat is unconstrained.\n\nSPEC 1.1: `join` IS NO LONGER RESERVED HERE — it is absorbed into the document proper and this seat is provider space again, exactly as drafted. An AUTHORED ext.join in a 1.1 document refuses (join-v11-ext-join), and so does an authored empty ext:{} (join-ext-empty), the latter so that de-projection is provably lossless.",
      "properties": {
        "join": {
          "$ref": "#/$defs/join"
        }
      }
    },
    "core": {
      "$ref": "#/$defs/core"
    },
    "integrity": {
      "$ref": "#/$defs/docIntegrity"
    }
  },
  "allOf": [
    {
      "$comment": "THE VERSION SWITCH. spec minor 1 -> the 1.1 ruleset. Anything else is a 1.0 document (the gate reads \"1\", \"1.0\" and \"1.0.0\" all as minor 0) and falls through to the 1.0 switch below, which is the draft's own, unchanged.",
      "if": {
        "type": "object",
        "required": [
          "spec"
        ],
        "properties": {
          "spec": {
            "type": "string",
            "pattern": "^1\\.1(\\..*)?$"
          }
        }
      },
      "then": {
        "$ref": "#/$defs/v11Profile"
      },
      "else": {
        "$ref": "#/$defs/v10Profile"
      }
    },
    {
      "$comment": "THE 1.0 PROFILE SWITCH, unchanged from the 1.0 file and still an if/then/ELSE on purpose. Guarded by the version switch so it cannot fire on a 1.1 document: a 1.1 document carries no ext.join, which would otherwise drop it into the builder profile and demand a faceRig of every live-vrm agent. A document that carries ext.join opts into the joined ruleset. A document that does not IS a builder-profile document and is held to $defs.builderProfile — which is where the draft's live-vrm<=>faceRig rule lives. Dropping the else branch would silently retire that rule and two of the 31 parity vectors would stop being caught.",
      "if": {
        "allOf": [
          {
            "not": {
              "type": "object",
              "required": [
                "spec"
              ],
              "properties": {
                "spec": {
                  "type": "string",
                  "pattern": "^1\\.1(\\..*)?$"
                }
              }
            }
          }
        ]
      },
      "then": {
        "allOf": [
          {
            "if": {
              "type": "object",
              "required": [
                "ext"
              ],
              "properties": {
                "ext": {
                  "type": "object",
                  "required": [
                    "join"
                  ]
                }
              }
            },
            "then": {
              "$ref": "#/$defs/joinedProfile"
            },
            "else": {
              "$ref": "#/$defs/builderProfile"
            }
          }
        ]
      }
    }
  ],
  "$defs": {
    "identity": {
      "type": "object",
      "required": [
        "name"
      ],
      "additionalProperties": false,
      "description": "Who she is. Every field maps 1:1 onto an existing registry row — no new vocabulary. Lengths are the registry's own maxLen values, carried verbatim, and they are HARD in the join gate: validateJoin :123-128 REFUSES 'join-cap-overflow' on overflow rather than truncating. That is the opposite of alive-package-1, whose cleanPkgName SLICES. Do not assume one format's leniency in the other.\n\nvalidateJoin :120-122 requires identity to be an object with a non-empty trimmed string `name` — 'join-bad-identity'. The other three rows are optional; when present each must be a string (:125, else 'join-bad-identity'), within cap (:126, 'join-cap-overflow') and free of host-path shapes (:127, 'join-host-path'). A row set to null is SKIPPED, not refused (:124 `if (id[row] == null) continue`) — so identity.notes:null is legal.\n\nCAPS ARE REGISTRY TRUTH, verified 2026-08-04 against config/registry.json (nsv-agent-join.mjs :33-34).",
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 60,
          "$ref": "#/$defs/noHostPath",
          "description": "registry identity.name. CAPS.name = 60 (:34). REQUIRED and must be non-empty after trim (:122)."
        },
        "tagline": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 120,
          "$ref": "#/$defs/noHostPath",
          "description": "registry identity.tagline. CAPS.tagline = 120 (:34). null is legal (:124)."
        },
        "greeting": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 240,
          "$ref": "#/$defs/noHostPath",
          "description": "registry persona.greeting — the first thing she says. CAPS.greeting = 240 (:34). null is legal."
        },
        "notes": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 2000,
          "$ref": "#/$defs/noHostPath",
          "description": "registry persona.notes. CAPS.notes = 2000 (:34). TRAVELS AS CONTEXT, NEVER AS INSTRUCTIONS — the untrusted-content law (:23 'the document is DATA, not instructions'). null is legal."
        }
      }
    },
    "avatar": {
      "type": "object",
      "required": [
        "type"
      ],
      "additionalProperties": false,
      "description": "The body she wears. validateJoin :132 — must be an object whose `type` is in the frozen six, else 'join-bad-avatar-type'. The whitelist (:30) is identical to alive-package.js AVATAR_TYPES; the estate's three copies agree.",
      "properties": {
        "type": {
          "enum": [
            "svg",
            "ascii",
            "sketch-line",
            "hybrid",
            "kinetic-typography",
            "live-vrm"
          ],
          "description": "THE wire value. Unknown -> reject whole ('join-bad-avatar-type' :132 / 'unknown-type' in the package gate)."
        },
        "rung": {
          "type": "integer",
          "minimum": 1,
          "maximum": 6,
          "description": "PRESENTATION ORDINAL ONLY — never a wire fact, never a substitute for `type`. The ascii->3D ladder: 1 ascii · 2 kinetic-typography · 3 sketch-line · 4 svg · 5 hybrid · 6 live-vrm. Behaviour MUST key off `type`; a disagreement resolves in favour of `type`. Unread by validateJoin. Carried from the draft, whose own note records the ordering as a lane proposal owed a confirmation (draft OWED-2)."
        },
        "ref": {
          "type": "object",
          "additionalProperties": false,
          "description": "The light form: point at a body the destination already has; resolution is the destination's job.\n\nWhen `ref` is present the gate requires at least one of castKey/packageUrl to be a STRING (:136-138), else 'join-body-none'. Expressed below as anyOf.\n\nSPEC 1.1 adds `integrity` here — the seat OWED-J1 asked for. In 1.0 this object was additionalProperties:false with castKey/packageUrl only, which is exactly why the pin had to live in ext.join.body and exactly what that ruling was about.",
          "properties": {
            "castKey": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_.-]{1,80}$",
              "description": "Key into the cast registry netshow-cast-live-1 {avatars:{key:row}}. Pattern is the draft's, matching the admin console's show route; validateJoin checks only that it is a string (:136)."
            },
            "packageUrl": {
              "type": "string",
              "maxLength": 500,
              "description": "URL of an .alive.json package; the destination fetches it under its own budget and gate.\n\nEXPLICITLY EXEMPT FROM THE HOST-PATH RULE. nsv-agent-join.mjs :47-49 states it in terms: HOST_PATH_RE is 'NOT applied to avatar.ref.packageUrl — root-relative is that field's law'. A leading '/' is CORRECT here and is refused everywhere else. Do not add a noHostPath ref to this property; doing so would reject all four preset agents, every one of which carries a root-relative packageUrl."
            },
            "integrity": {
              "$ref": "#/$defs/refIntegrity"
            }
          },
          "anyOf": [
            {
              "required": [
                "castKey"
              ],
              "properties": {
                "castKey": {
                  "type": "string"
                }
              }
            },
            {
              "required": [
                "packageUrl"
              ],
              "properties": {
                "packageUrl": {
                  "type": "string"
                }
              }
            }
          ]
        },
        "package": {
          "type": "object",
          "required": [
            "fmt"
          ],
          "properties": {
            "fmt": {
              "const": "alive-package-1"
            }
          },
          "description": "The heavy form: the whole alive-package-1 envelope inline. validateJoin :139 checks ONLY that av.package.fmt === 'alive-package-1' ('join-bad-package-fmt'); the envelope's own structure is alive-package.js parse()'s job and the payload's is the per-type gate's.\n\nTHIS SCHEMA DELIBERATELY DOES NOT RE-SPECIFY THE ENVELOPE. One spec owns it — see alive-package-1.schema.json. Duplicating it here would create the second vocabulary the estate has spent three rounds avoiding. nsv-validate.mjs DOES recurse into it and validates it against that file, which is the right place for the rule to live."
        },
        "faceRig": {
          "$ref": "#/$defs/faceRig",
          "description": "The five-number join, for live-vrm bodies built by the BUILDER. Declared here at the same level as additionalProperties:false — a property introduced only by an if/then subschema is invisible to this object's additionalProperties evaluation under draft 2020-12 annotation scope, so declaring it only inside the conditional would make every live-vrm document invalid. That subtlety is the draft's and it is correct; it is preserved."
        },
        "name": {
          "type": "string",
          "maxLength": 60,
          "$ref": "#/$defs/noHostPath",
          "description": "SPEC 1.1. The body's display name, copied from the package at emit time (emitJoin slices to 60). Structurally identical to 1.0's ext.join.body.name, and it refuses under the SAME string: validateJoin returns join-bad-body-name on a non-string, on length > 60, or on a host-path shape. This cap REJECTS, it does not truncate.\n\nREQUIRED in 1.1, FORBIDDEN in 1.0 — see $defs.v11Profile / $defs.v10Profile."
        }
      },
      "allOf": [
        {
          "description": "EXACTLY ONE BODY SOURCE. Neither cannot mount; both is ambiguous and MUST be rejected rather than silently preferred. validateJoin :133-135 — 'join-body-both' / 'join-body-none'.",
          "oneOf": [
            {
              "required": [
                "ref"
              ],
              "not": {
                "required": [
                  "package"
                ]
              }
            },
            {
              "required": [
                "package"
              ],
              "not": {
                "required": [
                  "ref"
                ]
              }
            }
          ]
        }
      ]
    },
    "faceRig": {
      "type": "object",
      "required": [
        "contract",
        "tune"
      ],
      "additionalProperties": false,
      "description": "The nsv-face-plate/1 claim, equal value-for-value to the VRM file's own extras.nsvFaceRig — enforced UPSTREAM by the G24.faceRig intake gate (tools/nsv-facerig-gate.mjs) and NOT re-checkable from a document reader.\n\nTHE FILE IS THE TUNE AUTHORITY. These five numbers are plate-canvas fractions. They are NEVER derived from a READY-FOR-FACELAB handoff's face.tune_measured, which is face-box space — a different coordinate system with the same five names. tools/nsv-emit-live-vrm-row.mjs :23-26 and nsv-seal-livevrm.js :130-139 both refuse that conversion, and the conversion is undefined estate-wide. See ready-for-facelab-1.schema.json, face.tune_measured.",
      "properties": {
        "contract": {
          "const": "nsv-face-plate/1"
        },
        "canvas": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "w": {
              "type": "integer",
              "minimum": 1,
              "maximum": 4096
            },
            "h": {
              "type": "integer",
              "minimum": 1,
              "maximum": 4096
            }
          }
        },
        "skin": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$"
        },
        "tune": {
          "type": "object",
          "required": [
            "cx",
            "eyeY",
            "spread",
            "mouthY",
            "scale"
          ],
          "additionalProperties": false,
          "description": "THE SAME FIVE NUMBERS the 2D engine calls `zones` — same names, same meaning, carried verbatim, never remapped. No third vocabulary exists.",
          "properties": {
            "cx": {
              "type": "number"
            },
            "eyeY": {
              "type": "number"
            },
            "spread": {
              "type": "number"
            },
            "mouthY": {
              "type": "number"
            },
            "scale": {
              "type": "number"
            }
          }
        }
      }
    },
    "mind": {
      "type": [
        "object",
        "null"
      ],
      "additionalProperties": false,
      "description": "The model that thinks for her. NAMING NOTE: the estate already uses 'model' for her BODY (registry category model = 'Her body'; package payload model.url = the .vrm file), so the thinking lane is `mind` — house vocabulary, vendor-free.\n\nnull IS LEGAL: validateJoin :142 gates on `v.mind != null`, so an absent or null mind skips every check.\n\nLANE ENUM IS JOINED-PROFILE ONLY. In the BUILDER profile `lane` is a free string — the draft deliberately refused to enumerate platform lane ids it could not keep true. In the JOINED profile validateJoin :143 enforces MIND_LANES = ['local-rules','stage-realtime'] (:40) and returns 'join-bad-mind-lane'. $defs.joinedProfile applies that enum; this base definition does not.",
      "properties": {
        "lane": {
          "type": "string",
          "maxLength": 60,
          "description": "Which routed lane thinks for her. Builder profile: free string. Joined profile: MUST be 'local-rules' or 'stage-realtime'."
        },
        "model": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 120,
          "description": "The model id inside that lane, when the lane exposes a choice. validateJoin :144 — if non-null it MUST be a string, else 'join-bad-mind-lane' (the reason string is shared with the lane check; that is the gate's own behaviour, not a transcription error)."
        },
        "note": {
          "type": "string",
          "maxLength": 500
        },
        "persona": {
          "$ref": "#/$defs/joinPersona",
          "description": "SPEC 1.1. The native home of 1.0's ext.join.persona — unchanged in shape, unchanged in refusal string (join-bad-persona), and it belongs to the mind because a persona IS mind. null or absent is the honest empty state and v1 emitters SHOULD emit it: the estate has exactly one alive-persona document, hand-authored and surface-bound, and a seat that ships empty is honest where a seat that ships fabricated core[] is not.\n\nFORBIDDEN in 1.0 — see $defs.v10Profile."
        }
      },
      "$comment": "STRICTER THAN THE GATE. validateJoin never walks mind's keys, so an unknown key inside mind is ACCEPTED by the shipped gate — confirmed empirically by the intake probe X4b (s45-intake-f-20260804/intake-adversarial.mjs, 'OBSERVATION unknown key inside mind: ACCEPTED'). additionalProperties:false is retained from the proven draft as producer discipline. A document that trips only this rule is GATE-VALID; nsv-validate.mjs classifies it SCHEMA-STRICTER-THAN-GATE, never a bare FAIL. Note the seal covers such a key either way, so it cannot be injected after sealing."
    },
    "voice": {
      "type": [
        "object",
        "null"
      ],
      "additionalProperties": false,
      "description": "How she sounds. HONESTY GATE: the live registry's voice category ships fields:[] — literally zero decisions. ONE realtime lane exists today, walt-gated, on the stage. This block is shaped for a later breadth round rather than pretending options exist. Entirely unread by validateJoin. emitJoin :328 emits {lane:'stage-realtime', voiceId:null}.",
      "properties": {
        "lane": {
          "type": "string",
          "maxLength": 60,
          "description": "The voice lane id. One lane exists today; the registry refuses to fake options and so does this schema."
        },
        "voiceId": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 120,
          "description": "The voice inside that lane. null is the HONEST value when the lane exposes no choice — never a fabricated default."
        },
        "note": {
          "type": "string",
          "maxLength": 500
        }
      }
    },
    "capabilities": {
      "type": [
        "object",
        "null"
      ],
      "description": "Every capability is an ON / OFF / DEFAULT decision, and the pick is saved into the agent's JSON. Keys are capability ids — the SAME ids as registry fields[].id and admin-catalog capabilities[].id. One id space, three files, zero translation.\n\nnull/absent is legal: validateJoin :148 gates on `v.capabilities != null`.\n\nKEY PATTERN — THE GATE'S, NOT THE DRAFT'S. validateJoin :46 CAP_ID_RE = /^[a-z][a-zA-Z0-9-]*(?:\\.[a-zA-Z0-9][a-zA-Z0-9_-]*)+$/ and :151 additionally rejects the literal '__proto__' and any segment beginning with '_' (/(^|\\.)_/) — 'join-bad-cap-id'. At least one dot is structurally required.\n\nThe draft used the STRICTER ^[a-z][A-Za-z0-9]*(\\.[a-zA-Z][A-Za-z0-9_]*)+$, which forbids hyphens and forbids a digit starting a later segment. The gate is the truth and the looser form is published; verified to leave all 31 parity vectors correct. THE UNDERSCORE IS NOT COSMETIC — six real ids (tools.grant.{guest_note, reminder_set, clock_today, kb_lookup, show_page, web_lookup}) carry a live wire tool name in their tail segment, consumed by running code and frozen by naming law. A pattern excluding them is a pattern no honest implementation could satisfy. A leading '_' is rejected in every segment, so '__proto__' cannot appear as a capability id by pattern alone; the gate belts-and-braces it anyway.",
      "propertyNames": {
        "pattern": "^[a-z][a-zA-Z0-9-]*(\\.[a-zA-Z0-9][a-zA-Z0-9_-]*)+$",
        "maxLength": 80
      },
      "additionalProperties": {
        "$ref": "#/$defs/pick"
      },
      "properties": {
        "standing.now": {
          "allOf": [
            {
              "$ref": "#/$defs/pick"
            }
          ],
          "maxLength": 280,
          "description": "CAP-ENFORCED. validateJoin :158-160 — a string over CAPS.standingNow (280, :34) is 'join-cap-overflow'. The literal token 'default' is EXEMPT from the cap (:158 tests `p !== 'default'` first) and is shorter anyway."
        },
        "standing.keepInMind": {
          "allOf": [
            {
              "$ref": "#/$defs/pick"
            }
          ],
          "maxLength": 500,
          "description": "CAP-ENFORCED. validateJoin :161-163 — CAPS.standingKeepInMind = 500 (:34). 'default' exempt, as above."
        }
      },
      "$comment": "NO GENERAL STRING CAP. The draft capped every pick string at 2000; validateJoin caps ONLY these two keys. Publishing the draft's cap would reject gate-valid documents, so it is not published. maxLength on the two named keys is a no-op for non-string picks, which is correct — maxLength applies only to strings under JSON Schema."
    },
    "pick": {
      "description": "One capability pick. validateJoin :152-157 accepts EXACTLY: true, false, the literal string 'default', any other string, or a FINITE number. It REFUSES null explicitly (:153 — 'join-bad-pick'), and refuses arrays/objects/undefined by exhaustion (:155-156).\n\nnull IS NOT ABSENCE HERE. Omitting a key means 'never picked'; writing null is a malformed pick and rejects the whole document. That asymmetry is deliberate and is the reason null is excluded from the type list below.\n\nTRI-STATE ROWS (registry type 'toggle'):\n  true      -> pinned ON for this agent; holds even if the platform default moves\n  false     -> pinned OFF; same pin semantics (falsy-zero law: false is a VALUE, never absence)\n  'default' -> explicit follow; inherits platformDefaults, else the shipped default\n  (absent)  -> never picked; behaves as DEFAULT (back-compat)\n\nVALUE ROWS (ranges, texts, selects, colours): a typed value pins it; 'default' is an explicit follow.\n\nFALSY-ZERO LAW: every comparison against a pick is === true / === false / === 'default'. The token 'default' is TRUTHY; a !! shortcut silently reads an explicit follow as a pin ON. nsv-agent-join.mjs :20-21 names this as a carried law — 'tri-state law: strict comparisons only; \"default\" is truthy and must never be read through !!; null pick rejects'.\n\nSTRICT-OUT LAW: the 'default' token NEVER escapes the resolver. Everything downstream of resolution sees a resolved typed value only.\n\nFINITENESS: :155 tests isFinite(p). JSON cannot represent Infinity or NaN, so {type:'number'} is exactly equivalent for any document that parsed — no extra keyword is needed or possible.",
      "anyOf": [
        {
          "type": "boolean"
        },
        {
          "const": "default"
        },
        {
          "type": "number"
        },
        {
          "type": "string"
        }
      ]
    },
    "embed": {
      "type": "object",
      "additionalProperties": false,
      "description": "Where she may live. Consumed by the site kit: one script tag + one agent-JSON URL = a living agent on any website. Entirely unread by validateJoin — inside AGENT_TOP_KEYS, never inspected. Carried forward from the draft unchanged.",
      "properties": {
        "targets": {
          "type": "array",
          "maxItems": 32,
          "items": {
            "$ref": "#/$defs/embedTarget"
          },
          "description": "Zero targets is a legal and meaningful state: the agent exists and is not embedded anywhere. It is rendered differently from 'could not read'."
        },
        "siteKey": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{8,64}$",
          "description": "Per-site key. NOT a secret and MUST NOT be treated as one: it ships inside a public script tag on a public page. It identifies for routing and rate-limiting; anything that must be unforgeable rides a short-lived signed token minted by an authenticated route, never this value."
        },
        "allowedOrigins": {
          "type": "array",
          "maxItems": 64,
          "items": {
            "type": "string",
            "maxLength": 200,
            "pattern": "^(https?://[A-Za-z0-9.-]+(:[0-9]{1,5})?|\\*)$"
          },
          "description": "Origins permitted to mount this agent. '*' is legal and means the owner accepted an open embed — a decision the builder must show in words, never a quiet default."
        }
      }
    },
    "embedTarget": {
      "type": "object",
      "required": [
        "origin"
      ],
      "additionalProperties": false,
      "properties": {
        "origin": {
          "type": "string",
          "maxLength": 200,
          "pattern": "^https?://[A-Za-z0-9.-]+(:[0-9]{1,5})?$"
        },
        "label": {
          "type": "string",
          "maxLength": 80
        },
        "mount": {
          "enum": [
            "corner",
            "inline",
            "fullscreen"
          ],
          "description": "corner is the companion presence; inline mounts into a host element; fullscreen takes the page."
        },
        "selector": {
          "type": "string",
          "maxLength": 120,
          "description": "Host element for inline. Ignored by the other mounts."
        },
        "enabled": {
          "type": "boolean",
          "description": "Absent = enabled. A disabled target is KEPT, not deleted, so turning a site back on is one flip and never a re-typed origin."
        }
      }
    },
    "integration": {
      "type": "object",
      "additionalProperties": false,
      "description": "The cast row's integration triad, carried through VERBATIM when this document is built from a netshow-cast-live-1 row. null is BY DESIGN on a fresh row and means PENDING, not broken. Unread by validateJoin.\n\nOPEN QUESTION CARRIED FORWARD (draft OWED-1): the estate has never stated whether integration.model names the thinking model or the VRM body — both readings are live in current files. This schema carries the triad OPAQUELY and does NOT map it onto `mind` or `avatar`. A reader MUST NOT infer one.",
      "properties": {
        "voice": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 120
        },
        "model": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 200
        },
        "persona": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 120
        }
      }
    },
    "provenance": {
      "type": "object",
      "additionalProperties": false,
      "description": "Where the facts came from. Informational: a reader SHOWS it, never TRUSTS it for authorization. Unread by validateJoin.\n\nHOST-PATH RULE, BOTH HALVES: additionalProperties:false refuses a filesystem path arriving as an UNEXPECTED key; $defs.noHostPath refuses one hiding in an ALLOW-LISTED value. Both are needed — the leak this closes was re-created on the DATA plane, inside a field a page renders, where no source grep of any page could see it. This is the PRE->POST change in the draft's lineage and it is preserved verbatim.",
      "properties": {
        "castKey": {
          "type": "string",
          "maxLength": 80,
          "$ref": "#/$defs/noHostPath"
        },
        "readyForFacelab": {
          "type": "object",
          "additionalProperties": false,
          "description": "A REFERENCE to the studio's acceptance artifact (netshow-alive/ready-for-facelab/1), not a copy of it. Only the non-path fields travel: agent.vrm is an absolute filesystem path in the real artifact and MUST NOT be copied into a document a page may render. See ready-for-facelab-1.schema.json for the artifact itself — this block is a different document referencing that one, never a field of it.",
          "properties": {
            "schema": {
              "const": "netshow-alive/ready-for-facelab/1"
            },
            "acceptedAt": {
              "type": "string",
              "format": "date-time",
              "$ref": "#/$defs/noHostPath"
            },
            "sha256": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            },
            "bytes": {
              "type": "integer",
              "minimum": 0
            },
            "title": {
              "type": "string",
              "maxLength": 120,
              "$ref": "#/$defs/noHostPath"
            },
            "license": {
              "type": "string",
              "maxLength": 60,
              "$ref": "#/$defs/noHostPath"
            }
          }
        },
        "emittedBy": {
          "type": "string",
          "maxLength": 120,
          "$ref": "#/$defs/noHostPath"
        }
      }
    },
    "noHostPath": {
      "description": "A string that is NOT shaped like a filesystem path. Five alternatives: POSIX absolute (/…), home-relative (~/… ~\\…), a Windows drive (C:/… C:\\…), the file: scheme, and a backslash ANYWHERE.\n\nRELATIVE PATHS STAY LEGAL AND MUST — provenance.emittedBy is a repo-relative tool name — so 'contains a slash' would be the wrong rule and would refuse every valid document.\n\nRELATION TO THE JOIN GATE: nsv-agent-join.mjs :50 HOST_PATH_RE is /(^\\s*\\/)|(^\\s*~\\/)|(^\\s*[A-Za-z]:[\\\\/])|(file:)|(\\\\)/ — the same five shapes with two differences. The gate TOLERATES LEADING WHITESPACE before the anchor (so ' /etc/passwd' is caught by the gate) and matches 'file:' anywhere rather than only at the start. This definition adds \\s* to the three anchored alternatives so the published contract is not weaker than the shipped gate. WHERE THE GATE APPLIES IT: identity.{name,tagline,greeting,notes} (:127) and string capability picks (:164) — and NEVER to avatar.ref.packageUrl, which is exempt by the gate's own comment (:47-49).\n\nWHERE IT IS NOT APPLIED HERE, and why that is not a gap: readyForFacelab.schema is a const and readyForFacelab.sha256 is a total ^[0-9a-f]{64}$ — neither can hold any of the five shapes, so a ref there would be a check that cannot fail. An editor who loosens either must add the ref back.\n\nPortability: anchors, classes and alternation only, so Python re, PCRE and ECMA RegExp agree.",
      "not": {
        "type": "string",
        "pattern": "(^\\s*/)|(^\\s*~[/\\\\])|(^\\s*[A-Za-z]:[/\\\\])|(file:)|(\\\\)"
      }
    },
    "join": {
      "type": "object",
      "required": [
        "contract",
        "body"
      ],
      "additionalProperties": false,
      "description": "THE JOIN BLOCK — ext.join. Pins the body (a sealed alive-package-1) and seals the joined whole.\n\nvalidateJoin :169 requires ext to be an object AND ext.join to be an object, else 'join-missing'. :172-174 closes the key set to JOIN_KEYS (:38) = contract, body, persona, integrity — anything else is 'join-unknown-key'.\n\nWHAT LIVES HERE AND WHY: the mind travels in the document; the BODY does not. What travels instead is a pair of hashes that identify exactly which body this mind was joined to. That is the security property of the format — see $defs.joinedProfile.",
      "properties": {
        "contract": {
          "const": "nsv-agent-join/1",
          "description": "validateJoin :171 — 'join-bad-contract'. JOIN_CONTRACT is declared :27. Exact match; there is no version negotiation on this string."
        },
        "body": {
          "$ref": "#/$defs/joinBody"
        },
        "persona": {
          "$ref": "#/$defs/joinPersona"
        },
        "integrity": {
          "$ref": "#/$defs/joinIntegrity"
        }
      }
    },
    "joinBody": {
      "type": "object",
      "required": [
        "source",
        "type",
        "name",
        "packageSha256"
      ],
      "additionalProperties": false,
      "description": "The body pins. validateJoin :176-187. Key set closed to BODY_KEYS (:39) = source, type, name, packageSha256, fileSha256 — 'join-unknown-key'. A non-object is 'join-body-none' (:176).",
      "properties": {
        "source": {
          "enum": [
            "ref",
            "embed"
          ],
          "description": "validateJoin :180 — anything else is 'join-bad-body-source'. :181 additionally requires AGREEMENT WITH THE AVATAR: (source === 'ref') must equal (avatar.ref is an object). Declaring 'embed' while the avatar carries a ref — or the reverse — is 'join-bad-body-source'. Expressed as a cross-field rule in $defs.joinedProfile, since it spans two branches of the document."
        },
        "type": {
          "enum": [
            "svg",
            "ascii",
            "sketch-line",
            "hybrid",
            "kinetic-typography",
            "live-vrm"
          ],
          "description": "MUST EQUAL avatar.type (:182 — 'join-body-type-mismatch') and, in embed mode, avatar.package.type as well (:183). Cross-field; enforced in $defs.joinedProfile and re-checked at load by verifyBody :261."
        },
        "name": {
          "type": "string",
          "maxLength": 60,
          "$ref": "#/$defs/noHostPath",
          "description": "The body's display name, copied from the package at emit time (emitJoin :302 slices to 60). validateJoin :184 REFUSES 'join-bad-body-name' on a non-string, on length > 60, or on a host-path shape — this cap REJECTS, it does not truncate."
        },
        "packageSha256": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$",
          "description": "THE JOIN PIN — the identity of the body this mind was joined to. Shape checked at :185 (isHex64, 'join-bad-pin').\n\nVALUE IS NOT SCHEMA-CHECKABLE. verifyBody :263-267 recomputes sha-256(canonJson(pkg.payload)) from the fetched body and requires (a) that it equals the body's OWN integrity.payload — 'join-body-seal-fail' — and (b) that it equals THIS field — 'join-body-mismatch'. Step (b) is the one that matters: a competent attacker can forge a body AND recompute its internal seal so step (a) passes, and the intake battery proves exactly that (s45-intake-f-20260804/intake-adversarial.mjs T3, 'self-consistent forged body re-pointed -> REFUSED by JOIN PIN'). This pin is the last line, and no JSON Schema can stand on it.\n\nNote it pins the PAYLOAD, not the file — so the body may be re-exported with a new exportedAt and still satisfy this pin."
        },
        "fileSha256": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$",
          "description": "THE ARTIFACT PIN — sha-256 over the package FILE's exact bytes as fetched. REF MODE ONLY.\n\nPRESENT-XOR-ABSENT BY SOURCE, and both directions reject: :186 requires it in ref mode ('join-bad-pin') and :187 requires it ABSENT in embed mode ('join-bad-pin'). Embed mode has no file, so a hash of one would be a fiction. Expressed as an if/then in $defs.joinedProfile.\n\nWhy both pins exist: packageSha256 survives a benign re-export; fileSha256 does not. An envelope-only tamper (editing `spec` or `name`, leaving the payload untouched) passes the payload seal AND the join pin, and is caught ONLY here — intake battery T2, 'envelope byte tamper -> REFUSED by file pin'. Two pins, two distinct attacks."
        }
      },
      "allOf": [
        {
          "$comment": "fileSha256 present-XOR-absent by source. validateJoin :186-187.",
          "if": {
            "properties": {
              "source": {
                "const": "ref"
              }
            },
            "required": [
              "source"
            ]
          },
          "then": {
            "required": [
              "fileSha256"
            ]
          },
          "else": {
            "not": {
              "required": [
                "fileSha256"
              ]
            }
          }
        }
      ]
    },
    "joinPersona": {
      "type": [
        "object",
        "null"
      ],
      "description": "The persona seat. validateJoin :190-205. null (or absent) is the ordinary state — all four preset agents carry persona:null — and it is checked FIRST (:190), so null short-circuits every rule below. Anything that is neither null/undefined nor an object with a legal `source` is 'join-bad-persona' (:192, :203).",
      "oneOf": [
        {
          "type": "null"
        },
        {
          "type": "object",
          "required": [
            "source",
            "url",
            "sha256"
          ],
          "properties": {
            "source": {
              "const": "ref"
            },
            "url": {
              "type": "string",
              "description": "validateJoin :194 requires a string; no shape rule beyond that."
            },
            "sha256": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$",
              "description": "isHex64 at :194. Pins the referenced persona document; the recompute is the consumer's, not this schema's."
            }
          },
          "description": "REF FORM: {source:'ref', url, sha256}. Both url and sha256 are mandatory — :194 rejects 'join-bad-persona' if either is missing or malformed."
        },
        {
          "type": "object",
          "required": [
            "source",
            "doc"
          ],
          "properties": {
            "source": {
              "const": "embed"
            },
            "doc": {
              "type": "object",
              "required": [
                "format",
                "v",
                "character",
                "core"
              ],
              "description": "validateJoin :196-201 — a rare place where the gate reaches INTO a nested document. All five conditions must hold or 'join-bad-persona'.",
              "properties": {
                "format": {
                  "const": "alive-persona",
                  "description": ":197 — exact."
                },
                "v": {
                  "const": 1,
                  "description": ":197 — the NUMBER 1, strictly. The string '1' fails."
                },
                "character": {
                  "type": "object",
                  "required": [
                    "name"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    }
                  },
                  "description": ":197-198 — must be an object with a STRING name."
                },
                "core": {
                  "type": "array",
                  "minItems": 1,
                  "items": {
                    "type": "string"
                  },
                  "description": ":198-199 — must be a NON-EMPTY array (`!d.core.length` rejects) of strings only (`d.core.some(s => typeof s !== 'string')` rejects)."
                }
              }
            }
          },
          "description": "EMBED FORM: {source:'embed', doc:{...}} carrying an alive-persona v1 document inline."
        }
      ]
    },
    "joinIntegrity": {
      "type": [
        "object",
        "null"
      ],
      "required": [
        "algo",
        "canon",
        "doc"
      ],
      "additionalProperties": false,
      "description": "THE DETACHED WHOLE-DOCUMENT SEAL. Shape gate at validateJoin :208-213 ('join-bad-integrity-shape'); the shape check runs only when integrity != null, so null/absent is legal and means UNSEALED.\n\nHOW THE SEAL IS COMPUTED — reproduce this exactly or every verification will disagree. sealJoin :224-232: (1) validateJoin the document, refusing to seal anything invalid; (2) DEEP-CLONE and DELETE ext.join.integrity (withoutSeal :218-222); (3) canonJson the result; (4) sha-256 -> lowercase hex; (5) write it back as {algo, canon, doc}. It is DETACHED — the seal is not part of what it covers, which is what makes it recomputable without a chicken-and-egg problem.\n\nWHAT IT COVERS: the ENTIRE document minus this one field. Every top-level key, the whole mind, the whole capabilities map, and both body pins. So a key the structure gate never inspects — an unknown key inside `mind`, for instance — is still sealed and cannot be injected after the fact.\n\nverifySeal :234-242 returns 'pass' | 'fail' | 'absent' | 'unavailable'. 'fail' on a document with a declared seal MUST reject whole. NO SCHEMA CAN CHECK THIS VALUE — a one-hex-digit flip leaves a perfectly well-shaped block that this subschema accepts and the recompute rejects (intake battery X5: 'tampered seal -> validate ok, verdict fail (never silent)'). That gap is precisely why nsv-validate.mjs exists.",
      "properties": {
        "algo": {
          "const": "sha-256",
          "description": ":210-211 — the condition is at :210, the refusal string 'join-bad-integrity-shape' at :211. Anything but 'sha-256' refuses."
        },
        "canon": {
          "const": "sorted-keys-json",
          "description": ":210-211 — names canonJson (:53-73), which mirrors alive-package.js byte-for-byte: sorted keys, no whitespace, undefined skipped, non-finite throws."
        },
        "doc": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$",
          "description": ":210 isHex64 (refusal at :211) — LOWERCASE only. Note the key is `doc` here and `payload` in alive-package-1: different scope, deliberately different name."
        }
      }
    },
    "joinedProfile": {
      "$comment": "APPLIED AUTOMATICALLY when ext.join is present (see the root allOf). This is nsv-agent-join.mjs validateJoin() expressed as schema, restricted to what schema can carry.",
      "description": "THE JOINED DOCUMENT RULESET. Everything here is in force only for documents that carry ext.join.\n\nFOUR CROSS-FIELD RULES the builder profile has no need of, plus the mind-lane enum. The rules below are the ones a single-branch subschema cannot express, so they live at document level.\n\nNOT EXPRESSIBLE HERE AT ALL — nsv-validate.mjs runs these against the real implementation:\n  · the seal recompute (verifySeal)\n  · both body pins (verifyBody: the body's own payload seal, the join pin, and the ref-mode file pin)\n  · the load-time type re-check against the FETCHED body (verifyBody :261)\nA document can satisfy every rule in this file and still be refused at load. That is not a defect in the schema; it is the difference between a shape and a fact.",
      "type": "object",
      "required": [
        "ext"
      ],
      "allOf": [
        {
          "$comment": "MIND LANE ENUM — validateJoin :143, MIND_LANES :40. Joined documents only; the builder profile deliberately leaves lane open. A null/absent mind skips the check entirely (:142), which is why `required` is not asserted.",
          "if": {
            "required": [
              "mind"
            ],
            "properties": {
              "mind": {
                "type": "object"
              }
            }
          },
          "then": {
            "properties": {
              "mind": {
                "required": [
                  "lane"
                ],
                "properties": {
                  "lane": {
                    "enum": [
                      "local-rules",
                      "stage-realtime"
                    ]
                  }
                }
              }
            }
          }
        },
        {
          "$comment": "BODY TYPE AGREEMENT — validateJoin :182, ext.join.body.type must EQUAL avatar.type. JSON Schema cannot compare two values directly, so this is six explicit branches, one per legal type. Re-checked at load against the fetched body by verifyBody :261.",
          "allOf": [
            {
              "if": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "type": {
                        "const": "svg"
                      }
                    },
                    "required": [
                      "type"
                    ]
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "ext": {
                    "properties": {
                      "join": {
                        "properties": {
                          "body": {
                            "properties": {
                              "type": {
                                "const": "svg"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "type": {
                        "const": "ascii"
                      }
                    },
                    "required": [
                      "type"
                    ]
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "ext": {
                    "properties": {
                      "join": {
                        "properties": {
                          "body": {
                            "properties": {
                              "type": {
                                "const": "ascii"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "type": {
                        "const": "sketch-line"
                      }
                    },
                    "required": [
                      "type"
                    ]
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "ext": {
                    "properties": {
                      "join": {
                        "properties": {
                          "body": {
                            "properties": {
                              "type": {
                                "const": "sketch-line"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "type": {
                        "const": "hybrid"
                      }
                    },
                    "required": [
                      "type"
                    ]
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "ext": {
                    "properties": {
                      "join": {
                        "properties": {
                          "body": {
                            "properties": {
                              "type": {
                                "const": "hybrid"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "type": {
                        "const": "kinetic-typography"
                      }
                    },
                    "required": [
                      "type"
                    ]
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "ext": {
                    "properties": {
                      "join": {
                        "properties": {
                          "body": {
                            "properties": {
                              "type": {
                                "const": "kinetic-typography"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "type": {
                        "const": "live-vrm"
                      }
                    },
                    "required": [
                      "type"
                    ]
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "ext": {
                    "properties": {
                      "join": {
                        "properties": {
                          "body": {
                            "properties": {
                              "type": {
                                "const": "live-vrm"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          ]
        },
        {
          "$comment": "BODY SOURCE AGREEMENT — validateJoin :181, `(b.source === 'ref') !== hasRef` is 'join-bad-body-source'. source 'ref' <-> avatar.ref present; source 'embed' <-> avatar.package present. Both directions, because a mismatch in either is ambiguity about where the body actually is.",
          "allOf": [
            {
              "if": {
                "properties": {
                  "ext": {
                    "properties": {
                      "join": {
                        "properties": {
                          "body": {
                            "properties": {
                              "source": {
                                "const": "ref"
                              }
                            },
                            "required": [
                              "source"
                            ]
                          }
                        },
                        "required": [
                          "body"
                        ]
                      }
                    },
                    "required": [
                      "join"
                    ]
                  }
                },
                "required": [
                  "ext"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "required": [
                      "ref"
                    ]
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "ext": {
                    "properties": {
                      "join": {
                        "properties": {
                          "body": {
                            "properties": {
                              "source": {
                                "const": "embed"
                              }
                            },
                            "required": [
                              "source"
                            ]
                          }
                        },
                        "required": [
                          "body"
                        ]
                      }
                    },
                    "required": [
                      "join"
                    ]
                  }
                },
                "required": [
                  "ext"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "required": [
                      "package"
                    ]
                  }
                }
              }
            }
          ]
        },
        {
          "$comment": "EMBED-MODE PACKAGE TYPE — validateJoin :183, `hasPkg && av.package.type !== av.type` is 'join-body-type-mismatch'. The inline envelope's own `type` must agree with the avatar's. Six branches again, and only when avatar.package is present.",
          "allOf": [
            {
              "if": {
                "properties": {
                  "avatar": {
                    "required": [
                      "package"
                    ],
                    "properties": {
                      "type": {
                        "const": "svg"
                      }
                    }
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "package": {
                        "properties": {
                          "type": {
                            "const": "svg"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "required": [
                      "package"
                    ],
                    "properties": {
                      "type": {
                        "const": "ascii"
                      }
                    }
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "package": {
                        "properties": {
                          "type": {
                            "const": "ascii"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "required": [
                      "package"
                    ],
                    "properties": {
                      "type": {
                        "const": "sketch-line"
                      }
                    }
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "package": {
                        "properties": {
                          "type": {
                            "const": "sketch-line"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "required": [
                      "package"
                    ],
                    "properties": {
                      "type": {
                        "const": "hybrid"
                      }
                    }
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "package": {
                        "properties": {
                          "type": {
                            "const": "hybrid"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "required": [
                      "package"
                    ],
                    "properties": {
                      "type": {
                        "const": "kinetic-typography"
                      }
                    }
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "package": {
                        "properties": {
                          "type": {
                            "const": "kinetic-typography"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "required": [
                      "package"
                    ],
                    "properties": {
                      "type": {
                        "const": "live-vrm"
                      }
                    }
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "package": {
                        "properties": {
                          "type": {
                            "const": "live-vrm"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          ]
        }
      ],
      "$comment#faceRig": "DELIBERATELY ABSENT: the draft's rule that a live-vrm avatar MUST carry faceRig. That rule is correct for the BUILDER (which emits faceRig from the file's G24-verified extras) and WRONG for a joined document, because emitJoin :324-326 emits avatar as {type, ref} or {type, package} and NEVER attaches faceRig. The moment the live-vrm seal door is wired and a live-vrm body is joined, the draft's rule would reject a document its own emitter produced. Unexercised today (zero live-vrm packages exist in the docroot), which is exactly why it is worth naming before it bites. The builder profile keeps the rule; see $defs.builderProfile."
    },
    "builderProfile": {
      "$comment": "APPLIED AUTOMATICALLY to any document that does NOT carry ext.join (the else branch of the root profile switch). It is NOT applied to joined documents — see joinedProfile $comment#faceRig for why that would reject a document emitJoin itself produces.",
      "description": "The draft's live-vrm rule, preserved for the producer it was written for. A BUILDER-emitted live-vrm agent carries the five-number join; no other type does, and a non-live-vrm document carrying faceRig is a document with a body claim it cannot honour.",
      "type": "object",
      "properties": {
        "avatar": {
          "allOf": [
            {
              "if": {
                "properties": {
                  "type": {
                    "const": "live-vrm"
                  }
                },
                "required": [
                  "type"
                ]
              },
              "then": {
                "required": [
                  "faceRig"
                ]
              },
              "else": {
                "not": {
                  "required": [
                    "faceRig"
                  ]
                }
              }
            }
          ]
        }
      }
    },
    "producerProfile": {
      "$comment": "OPT-IN. What a correct emitter writes, over and above what the gate demands.",
      "description": "Producer discipline: the strict spec form and the identity/provenance fields a document ought to carry. emitJoin (:309-331) always emits fmt, spec, product, kind, uid, createdAt, updatedAt, generator, identity, avatar, mind, voice, capabilities, ext — fourteen keys; embed/integration/provenance are left to later surfaces and are legitimately absent, which is why they are not required here.",
      "type": "object",
      "required": [
        "fmt",
        "spec",
        "product",
        "kind",
        "uid",
        "createdAt",
        "updatedAt",
        "generator",
        "identity",
        "avatar"
      ],
      "properties": {
        "spec": {
          "type": "string",
          "pattern": "^1\\.[0-9]+$",
          "description": "MAJOR.MINOR exactly. The gate accepts more; an emitter should not use the latitude."
        }
      }
    },
    "divergences": {
      "$comment": "DOCUMENTATION ONLY — never applied to a document. Listed so that every place this file is stricter or looser than the shipped gate is a recorded decision rather than an accident. nsv-validate.mjs classifies a document that trips only a STRICTER rule as SCHEMA-STRICTER-THAN-GATE.",
      "description": "STRICTER THAN validateJoin (gate-valid documents may fail these):\n  1. mind.additionalProperties:false — the gate never walks mind's keys; intake probe X4b observed an unknown key inside mind ACCEPTED. Kept as producer discipline; the seal covers such a key regardless.\n  2. identity.additionalProperties:false — the gate walks only the four capped rows (:123).\n  3. uid maxLength 80 — the gate caps nothing on uid.\n  4. provenance/generator noHostPath guards and additionalProperties:false — the gate never reads either block.\n  5. embed / integration / voice / faceRig shapes — entirely unread by the gate; inherited from the proven draft.\n  6. createdAt/updatedAt format:date-time — unread by the gate.\n\nLOOSER THAN THE DRAFT (deliberately, because the gate is the truth):\n  1. spec ^1(\\.(0|1)(\\..*)?)?$ — still accepts '1' and '1.0.0' as the gate does, but no longer accepts an unknown MINOR. RETIGHTENED IN 1.1: the 1.0 file's ^1(\\..*)?$ accepted '1.2', which the v1.1 gate refuses by name (join-bad-spec-minor). A schema accepting what the gate refuses is a FALSE PASS — GATE-STRICTER-THAN-SCHEMA — and this contract may never produce one.\n  2. capability id pattern — the gate's CAP_ID_RE admits hyphens and a digit-initial later segment; the draft's did not.\n  3. no 2000-char cap on general capability string picks — the gate caps only standing.now (280) and standing.keepInMind (500).\n  4. the live-vrm=>faceRig requirement is not applied to joined documents — emitJoin never emits faceRig.\nAll four looser points were re-verified against the 31-vector parity fixture on 2026-08-05: still 31/31 correct.\n\nADDED IN 1.1:\n  STRICTER — 7. $defs.core.additionalProperties:false is enforced here and by the gate alike (join-core-unknown-seat), so this is parity, not divergence; it is listed only because a reader expecting `core` to behave like `ext` will be surprised, and being surprised by a refusal is the outcome that seat exists to produce.\n  NOT CHECKABLE — 8. the compatibility projection is a RUNTIME artifact of validateJoin and never appears on disk, so no rule here can see it. A document read off disk is always the authored form; a projected document handed back to validateJoin re-validates idempotently, and one that carries an ext.join which is NOT the exact derived projection refuses join-v11-ext-join.\n  NOT CHECKABLE — 9. 1.1 embed mode drops 1.0's second copy of the payload hash. Under 1.0 that copy was compared to a value derived from the same document; the comparison could only fail if a writer contradicted itself, never if an attacker did, because an attacker editing the payload edits the copy too. What actually binds an embedded body is the package's own seal (verifyBody recomputes it) and the whole-document seal (which covers the entire envelope). Both are unchanged. REF MODE LOSES NOTHING AT ALL: both pins survive verbatim at avatar.ref.integrity.",
      "type": "null"
    },
    "refusalReasonIndex": {
      "$comment": "DOCUMENTATION ONLY. 34 stable refusal strings. THE AUTHORITY IS THE MODULE, NOT THIS FILE AND NOT THE SPEC PROSE: nsv-agent-join.mjs exports REFUSAL_REASONS, derived from its own bodies, and the lane suite proves the export against the source bytes. If this list and the module ever disagree, the module is right and this list is the bug.",
      "description": "THIRTY-FOUR REASONS — the 1.0 module's 29, carried unrenamed, plus 5 that name conditions only 1.1 can be in.\n\nTHE PHANTOM IS GONE. JOIN-SPEC 1.0 §7.4 published a 12-string list that included `join-seal-fail`. NO VERSION OF THE MODULE HAS EVER EMITTED THAT STRING. A consumer coding refusal handling from that list would have pinned a string that never arrives and missed 17 that do. The list was also silently partial — 12 of 29. Both faults have one cause: prose held a vocabulary that only code can own.\n\nTWO VOCABULARIES, AND THEY NEVER MIX:\n  REFUSAL REASONS (below) — returned by validateJoin / verifyBody as {ok:false, reason}. A reason means the document was REFUSED.\n  SEAL VERDICTS — returned by verifySeal as a bare string: pass | fail | absent | unavailable. A verdict is an OBSERVATION about a seal. `fail` on a declared seal MUST reject whole, but it is still not a reason string, and `absent` / `unavailable` are accept-with-honesty. `join-seal-fail` was born of blending these two; the module exports SEAL_VERDICTS beside REFUSAL_REASONS so the separation is code, not prose.\n\nWHICH MINOR CAN PRODUCE WHICH. Three 1.0 rows cannot fire on a 1.1 document, because 1.1 removed the thing they police rather than because they were dropped:\n  join-bad-contract     1.0 only — 1.1 carries the contract in `spec`\n  join-missing          1.0 only — 1.1 has no ext.join to miss\n  join-bad-body-source  1.0 only — 1.1 DERIVES source from the avatar, so it cannot disagree\nFive rows are 1.1-only: join-bad-spec-minor, join-v11-ext-join, join-ext-empty, join-bad-core, join-core-unknown-seat. The remaining 26 fire on both.\n\nSTRUCTURE GATE — validateJoin():\n  join-not-json          both    input string is not JSON                      | schema: n/a (parse precedes validation)\n  join-not-object        both    top level is not a plain object               | schema: type:object\n  join-bad-fmt           both    fmt !== 'nsv-agent-1'                         | schema: properties.fmt const\n  join-bad-spec          both    spec absent/non-string, or MAJOR !== '1'      | schema: properties.spec pattern\n  join-bad-spec-minor    1.1+    MAJOR 1 but an unknown MINOR (>=2)            | schema: properties.spec pattern (tightened; see $defs.divergences)\n  join-unknown-key       both    key outside the minor's top-key set, JOIN_KEYS, BODY_KEYS, avatar, avatar.ref or ref.integrity | schema: additionalProperties:false at every level + the profile closures\n  join-bad-uid           both    uid fails the kebab pattern                   | schema: properties.uid pattern\n  join-bad-identity      both    identity missing, name empty, or a row non-string | schema: $defs.identity required+types\n  join-cap-overflow      both    identity row or standing.* over cap           | schema: maxLength on those rows\n  join-host-path         both    host-path shape in an identity row or a string pick | schema: $defs.noHostPath\n  join-bad-avatar-type   both    avatar missing or type outside the six        | schema: $defs.avatar required+enum\n  join-body-both         both    avatar carries BOTH ref and package           | schema: $defs.avatar oneOf\n  join-body-none         both    neither body source; ref with no castKey/packageUrl; (1.0) ext.join.body not an object | schema: oneOf + ref.anyOf + joinBody type\n  join-bad-package-fmt   both    package fmt !== 'alive-package-1' (authored or fetched) | schema: $defs.avatar.package.fmt const\n  join-bad-mind-lane     both    lane outside MIND_LANES, or model non-string  | schema: the profile mind enum + mind.model type\n  join-bad-pick          both    capabilities not an object; a null pick; an illegal pick type | schema: $defs.pick anyOf\n  join-bad-cap-id        both    '__proto__', a leading-underscore segment, or a CAP_ID_RE miss | schema: capabilities.propertyNames pattern\n  join-bad-contract      1.0     ext.join.contract !== 'nsv-agent-join/1'      | schema: $defs.join.contract const\n  join-missing           1.0     ext or ext.join absent / not an object        | schema: the profile switch APPLIES join rules only when present; absence is legal for builder docs and is NOT a schema failure\n  join-bad-body-source   1.0     source outside {ref,embed}, or disagreeing with the avatar | schema: joinBody.source enum + joinedProfile source-agreement\n  join-body-type-mismatch both   the body type disagrees with avatar.type (1.0 ext.join.body.type; both: the embedded/fetched package.type) | schema: the six type-agreement branches in each profile\n  join-bad-body-name     both    display name non-string, over 60, or host-path shaped (1.0 ext.join.body.name / 1.1 avatar.name) | schema: maxLength + noHostPath\n  join-bad-pin           both    a body pin absent, malformed, or present where forbidden (1.0 ext.join.body.*Sha256 / 1.1 avatar.ref.integrity, embed: package.integrity.payload) | schema: $defs.refIntegrity + the joinBody if/then\n  join-bad-persona       both    persona seat neither null nor a well-shaped ref/embed (1.0 ext.join.persona / 1.1 mind.persona) | schema: $defs.joinPersona\n  join-bad-integrity-shape both  the whole-document seal block is MALFORMED (1.0 ext.join.integrity / 1.1 top-level integrity) | schema: $defs.joinIntegrity / $defs.docIntegrity. SHAPE ONLY — the VALUE is a verdict, never a reason\n  join-v11-ext-join      1.1+    a 1.1 document carries an ext.join that is not the exact derived projection | schema: v11Profile ext branch\n  join-ext-empty         1.1+    a 1.1 document authors an empty ext:{}        | schema: v11Profile ext minProperties:1\n  join-bad-core          1.1+    core is not an object, or a reserved seat holds a non-object | schema: $defs.core types\n  join-core-unknown-seat 1.1+    core carries a name outside the allocated seats | schema: $defs.core additionalProperties:false\n\nLOAD-TIME — verifyBody(), NOT SCHEMA-CHECKABLE AT ALL. These need the fetched body bytes and a hash recompute; nsv-validate.mjs runs the real implementation to reach them:\n  join-body-unreadable   both    ref-mode package text absent or unparseable\n  join-crypto-unavailable both   no crypto to recompute with — never faked\n  join-body-seal-fail    both    the body's OWN integrity.payload != a fresh recompute\n  join-body-mismatch     both    the body is intact but is NOT the body this mind was joined to — the join pin refuses it\n  join-file-mismatch     both    ref mode: fetched file bytes != the artifact pin\n\nSuites pin these strings; do not rename one without renaming it at every consuming seam.",
      "type": "null"
    },
    "refIntegrity": {
      "type": "object",
      "required": [
        "algo",
        "canon",
        "payload",
        "file"
      ],
      "additionalProperties": false,
      "description": "SPEC 1.1 — THE BODY PINS, NATIVE (OWED-J1). Ref mode only; there is no `ref` in embed mode to hang them on.\n\nThe vocabulary is alive-package-1's own (algo / canon / payload), because the value in `payload` IS that package's integrity.payload. Inventing a second name for one number is how two vocabularies start. `file` is the one addition — the artifact pin, which alive-package-1 has no need of because a package does not reference itself.\n\nBOTH ARE REQUIRED, and validateJoin refuses join-bad-pin on a missing or malformed either. NEITHER VALUE IS SCHEMA-CHECKABLE: verifyBody recomputes both from the fetched body and returns join-body-seal-fail / join-body-mismatch / join-file-mismatch. A document can satisfy every rule here and still be refused at load, which is the difference between a shape and a fact.",
      "properties": {
        "algo": {
          "const": "sha-256"
        },
        "canon": {
          "const": "sorted-keys-json"
        },
        "payload": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$",
          "description": "THE JOIN PIN — sha-256(canonJson(package.payload)); 1.0 called it ext.join.body.packageSha256 and the value is byte-identical. verifyBody requires it to equal BOTH a fresh recompute over the fetched body (else join-body-seal-fail) AND this declared value (else join-body-mismatch). The second comparison is the one that matters: a competent attacker forges a body and recomputes its internal seal so the first passes, and the intake battery proves exactly that (T3). This pin is the last line, and no JSON Schema can stand on it.\n\nIt pins the PAYLOAD, not the file — a benign re-export with a new exportedAt still satisfies it."
        },
        "file": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$",
          "description": "THE ARTIFACT PIN — sha-256 over the package FILE's exact bytes as fetched; 1.0 called it ext.join.body.fileSha256.\n\nWhy both pins exist: `payload` survives a benign re-export, `file` does not. An envelope-only tamper (editing `spec` or `name`, leaving the payload untouched) passes the payload seal AND the join pin, and is caught ONLY here — intake battery T2. Two pins, two distinct attacks."
        }
      }
    },
    "core": {
      "type": "object",
      "additionalProperties": false,
      "description": "SPEC 1.1 — THE RESERVED EXTENSION POINT.\n\nWHY IT IS NOT `ext`. `ext` is provider-namespaced space whose unknown entries a consumer MUST IGNORE. That contract is right for a vendor annotation and wrong for a first-party field, because the consumers obliged to ignore it are precisely the ones that must not. 1.0 put the load-bearing join block there anyway; OWED-J1 is the bill for that. `core` is the seat that does not repeat it: first-party, and unknown names REFUSE (join-core-unknown-seat) rather than ride.\n\nALLOCATED SEATS. Four names are reserved so two lanes cannot collide on one by accident. Proposing a fifth is a spec edit, not a document edit:\n  core.memory  — O24 agent-core-1: what she remembers per agent, and where it lives\n  core.files   — O24 agent-core-1: customer-uploaded documents the agent may use\n  core.prompt  — O24 agent-core-1: the per-agent intro / system prompt text\n  core.tools   — O29 mcp-tools-1: tool declarations that ride the joined document\n\nINTERIORS ARE NOT SPECIFIED BY 1.1. Each seat must be an object (else join-bad-core) and this minor looks no deeper. The owning lane specifies its interior and this file grows a $ref when it does. UNSPECIFIED IS NOT UNPROTECTED: the whole-document seal covers every byte inside a seat, so a field 1.1 cannot read is still one 1.1 can prove was not altered.",
      "properties": {
        "memory": {
          "type": "object",
          "description": "RESERVED — O24 agent-core-1. Interior unspecified in 1.1."
        },
        "files": {
          "type": "object",
          "description": "RESERVED — O24 agent-core-1. Interior unspecified in 1.1."
        },
        "prompt": {
          "type": "object",
          "description": "RESERVED — O24 agent-core-1. Interior unspecified in 1.1."
        },
        "tools": {
          "type": "object",
          "description": "RESERVED — O29 mcp-tools-1. Interior unspecified in 1.1."
        }
      }
    },
    "docIntegrity": {
      "type": [
        "object",
        "null"
      ],
      "required": [
        "algo",
        "canon",
        "doc"
      ],
      "additionalProperties": false,
      "description": "SPEC 1.1 — THE DETACHED WHOLE-DOCUMENT SEAL, at top level. 1.0 carried the identical block at ext.join.integrity; the law is unchanged and only the seat moved.\n\nHOW IT IS COMPUTED — reproduce this exactly or every verification will disagree. sealJoin: (1) validateJoin the document, refusing to seal anything invalid; (2) take the AUTHORED form (de-projected — see $defs.v11Profile on the compatibility projection) and DELETE this field; (3) canonJson the result; (4) sha-256 -> lowercase hex; (5) write it back. DETACHED: the seal is not part of what it covers, which is what makes it recomputable without a chicken-and-egg problem.\n\nWHAT IT COVERS: the entire authored document minus this one field — every top-level key, the whole mind, the whole capabilities map, both body pins, and all of `core`. So a field no rule in this file inspects is still sealed and cannot be injected after the fact.\n\nnull or absent is LEGAL and means UNSEALED. verifySeal returns pass | fail | absent | unavailable — VERDICTS, a different vocabulary from refusal reasons, and the two never mix. NO SCHEMA CAN CHECK THIS VALUE: a one-hex-digit flip leaves a perfectly well-shaped block this subschema accepts and the recompute rejects. That gap is why nsv-validate.mjs exists.",
      "properties": {
        "algo": {
          "const": "sha-256"
        },
        "canon": {
          "const": "sorted-keys-json",
          "description": "Names canonJson, which mirrors alive-package.js byte-for-byte: sorted keys, no whitespace, undefined skipped, non-finite throws."
        },
        "doc": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$",
          "description": "LOWERCASE only. The key is `doc` here and `payload` in alive-package-1: different scope, deliberately different name."
        }
      }
    },
    "v10Profile": {
      "$comment": "Applied when spec minor is 0. Mirrors AGENT_TOP_KEYS_10 and the 1.0 avatar shape.",
      "description": "SPEC 1.0 CLOSURE. The 1.1 seats did not exist under 1.0 and validateJoin refuses them there as join-unknown-key (top level) — nothing is retro-validated and nothing 1.1 added leaks backwards into a document published before it.",
      "type": "object",
      "allOf": [
        {
          "not": {
            "required": [
              "core"
            ]
          }
        },
        {
          "not": {
            "required": [
              "integrity"
            ]
          }
        },
        {
          "properties": {
            "avatar": {
              "allOf": [
                {
                  "not": {
                    "required": [
                      "name"
                    ]
                  }
                },
                {
                  "properties": {
                    "ref": {
                      "not": {
                        "required": [
                          "integrity"
                        ]
                      }
                    }
                  }
                }
              ]
            }
          }
        },
        {
          "properties": {
            "mind": {
              "not": {
                "required": [
                  "persona"
                ]
              }
            }
          }
        }
      ]
    },
    "v11Profile": {
      "$comment": "Applied when spec minor is 1. This is nsv-agent-join.mjs validateJoin()'s 1.1 branch expressed as schema, restricted to what schema can carry.",
      "description": "SPEC 1.1 RULESET.\n\nTHE COMPATIBILITY PROJECTION — read this before concluding a 1.1 document is missing something. validateJoin returns a document in RUNTIME-CANONICAL form: for 1.0 that is the document itself; for 1.1 it is the document PLUS a DERIVED ext.join block, every value of it computed from the native seats. That is how a consumer written against 1.0's runtime shape — the F8 run runtime reads doc.ext.join.body.source and has zero imports, deps.join being its only seam — runs a 1.1 document with not one line changed. THE PROJECTION IS NOT PART OF THE DOCUMENT and is never written to disk; it is deterministic, and de-projection (delete ext.join, then delete ext if it became empty) is its exact inverse. That is why an authored empty ext refuses.\n\nThe projection deliberately OMITS `integrity`. A seal value projected into a scope it was not computed over would be a false artifact; the honest projection of a seal that lives elsewhere is its ABSENCE, and `absent` is a verdict this format already defines as accept-with-honesty. A hand-rolled 1.0 verifier therefore reads absent — never a false pass — while verifySeal reads the real seat and returns the true verdict.\n\nNOT EXPRESSIBLE HERE AT ALL — nsv-validate.mjs runs these against the real implementation: the seal recompute, both body pins, and the load-time type re-check against the FETCHED body.",
      "type": "object",
      "allOf": [
        {
          "$comment": "ext.join is absorbed. An authored one refuses join-v11-ext-join; an authored empty ext refuses join-ext-empty.",
          "if": {
            "required": [
              "ext"
            ]
          },
          "then": {
            "properties": {
              "ext": {
                "type": "object",
                "minProperties": 1,
                "not": {
                  "required": [
                    "join"
                  ]
                }
              }
            }
          }
        },
        {
          "$comment": "avatar.name is the native home of the body display name — required in 1.1.",
          "properties": {
            "avatar": {
              "required": [
                "name"
              ]
            }
          },
          "required": [
            "avatar"
          ]
        },
        {
          "$comment": "REF MODE REQUIRES THE NATIVE PINS. validateJoin refuses join-bad-pin on a missing or malformed avatar.ref.integrity. Embed mode has no ref to pin: the embedded package carries its own integrity block, verifyBody recomputes it, and the whole-document seal binds it in place.",
          "if": {
            "properties": {
              "avatar": {
                "required": [
                  "ref"
                ]
              }
            },
            "required": [
              "avatar"
            ]
          },
          "then": {
            "properties": {
              "avatar": {
                "properties": {
                  "ref": {
                    "required": [
                      "integrity"
                    ]
                  }
                }
              }
            }
          }
        },
        {
          "$comment": "EMBED MODE: the embedded package must carry its own seal, because in 1.1 that block IS the pin. validateJoin refuses join-bad-pin when it is absent or malformed.",
          "if": {
            "properties": {
              "avatar": {
                "required": [
                  "package"
                ]
              }
            },
            "required": [
              "avatar"
            ]
          },
          "then": {
            "properties": {
              "avatar": {
                "properties": {
                  "package": {
                    "required": [
                      "integrity"
                    ],
                    "properties": {
                      "integrity": {
                        "type": "object",
                        "required": [
                          "payload"
                        ],
                        "properties": {
                          "payload": {
                            "type": "string",
                            "pattern": "^[0-9a-f]{64}$"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        {
          "$comment": "MIND LANE ENUM — validateJoin, MIND_LANES. Same rule the 1.0 joined profile carries; a null/absent mind skips it entirely, which is why `required` is not asserted.",
          "if": {
            "required": [
              "mind"
            ],
            "properties": {
              "mind": {
                "type": "object"
              }
            }
          },
          "then": {
            "properties": {
              "mind": {
                "required": [
                  "lane"
                ],
                "properties": {
                  "lane": {
                    "enum": [
                      "local-rules",
                      "stage-realtime"
                    ]
                  }
                }
              }
            }
          }
        },
        {
          "$comment": "BODY TYPE AGREEMENT, embed mode — avatar.package.type must equal avatar.type. JSON Schema cannot compare two values, so this is six explicit branches. In 1.0 this rule ALSO had to police ext.join.body.type against avatar.type; 1.1 derives that value instead of storing it, so half the rule simply stopped being expressible-and-necessary. Re-checked at load against the FETCHED body by verifyBody either way.",
          "allOf": [
            {
              "if": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "type": {
                        "const": "svg"
                      }
                    },
                    "required": [
                      "type",
                      "package"
                    ]
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "package": {
                        "properties": {
                          "type": {
                            "const": "svg"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "type": {
                        "const": "ascii"
                      }
                    },
                    "required": [
                      "type",
                      "package"
                    ]
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "package": {
                        "properties": {
                          "type": {
                            "const": "ascii"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "type": {
                        "const": "sketch-line"
                      }
                    },
                    "required": [
                      "type",
                      "package"
                    ]
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "package": {
                        "properties": {
                          "type": {
                            "const": "sketch-line"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "type": {
                        "const": "hybrid"
                      }
                    },
                    "required": [
                      "type",
                      "package"
                    ]
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "package": {
                        "properties": {
                          "type": {
                            "const": "hybrid"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "type": {
                        "const": "kinetic-typography"
                      }
                    },
                    "required": [
                      "type",
                      "package"
                    ]
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "package": {
                        "properties": {
                          "type": {
                            "const": "kinetic-typography"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              "if": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "type": {
                        "const": "live-vrm"
                      }
                    },
                    "required": [
                      "type",
                      "package"
                    ]
                  }
                },
                "required": [
                  "avatar"
                ]
              },
              "then": {
                "properties": {
                  "avatar": {
                    "properties": {
                      "package": {
                        "properties": {
                          "type": {
                            "const": "live-vrm"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          ]
        }
      ]
    }
  }
}
