Add content-addressed components plan to sx-docs
7-phase plan: canonical serialization, CID computation, component manifests, IPFS storage & resolution cascade, security model (purity verification, content verification, eval limits, trust tiers), wire format integration with prefetch system, and federated sharing via AP component registry actors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -114,7 +114,9 @@
|
||||
(dict :label "SX-Activity" :href "/plans/sx-activity"
|
||||
:summary "A new web built on SX — executable content, shared components, parsers, and logic on IPFS, provenance on Bitcoin, all running within your own security context.")
|
||||
(dict :label "Predictive Prefetching" :href "/plans/predictive-prefetch"
|
||||
:summary "Prefetch missing component definitions before the user clicks — hover a link, fetch its deps, navigate client-side.")))
|
||||
:summary "Prefetch missing component definitions before the user clicks — hover a link, fetch its deps, navigate client-side.")
|
||||
(dict :label "Content-Addressed Components" :href "/plans/content-addressed-components"
|
||||
:summary "Components identified by CID, stored on IPFS, fetched from anywhere. Canonical serialization, content verification, federated sharing.")))
|
||||
|
||||
(define bootstrappers-nav-items (list
|
||||
(dict :label "Overview" :href "/bootstrappers/")
|
||||
|
||||
416
sx/sx/plans.sx
416
sx/sx/plans.sx
@@ -594,6 +594,422 @@
|
||||
(td :class "px-3 py-2 text-stone-700" "Content addressing — shared with component CIDs")
|
||||
(td :class "px-3 py-2 text-stone-600" "2, 3"))))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Content-Addressed Components
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plan-content-addressed-components-content ()
|
||||
(~doc-page :title "Content-Addressed Components"
|
||||
|
||||
(~doc-section :title "The Premise" :id "premise"
|
||||
(p "SX components are pure functions. Boundary enforcement guarantees it — a component cannot call IO primitives, make network requests, access cookies, or touch the filesystem. " (code "Component.is_pure") " is a structural property, verified at registration time by scanning the transitive closure of IO references via " (code "deps.sx") ".")
|
||||
(p "Pure functions have a remarkable property: " (strong "their identity is their content.") " Two components that produce the same serialized form are the same component, regardless of who wrote them or where they're hosted. This means we can content-address them — compute a cryptographic hash of the canonical serialized form, and that hash " (em "is") " the component's identity.")
|
||||
(p "Content addressing turns components into shared infrastructure. Define " (code "~card") " once, pin it to IPFS, and every SX application on the planet can use it by CID. No package registry, no npm install, no version conflicts. The CID " (em "is") " the version. The hash " (em "is") " the trust. Boundary enforcement " (em "is") " the sandbox.")
|
||||
(p "This plan details how to get from the current name-based, per-server component model to a content-addressed, globally-shared one."))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Current State
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(~doc-section :title "Current State" :id "current-state"
|
||||
(p "What already exists and what's missing.")
|
||||
|
||||
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Capability")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Where")))
|
||||
(tbody
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Deterministic serialization")
|
||||
(td :class "px-3 py-2 text-stone-700" "Partial — " (code "serialize(body, pretty=True)") " from AST, but no canonical normalization")
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "parser.py:296-427"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Component identity")
|
||||
(td :class "px-3 py-2 text-stone-700" "By name (" (code "~card") ") — names are mutable, server-local")
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "types.py:157-180"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Bundle hashing")
|
||||
(td :class "px-3 py-2 text-stone-700" "SHA256 of all defs concatenated — per-bundle, not per-component")
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "jinja_bridge.py:60-86"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Purity verification")
|
||||
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Complete") " — " (code "is_pure") " via transitive IO ref analysis")
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "deps.sx, boundary.py"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Dependency graph")
|
||||
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Complete") " — " (code "Component.deps") " transitive closure")
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "deps.sx"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "IPFS infrastructure")
|
||||
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Exists") " — IPFSPin model, async upload tasks, CID tracking")
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "models/federation.py, artdag/l1/tasks/"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Client component caching")
|
||||
(td :class "px-3 py-2 text-stone-700" "Hash-based localStorage — but keyed by bundle hash, not individual CID")
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "boot.sx, helpers.py"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Content-addressed components")
|
||||
(td :class "px-3 py-2 text-stone-700" (span :class "text-red-700 font-medium" "Not yet") " — no per-component CID, no IPFS resolution")
|
||||
(td :class "px-3 py-2 text-stone-600" "—"))))))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Canonical Serialization
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(~doc-section :title "Phase 1: Canonical Serialization" :id "canonical-serialization"
|
||||
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
|
||||
(p :class "text-violet-900 font-medium" "The foundation")
|
||||
(p :class "text-violet-800" "Same component must always produce the same bytes, regardless of original formatting, whitespace, or comment placement. Without this, content addressing is meaningless."))
|
||||
|
||||
(~doc-subsection :title "The Problem"
|
||||
(p "Currently " (code "serialize(body, pretty=True)") " produces readable SX source from the parsed AST. But serialization isn't fully canonical — it depends on the internal representation order, and there's no normalization pass. Two semantically identical components formatted differently would produce different hashes.")
|
||||
(p "We need a " (strong "canonical form") " that strips all variance:"))
|
||||
|
||||
(~doc-subsection :title "Canonical Form Rules"
|
||||
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
|
||||
(li (strong "Strip comments.") " Comments are parsing artifacts, not part of the AST. The serializer already ignores them (it works from the parsed tree), but any future comment-preserving parser must not affect canonical output.")
|
||||
(li (strong "Normalize whitespace.") " Single space between tokens, newline before each top-level form in a body. No trailing whitespace. No blank lines.")
|
||||
(li (strong "Sort keyword arguments alphabetically.") " In component calls: " (code "(~card :class \"x\" :title \"y\")") " not " (code "(~card :title \"y\" :class \"x\")") ". In dict literals: " (code "{:a 1 :b 2}") " not " (code "{:b 2 :a 1}") ".")
|
||||
(li (strong "Normalize string escapes.") " Use " (code "\\n") " not literal newlines in strings. Escape only what must be escaped.")
|
||||
(li (strong "Normalize numbers.") " " (code "1.0") " not " (code "1.00") " or " (code "1.") ". " (code "42") " not " (code "042") ".")
|
||||
(li (strong "Include the full definition form.") " Hash the complete " (code "(defcomp ~name (params) body)") ", not just the body. The name and parameter signature are part of the component's identity.")))
|
||||
|
||||
(~doc-subsection :title "Implementation"
|
||||
(p "New spec function in a " (code "canonical.sx") " module:")
|
||||
(~doc-code :code (highlight "(define canonical-serialize\n (fn (node)\n ;; Produce a canonical s-expression string from an AST node.\n ;; Deterministic: same AST always produces same output.\n ;; Used for CID computation — NOT for human-readable output.\n (case (type-of node)\n \"list\"\n (str \"(\" (join \" \" (map canonical-serialize node)) \")\")\n \"dict\"\n (let ((sorted-keys (sort (keys node))))\n (str \"{\" (join \" \"\n (map (fn (k)\n (str \":\" k \" \" (canonical-serialize (get node k))))\n sorted-keys)) \"}\"))\n \"string\"\n (str '\"' (escape-canonical node) '\"')\n \"number\"\n (canonical-number node)\n \"symbol\"\n (symbol-name node)\n \"keyword\"\n (str \":\" (keyword-name node))\n \"boolean\"\n (if node \"true\" \"false\")\n \"nil\"\n \"nil\")))" "lisp"))
|
||||
(p "This function must be bootstrapped to both Python and JS — the server computes CIDs at registration time, the client verifies them on fetch.")
|
||||
(p "The canonical serializer is distinct from " (code "serialize()") " for display. " (code "serialize(pretty=True)") " remains for human-readable output. " (code "canonical-serialize") " is for hashing only.")))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; CID Computation
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(~doc-section :title "Phase 2: CID Computation" :id "cid-computation"
|
||||
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
|
||||
(p :class "text-violet-900 font-medium" "What it enables")
|
||||
(p :class "text-violet-800" "Every component gets a stable, unique content identifier. Same source → same CID, always. Different source → different CID, always."))
|
||||
|
||||
(~doc-subsection :title "CID Format"
|
||||
(p "Use " (a :href "https://github.com/multiformats/cid" :class "text-violet-700 underline" "CIDv1") " with:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li (strong "Hash function:") " SHA3-256 (already used by artdag for content addressing)")
|
||||
(li (strong "Codec:") " raw (the content is the canonical SX source bytes, not a DAG-PB wrapper)")
|
||||
(li (strong "Base encoding:") " base32lower for URL-safe representation (" (code "bafy...") " prefix)"))
|
||||
(~doc-code :code (highlight ";; CID computation pipeline\n(define component-cid\n (fn (component)\n ;; 1. Reconstruct full defcomp form\n ;; 2. Canonical serialize\n ;; 3. SHA3-256 hash\n ;; 4. Wrap as CIDv1\n (let ((source (canonical-serialize\n (list 'defcomp\n (symbol (str \"~\" (component-name component)))\n (component-params-list component)\n (component-body component)))))\n (cid-v1 :sha3-256 :raw (encode-utf8 source)))))" "lisp")))
|
||||
|
||||
(~doc-subsection :title "Where CIDs Live"
|
||||
(p "Each " (code "Component") " object gains a " (code "cid") " field, computed at registration time:")
|
||||
(~doc-code :code (highlight ";; types.py extension\n@dataclass\nclass Component:\n name: str\n params: list[str]\n has_children: bool\n body: Any\n closure: dict[str, Any]\n css_classes: set[str]\n deps: set[str] # by name\n io_refs: set[str]\n cid: str | None = None # computed after registration\n dep_cids: dict[str, str] | None = None # name → CID" "python"))
|
||||
(p "After " (code "compute_all_deps()") " runs, a new " (code "compute_all_cids()") " pass fills in CIDs for every component. Dependency CIDs are also recorded — when a component references " (code "~card") ", we store both the name and card's CID."))
|
||||
|
||||
(~doc-subsection :title "CID Stability"
|
||||
(p "A component's CID changes when and only when its " (strong "semantics") " change:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li "Reformatting the " (code ".sx") " source file → same AST → same canonical form → " (strong "same CID"))
|
||||
(li "Adding a comment → stripped by parser → same AST → " (strong "same CID"))
|
||||
(li "Changing a class name in the body → different AST → " (strong "different CID"))
|
||||
(li "Renaming the component → different defcomp form → " (strong "different CID") " (name is part of identity)"))
|
||||
(p "This means CIDs are " (em "immutable versions") ". There's no " (code "~card@1.2.3") " — there's " (code "~card") " at CID " (code "bafy...abc") " and " (code "~card") " at CID " (code "bafy...def") ". The name is a human-friendly alias; the CID is the truth.")))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Component Manifest
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(~doc-section :title "Phase 3: Component Manifest" :id "manifest"
|
||||
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
|
||||
(p :class "text-violet-900 font-medium" "What it enables")
|
||||
(p :class "text-violet-800" "Metadata that travels with a CID — what a component needs, what it provides, whether it's safe to run. Enough information to resolve, validate, and render without fetching the source first."))
|
||||
|
||||
(~doc-subsection :title "Manifest Structure"
|
||||
(~doc-code :code (highlight ";; Component manifest — published alongside the source\n(SxComponent\n :name \"~product-card\"\n :cid \"bafy...productcard\"\n :source-bytes 847\n :params (:title :price :image-url)\n :has-children true\n :pure true\n :deps (\n {:name \"~card\" :cid \"bafy...card\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"}\n {:name \"~lazy-image\" :cid \"bafy...lazyimg\"})\n :css-atoms (:border :rounded :p-4 :text-sm :font-bold\n :text-green-700 :line-through :text-stone-400)\n :author \"https://rose-ash.com/apps/market\"\n :published \"2026-03-06T14:30:00Z\")" "lisp"))
|
||||
(p "Key fields:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li (code ":cid") " — content address of the canonical serialized source")
|
||||
(li (code ":deps") " — dependency CIDs, not just names. A consumer can recursively resolve the entire tree by CID without name ambiguity")
|
||||
(li (code ":pure") " — pre-computed purity flag. The consumer " (em "re-verifies") " this after fetching (never trust the manifest alone), but it enables fast rejection of IO-dependent components before downloading")
|
||||
(li (code ":css-atoms") " — CSSX class names the component uses. The consumer can pre-resolve CSS rules without parsing the source")
|
||||
(li (code ":params") " — parameter signature for tooling, documentation, IDE support")
|
||||
(li (code ":author") " — who published this. AP actor URL, verifiable via HTTP Signatures")))
|
||||
|
||||
(~doc-subsection :title "Manifest CID"
|
||||
(p "The manifest itself is content-addressed. But the manifest CID is " (em "not") " the component CID — they're separate objects. The component CID is derived from the source alone (pure content). The manifest CID includes metadata that could change (author, publication date) without changing the component.")
|
||||
(p "Resolution order: manifest CID → manifest → component CID → component source. Or shortcut: component CID → source directly, if you already know what you need.")))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; IPFS Storage & Resolution
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(~doc-section :title "Phase 4: IPFS Storage & Resolution" :id "ipfs"
|
||||
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
|
||||
(p :class "text-violet-900 font-medium" "What it enables")
|
||||
(p :class "text-violet-800" "Components live on IPFS. Any browser can fetch them by CID. No origin server needed. No CDN. No DNS. The content network IS the distribution network."))
|
||||
|
||||
(~doc-subsection :title "Server-Side: Publication"
|
||||
(p "On component registration (startup or hot-reload), the server:")
|
||||
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
|
||||
(li "Computes canonical form and CID")
|
||||
(li "Checks " (code "IPFSPin") " — if CID already pinned, skip (content can't have changed)")
|
||||
(li "Pins canonical source to IPFS (async Celery task, same pattern as artdag)")
|
||||
(li "Creates/updates " (code "IPFSPin") " record with " (code "pin_type=\"component\""))
|
||||
(li "Publishes manifest to IPFS (separate CID)")
|
||||
(li "Optionally announces via AP outbox for federated discovery"))
|
||||
(~doc-code :code (highlight ";; IPFSPin usage for components\nIPFSPin(\n content_hash=\"sha3-256:abcdef...\",\n ipfs_cid=\"bafy...productcard\",\n pin_type=\"component\",\n source_type=\"market\", # which service defined it\n metadata={\n \"name\": \"~product-card\",\n \"manifest_cid\": \"bafy...manifest\",\n \"deps\": [\"bafy...card\", \"bafy...pricetag\"],\n \"pure\": True\n }\n)" "python")))
|
||||
|
||||
(~doc-subsection :title "Client-Side: Resolution"
|
||||
(p "New spec module " (code "resolve.sx") " — the client-side component resolution pipeline:")
|
||||
(~doc-code :code (highlight "(define resolve-component-by-cid\n (fn (cid callback)\n ;; Resolution cascade:\n ;; 1. Check component env (already loaded?)\n ;; 2. Check localStorage (keyed by CID = cache-forever)\n ;; 3. Check origin server (/sx/components?cid=bafy...)\n ;; 4. Fetch from IPFS gateway\n ;; 5. Verify hash matches CID\n ;; 6. Parse, validate purity, register, callback\n (let ((cached (local-storage-get (str \"sx-cid:\" cid))))\n (if cached\n (do\n (register-component-source cached)\n (callback true))\n (fetch-component-by-cid cid\n (fn (source)\n (if (verify-cid cid source)\n (do\n (local-storage-set (str \"sx-cid:\" cid) source)\n (register-component-source source)\n (callback true))\n (do\n (log-warn (str \"sx:cid verification failed \" cid))\n (callback false)))))))))" "lisp"))
|
||||
(p "The cache-forever semantics are the key insight: because CIDs are content-addressed, a cached component " (strong "can never be stale") ". If the source changes, it gets a new CID. Old CIDs remain valid forever. There is no cache invalidation problem."))
|
||||
|
||||
(~doc-subsection :title "Resolution Cascade"
|
||||
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Layer")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Lookup")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Latency")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "When")))
|
||||
(tbody
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "1. Component env")
|
||||
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "(env-has? env cid)")
|
||||
(td :class "px-3 py-2 text-stone-600" "0ms")
|
||||
(td :class "px-3 py-2 text-stone-600" "Already loaded this session"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "2. localStorage")
|
||||
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "localStorage[\"sx-cid:\" + cid]")
|
||||
(td :class "px-3 py-2 text-stone-600" "<1ms")
|
||||
(td :class "px-3 py-2 text-stone-600" "Previously fetched, persists across sessions"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "3. Origin server")
|
||||
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "GET /sx/components?cid=bafy...")
|
||||
(td :class "px-3 py-2 text-stone-600" "~20ms")
|
||||
(td :class "px-3 py-2 text-stone-600" "Same-origin component, not yet cached"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "4. IPFS gateway")
|
||||
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "GET https://gateway/ipfs/{cid}")
|
||||
(td :class "px-3 py-2 text-stone-600" "~200ms")
|
||||
(td :class "px-3 py-2 text-stone-600" "Foreign component, federated content"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "5. Local IPFS node")
|
||||
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "ipfs cat {cid}")
|
||||
(td :class "px-3 py-2 text-stone-600" "~5ms")
|
||||
(td :class "px-3 py-2 text-stone-600" "User runs own IPFS node (power users)")))))
|
||||
(p "Layer 5 is optional — checked between 2 and 3 if " (code "window.ipfs") " or a local gateway is detected. For most users, layers 1-4 cover all cases.")))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Security Model
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(~doc-section :title "Phase 5: Security Model" :id "security"
|
||||
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
|
||||
(p :class "text-violet-900 font-medium" "The hard part")
|
||||
(p :class "text-violet-800" "Loading code from the network is the web's original sin. Content-addressed components are safe because of three structural guarantees — not policies, not trust, not sandboxes that can be escaped."))
|
||||
|
||||
(~doc-subsection :title "Guarantee 1: Purity is Structural"
|
||||
(p "SX boundary enforcement isn't a runtime sandbox — it's a registration-time structural check. When a component is loaded from IPFS and parsed, " (code "compute_all_io_refs()") " walks its entire AST and transitive dependencies. If " (em "any") " node references an IO primitive, the component is classified as IO-dependent and " (strong "rejected for untrusted registration."))
|
||||
(p "This means the evaluator literally doesn't have IO primitives in scope when running an IPFS-loaded component. It's not that we catch IO calls — the names don't resolve. There's nothing to catch.")
|
||||
(~doc-code :code (highlight "(define register-untrusted-component\n (fn (source origin)\n ;; Parse the defcomp from source\n ;; Run compute-all-io-refs on the parsed component\n ;; If io_refs is non-empty → REJECT\n ;; If pure → register in env with :origin metadata\n (let ((comp (parse-component source)))\n (if (not (component-pure? comp))\n (do\n (log-warn (str \"sx:reject IO component from \" origin))\n nil)\n (do\n (register-component comp)\n (log-info (str \"sx:registered \" (component-name comp)\n \" from \" origin))\n comp)))))" "lisp")))
|
||||
|
||||
(~doc-subsection :title "Guarantee 2: Content Verification"
|
||||
(p "The CID IS the hash. When you fetch " (code "bafy...abc") " from any source — IPFS gateway, origin server, peer — you hash the response and compare. If it doesn't match, you reject it. No MITM attack can alter the content without changing the CID.")
|
||||
(p "This is stronger than HTTPS. HTTPS trusts the certificate authority, the DNS resolver, and the server operator. Content addressing trusts " (em "mathematics") ". The hash either matches or it doesn't."))
|
||||
|
||||
(~doc-subsection :title "Guarantee 3: Evaluation Limits"
|
||||
(p "Pure doesn't mean terminating. A component could contain an infinite loop or exponential recursion. SX evaluators enforce step limits:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li (strong "Max eval steps:") " configurable per context. Untrusted components get a lower limit than local ones.")
|
||||
(li (strong "Max recursion depth:") " prevents stack exhaustion.")
|
||||
(li (strong "Max output size:") " prevents a component from producing gigabytes of DOM nodes."))
|
||||
(p "Exceeding any limit halts evaluation and returns an error node. The worst case is wasted CPU — never data exfiltration, never unauthorized IO."))
|
||||
|
||||
(~doc-subsection :title "Trust Tiers"
|
||||
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Tier")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Source")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Allowed")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Eval limits")))
|
||||
(tbody
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-semibold text-stone-800" "Local")
|
||||
(td :class "px-3 py-2 text-stone-700" "Server's own " (code ".sx") " files")
|
||||
(td :class "px-3 py-2 text-stone-700" "Pure + IO primitives + page helpers")
|
||||
(td :class "px-3 py-2 text-stone-600" "None (trusted)"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-semibold text-stone-800" "Followed")
|
||||
(td :class "px-3 py-2 text-stone-700" "Components from followed AP actors")
|
||||
(td :class "px-3 py-2 text-stone-700" "Pure only (IO rejected)")
|
||||
(td :class "px-3 py-2 text-stone-600" "Standard limits"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-semibold text-stone-800" "Federated")
|
||||
(td :class "px-3 py-2 text-stone-700" "Components from any IPFS source")
|
||||
(td :class "px-3 py-2 text-stone-700" "Pure only (IO rejected)")
|
||||
(td :class "px-3 py-2 text-stone-600" "Strict limits"))))))
|
||||
|
||||
(~doc-subsection :title "What Can Go Wrong"
|
||||
(p "Honest accounting of the attack surface:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li (strong "Visual spoofing:") " A malicious component could render UI that looks like a login form. Mitigation: untrusted components render inside a visually distinct container with origin attribution.")
|
||||
(li (strong "CSS abuse:") " A component's CSS atoms could interfere with page layout. Mitigation: scoped CSS — untrusted components' classes are namespaced.")
|
||||
(li (strong "Resource exhaustion:") " A component could be expensive to evaluate. Mitigation: step limits, timeout, lazy rendering for off-screen components.")
|
||||
(li (strong "Privacy leak via CSS:") " Background-image URLs could phone home. Mitigation: CSP restrictions on untrusted component rendering contexts.")
|
||||
(li (strong "Dependency confusion:") " A malicious manifest could claim deps that are different components with the same name. Mitigation: deps are referenced by CID, not name. Name is informational only."))))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Wire Format & Prefetch Integration
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(~doc-section :title "Phase 6: Wire Format & Prefetch Integration" :id "wire-format"
|
||||
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
|
||||
(p :class "text-violet-900 font-medium" "What it enables")
|
||||
(p :class "text-violet-800" "Pages and SX responses reference components by CID. The prefetch system resolves them from the most efficient source. Components become location-independent."))
|
||||
|
||||
(~doc-subsection :title "CID References in Page Registry"
|
||||
(p "The page registry (shipped to the client as " (code "<script type=\"text/sx-pages\">") ") currently lists deps by name. Extend to include CIDs:")
|
||||
(~doc-code :code (highlight "{:name \"docs-page\" :path \"/docs/<slug>\"\n :auth \"public\" :has-data false\n :deps ({:name \"~essay-foo\" :cid \"bafy...essay\"}\n {:name \"~doc-code\" :cid \"bafy...doccode\"})\n :content \"(case slug ...)\" :closure {}}" "lisp"))
|
||||
(p "The " (a :href "/plans/predictive-prefetch" :class "text-violet-700 underline" "predictive prefetch system") " uses these CIDs to fetch components from the resolution cascade rather than only from the origin server's " (code "/sx/components") " endpoint."))
|
||||
|
||||
(~doc-subsection :title "SX Response Component Headers"
|
||||
(p "Currently, " (code "SX-Components") " header lists loaded component names. Extend to support CIDs:")
|
||||
(~doc-code :code (highlight "Request:\nSX-Components: ~card:bafy...card,~nav:bafy...nav\n\nResponse:\nSX-Component-CIDs: ~essay-foo:bafy...essay,~doc-code:bafy...doccode\n\n;; Response body only includes defs the client doesn't have\n(defcomp ~essay-foo ...)" "http"))
|
||||
(p "The client can then verify received components match their declared CIDs. If the origin server is compromised, CID verification catches the tampered response."))
|
||||
|
||||
(~doc-subsection :title "Federated Content"
|
||||
(p "When an ActivityPub activity arrives with SX content, it declares component requirements by CID:")
|
||||
(~doc-code :code (highlight "(Create\n :actor \"https://other-instance.com/users/bob\"\n :object (Note\n :content (~product-card :title \"Bob's Widget\" :price 29.99)\n :requires (list\n {:name \"~product-card\" :cid \"bafy...prodcard\"}\n {:name \"~price-tag\" :cid \"bafy...pricetag\"})))" "lisp"))
|
||||
(p "The receiving browser resolves required components through the cascade. If Bob's instance is down, the components are still fetchable from IPFS. The content is self-describing and self-resolving.")))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Component Sharing & Discovery
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(~doc-section :title "Phase 7: Sharing & Discovery" :id "sharing"
|
||||
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
|
||||
(p :class "text-violet-900 font-medium" "What it enables")
|
||||
(p :class "text-violet-800" "Servers publish component collections via AP. Other servers follow them. Like npm, but federated, content-addressed, and structurally safe."))
|
||||
|
||||
(~doc-subsection :title "Component Registry as AP Actor"
|
||||
(p "Each server exposes a component registry actor:")
|
||||
(~doc-code :code (highlight "(Service\n :id \"https://rose-ash.com/sx-registry\"\n :type \"SxComponentRegistry\"\n :name \"Rose Ash Components\"\n :outbox \"https://rose-ash.com/sx-registry/outbox\"\n :followers \"https://rose-ash.com/sx-registry/followers\")" "lisp"))
|
||||
(p "Follow the registry to receive component updates. The outbox is a chronological feed of Create/Update/Delete activities for components. 'Update' means a new CID for the same name — consumers decide whether to adopt it."))
|
||||
|
||||
(~doc-subsection :title "Discovery Protocol"
|
||||
(p "Webfinger-style lookup for components by name:")
|
||||
(~doc-code :code (highlight "GET /.well-known/sx-component?name=~product-card\n\n{\n \"name\": \"~product-card\",\n \"cid\": \"bafy...prodcard\",\n \"manifest_cid\": \"bafy...manifest\",\n \"gateway\": \"https://rose-ash.com/ipfs/\",\n \"author\": \"https://rose-ash.com/apps/market\"\n}" "http"))
|
||||
(p "This is an optional convenience — any consumer that knows the CID can skip discovery and fetch directly from IPFS. Discovery answers the question: " (em "\"what's the current version of ~product-card on rose-ash.com?\""))
|
||||
)
|
||||
|
||||
(~doc-subsection :title "Name Resolution"
|
||||
(p "Names are human-friendly aliases for CIDs. The same name on different servers can refer to different components (different CIDs). Conflict resolution is simple:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li (strong "Local wins:") " If the server defines " (code "~card") ", that definition takes precedence over any federated " (code "~card") ".")
|
||||
(li (strong "CID pinning:") " When referencing a federated component, pin the CID. " (code "(:name \"~card\" :cid \"bafy...abc\")") " — the name is informational, the CID is authoritative.")
|
||||
(li (strong "No global namespace:") " There is no \"npm\" that owns " (code "~card") ". Names are scoped to the server that defines them. CIDs are global."))))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Spec modules
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(~doc-section :title "Spec Modules" :id "spec-modules"
|
||||
(p "Per the SX host architecture principle, all content-addressing logic is specced in " (code ".sx") " files and bootstrapped:")
|
||||
|
||||
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Spec module")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Functions")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Platform obligations")))
|
||||
(tbody
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "canonical.sx")
|
||||
(td :class "px-3 py-2 text-stone-700" (code "canonical-serialize") ", " (code "canonical-number") ", " (code "escape-canonical"))
|
||||
(td :class "px-3 py-2 text-stone-600" "None — pure string operations"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "cid.sx")
|
||||
(td :class "px-3 py-2 text-stone-700" (code "component-cid") ", " (code "verify-cid") ", " (code "cid-to-string") ", " (code "parse-cid"))
|
||||
(td :class "px-3 py-2 text-stone-600" (code "sha3-256") ", " (code "encode-base32") ", " (code "encode-utf8")))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "resolve.sx")
|
||||
(td :class "px-3 py-2 text-stone-700" (code "resolve-component-by-cid") ", " (code "resolve-deps-recursive") ", " (code "register-untrusted-component"))
|
||||
(td :class "px-3 py-2 text-stone-600" (code "local-storage-get/set") ", " (code "fetch-cid") ", " (code "register-component-source"))))))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Critical files
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(~doc-section :title "Critical Files" :id "critical-files"
|
||||
(div :class "overflow-x-auto rounded border border-stone-200"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "File")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Role")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Phase")))
|
||||
(tbody
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/canonical.sx")
|
||||
(td :class "px-3 py-2 text-stone-700" "Canonical serialization spec (new)")
|
||||
(td :class "px-3 py-2 text-stone-600" "1"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/cid.sx")
|
||||
(td :class "px-3 py-2 text-stone-700" "CID computation and verification spec (new)")
|
||||
(td :class "px-3 py-2 text-stone-600" "2"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/types.py")
|
||||
(td :class "px-3 py-2 text-stone-700" "Add " (code "cid") " and " (code "dep_cids") " to Component")
|
||||
(td :class "px-3 py-2 text-stone-600" "2"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/jinja_bridge.py")
|
||||
(td :class "px-3 py-2 text-stone-700" "Add " (code "compute_all_cids()") " to registration lifecycle")
|
||||
(td :class "px-3 py-2 text-stone-600" "2"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/models/federation.py")
|
||||
(td :class "px-3 py-2 text-stone-700" "IPFSPin records for component CIDs")
|
||||
(td :class "px-3 py-2 text-stone-600" "4"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/resolve.sx")
|
||||
(td :class "px-3 py-2 text-stone-700" "Client-side CID resolution cascade (new)")
|
||||
(td :class "px-3 py-2 text-stone-600" "4"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/helpers.py")
|
||||
(td :class "px-3 py-2 text-stone-700" "CIDs in page registry, " (code "/sx/components?cid=") " endpoint")
|
||||
(td :class "px-3 py-2 text-stone-600" "6"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/orchestration.sx")
|
||||
(td :class "px-3 py-2 text-stone-700" "CID-aware prefetch in resolution cascade")
|
||||
(td :class "px-3 py-2 text-stone-600" "6"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/infrastructure/activitypub.py")
|
||||
(td :class "px-3 py-2 text-stone-700" "Component registry actor, Webfinger extension")
|
||||
(td :class "px-3 py-2 text-stone-600" "7"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/boundary.py")
|
||||
(td :class "px-3 py-2 text-stone-700" "Trust tier enforcement for untrusted components")
|
||||
(td :class "px-3 py-2 text-stone-600" "5"))))))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Relationship
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(~doc-section :title "Relationships" :id "relationships"
|
||||
(p "This plan is the foundation for several other plans and roadmaps:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li (a :href "/plans/sx-activity" :class "text-violet-700 underline" "SX-Activity") " Phase 2 (content-addressed components on IPFS) is a summary of this plan. This plan supersedes that section with full detail.")
|
||||
(li (a :href "/plans/predictive-prefetch" :class "text-violet-700 underline" "Predictive prefetching") " gains CID-based resolution — the " (code "/sx/components") " endpoint and IPFS gateway become alternative resolution paths in the prefetch cascade.")
|
||||
(li (a :href "/plans/isomorphic-architecture" :class "text-violet-700 underline" "Isomorphic architecture") " Phase 1 (component distribution) is enhanced — CIDs make per-page bundles verifiable and cross-server shareable.")
|
||||
(li "The SX-Activity vision of " (strong "serverless applications on IPFS") " depends entirely on this plan. Without content-addressed components, applications can't be pinned to IPFS as self-contained artifacts."))
|
||||
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
|
||||
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "deps.sx (complete), boundary enforcement (complete), IPFS infrastructure (exists in artdag, needs wiring to web platform).")))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Predictive Component Prefetching
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -478,4 +478,5 @@
|
||||
"reader-macros" (~plan-reader-macros-content)
|
||||
"sx-activity" (~plan-sx-activity-content)
|
||||
"predictive-prefetch" (~plan-predictive-prefetch-content)
|
||||
"content-addressed-components" (~plan-content-addressed-components-content)
|
||||
:else (~plans-index-content)))
|
||||
|
||||
Reference in New Issue
Block a user