Verify manifest.integrity at publish preflight — the framework leg of #11331 (does not discharge the unpack ruling) #13464

Description

@claude

Part-of #11331 · step 2 (framework leg), sized from the #13455 locating read (accepted; full evidence: #13455 (comment)).

⚠️ Scope framing — this card does NOT discharge the #11331 ruling

The ruled enforce leg (re-verification at unpack) has no landing surface in this repo — there is no unpack (ADR-0025 §3.5 steps 4–7 unimplemented). That leg routes to the cloud control plane and is tracked on #11331. This card lands the smaller real framework leg: today os plugin publish uploads artifacts whose bytes may no longer match their own declared manifest.integrity digests, because nothing anywhere reads that map. After this card, the publisher checks its own artifact pre-upload. The spec's promise of re-verification at unpack remains outstanding and must not be marked resolved by this PR.

Task

  1. verifyIntegrity(files, integrity) (~40 lines + a structured rejection envelope) in packages/core/src/security/ beside plugin-artifact-signature.ts (which already declares itself byte-for-byte mirrored by cloud's package-signing.ts — keep that mirroring property in mind: pure, dependency-free, portable). Semantics: for each entry in the integrity map, hash the corresponding in-memory file and compare; refuse on mismatch, missing file for a declared entry, and (report, see decision below) extra files not in the map.
  2. One call site: os plugin publish preflight at packages/cli/src/commands/plugin/publish.ts:84 — the full tree is already in memory (packages/cli/src/utils/osplugin.ts:156) and currently discarded. Verify before upload; refuse the publish on failure with an actionable message.
  3. TSDoc prose corrections (prose only, no shape change): packages/spec/src/kernel/manifest.zod.ts:100-109 and :610-614 — stop claiming the runtime re-verifies at unpack; state what is true (computed at build, self-checked at publish; unpack-time verification is the cloud control plane's obligation, not yet implemented here).
  4. Ledger note prose: packages/spec/liveness/manifest.json integrity row (~:265-269) — statusstays dead (its verdict is a per-repo reader census; the publish self-check makes the CLI a reader, so if check:liveness semantics say a non-test reader flips it, follow the tool's verdict — never hand-edit a status the tool disagrees with). Correct the note prose either way.
  5. Tests: match / single-file mismatch / missing declared entry / extra file / absent map ⇒ permissive pass (the field is .optional()).
  6. Changeset (.changeset/*.md): os plugin publish gains a refusal — patch bump, plain sentence.

Decision mandate (pre-answered, carry in PR description)

Absent integrity map = permissive (publish proceeds, optionally with a one-line notice). Strict-by-default would reject every pre-computeIntegrity artifact and would be a contract change to an .optional() field — that needs its own ruling, not a rider here. Extra-files-not-in-map: refuse (a stale map is exactly the drift this check exists to catch) — if that proves controversial in review, downgrade to warn there, not here.

Error code

Per the precedent at packages/runtime/src/artifact-reference.ts:68-73, a pre-server-bind CLI refusal is NOT registered in ERROR_CODE_LEDGER — keep this card clear of packages/spec/src/api/error-code-ledger.zod.ts.

Gates owed

pnpm --filter @objectstack/cli test · pnpm --filter @objectstack/core test · pnpm --filter @objectstack/spec check:liveness · pnpm --filter @objectstack/spec check:authorable-surface (baseline measured unmoved by the prose edits — keys only, no describe text) · changeset present.

Clause ② (pre-recorded)

  • Path limb fires: the diff touches packages/spec/src/kernel/manifest.zod.ts ⇒ the PR parks as DRAFT with needs:contract-review on both carriers (PR + card). Review chain owns enqueue.
  • Content limb fires: publish gains an accept/reject change ⇒ fable-mandatory dispatch.

Blocked-by: (none)


Generated by Claude Code

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    Verify manifest.integrity at publish preflight — the framework leg of #11331 (does not discharge the unpack ruling) #13464

    Description

    @claude

    Part-of #11331 · step 2 (framework leg), sized from the #13455 locating read (accepted; full evidence: #13455 (comment)).

    ⚠️ Scope framing — this card does NOT discharge the #11331 ruling

    The ruled enforce leg (re-verification at unpack) has no landing surface in this repo — there is no unpack (ADR-0025 §3.5 steps 4–7 unimplemented). That leg routes to the cloud control plane and is tracked on #11331. This card lands the smaller real framework leg: today os plugin publish uploads artifacts whose bytes may no longer match their own declared manifest.integrity digests, because nothing anywhere reads that map. After this card, the publisher checks its own artifact pre-upload. The spec's promise of re-verification at unpack remains outstanding and must not be marked resolved by this PR.

    Task

    1. verifyIntegrity(files, integrity) (~40 lines + a structured rejection envelope) in packages/core/src/security/ beside plugin-artifact-signature.ts (which already declares itself byte-for-byte mirrored by cloud's package-signing.ts — keep that mirroring property in mind: pure, dependency-free, portable). Semantics: for each entry in the integrity map, hash the corresponding in-memory file and compare; refuse on mismatch, missing file for a declared entry, and (report, see decision below) extra files not in the map.
    2. One call site: os plugin publish preflight at packages/cli/src/commands/plugin/publish.ts:84 — the full tree is already in memory (packages/cli/src/utils/osplugin.ts:156) and currently discarded. Verify before upload; refuse the publish on failure with an actionable message.
    3. TSDoc prose corrections (prose only, no shape change): packages/spec/src/kernel/manifest.zod.ts:100-109 and :610-614 — stop claiming the runtime re-verifies at unpack; state what is true (computed at build, self-checked at publish; unpack-time verification is the cloud control plane's obligation, not yet implemented here).
    4. Ledger note prose: packages/spec/liveness/manifest.json integrity row (~:265-269) — statusstays dead (its verdict is a per-repo reader census; the publish self-check makes the CLI a reader, so if check:liveness semantics say a non-test reader flips it, follow the tool's verdict — never hand-edit a status the tool disagrees with). Correct the note prose either way.
    5. Tests: match / single-file mismatch / missing declared entry / extra file / absent map ⇒ permissive pass (the field is .optional()).
    6. Changeset (.changeset/*.md): os plugin publish gains a refusal — patch bump, plain sentence.

    Decision mandate (pre-answered, carry in PR description)

    Absent integrity map = permissive (publish proceeds, optionally with a one-line notice). Strict-by-default would reject every pre-computeIntegrity artifact and would be a contract change to an .optional() field — that needs its own ruling, not a rider here. Extra-files-not-in-map: refuse (a stale map is exactly the drift this check exists to catch) — if that proves controversial in review, downgrade to warn there, not here.

    Error code

    Per the precedent at packages/runtime/src/artifact-reference.ts:68-73, a pre-server-bind CLI refusal is NOT registered in ERROR_CODE_LEDGER — keep this card clear of packages/spec/src/api/error-code-ledger.zod.ts.

    Gates owed

    pnpm --filter @objectstack/cli test · pnpm --filter @objectstack/core test · pnpm --filter @objectstack/spec check:liveness · pnpm --filter @objectstack/spec check:authorable-surface (baseline measured unmoved by the prose edits — keys only, no describe text) · changeset present.

    Clause ② (pre-recorded)

    • Path limb fires: the diff touches packages/spec/src/kernel/manifest.zod.ts ⇒ the PR parks as DRAFT with needs:contract-review on both carriers (PR + card). Review chain owns enqueue.
    • Content limb fires: publish gains an accept/reject change ⇒ fable-mandatory dispatch.

    Blocked-by: (none)


    Generated by Claude Code

    Metadata

    Metadata

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      Verify manifest.integrity at publish preflight — the framework leg of #11331 (does not discharge the unpack ruling) #13464

      Description

      @claude

      Part-of #11331 · step 2 (framework leg), sized from the #13455 locating read (accepted; full evidence: #13455 (comment)).

      ⚠️ Scope framing — this card does NOT discharge the #11331 ruling

      The ruled enforce leg (re-verification at unpack) has no landing surface in this repo — there is no unpack (ADR-0025 §3.5 steps 4–7 unimplemented). That leg routes to the cloud control plane and is tracked on #11331. This card lands the smaller real framework leg: today os plugin publish uploads artifacts whose bytes may no longer match their own declared manifest.integrity digests, because nothing anywhere reads that map. After this card, the publisher checks its own artifact pre-upload. The spec's promise of re-verification at unpack remains outstanding and must not be marked resolved by this PR.

      Task

      1. verifyIntegrity(files, integrity) (~40 lines + a structured rejection envelope) in packages/core/src/security/ beside plugin-artifact-signature.ts (which already declares itself byte-for-byte mirrored by cloud's package-signing.ts — keep that mirroring property in mind: pure, dependency-free, portable). Semantics: for each entry in the integrity map, hash the corresponding in-memory file and compare; refuse on mismatch, missing file for a declared entry, and (report, see decision below) extra files not in the map.
      2. One call site: os plugin publish preflight at packages/cli/src/commands/plugin/publish.ts:84 — the full tree is already in memory (packages/cli/src/utils/osplugin.ts:156) and currently discarded. Verify before upload; refuse the publish on failure with an actionable message.
      3. TSDoc prose corrections (prose only, no shape change): packages/spec/src/kernel/manifest.zod.ts:100-109 and :610-614 — stop claiming the runtime re-verifies at unpack; state what is true (computed at build, self-checked at publish; unpack-time verification is the cloud control plane's obligation, not yet implemented here).
      4. Ledger note prose: packages/spec/liveness/manifest.json integrity row (~:265-269) — statusstays dead (its verdict is a per-repo reader census; the publish self-check makes the CLI a reader, so if check:liveness semantics say a non-test reader flips it, follow the tool's verdict — never hand-edit a status the tool disagrees with). Correct the note prose either way.
      5. Tests: match / single-file mismatch / missing declared entry / extra file / absent map ⇒ permissive pass (the field is .optional()).
      6. Changeset (.changeset/*.md): os plugin publish gains a refusal — patch bump, plain sentence.

      Decision mandate (pre-answered, carry in PR description)

      Absent integrity map = permissive (publish proceeds, optionally with a one-line notice). Strict-by-default would reject every pre-computeIntegrity artifact and would be a contract change to an .optional() field — that needs its own ruling, not a rider here. Extra-files-not-in-map: refuse (a stale map is exactly the drift this check exists to catch) — if that proves controversial in review, downgrade to warn there, not here.

      Error code

      Per the precedent at packages/runtime/src/artifact-reference.ts:68-73, a pre-server-bind CLI refusal is NOT registered in ERROR_CODE_LEDGER — keep this card clear of packages/spec/src/api/error-code-ledger.zod.ts.

      Gates owed

      pnpm --filter @objectstack/cli test · pnpm --filter @objectstack/core test · pnpm --filter @objectstack/spec check:liveness · pnpm --filter @objectstack/spec check:authorable-surface (baseline measured unmoved by the prose edits — keys only, no describe text) · changeset present.

      Clause ② (pre-recorded)

      • Path limb fires: the diff touches packages/spec/src/kernel/manifest.zod.ts ⇒ the PR parks as DRAFT with needs:contract-review on both carriers (PR + card). Review chain owns enqueue.
      • Content limb fires: publish gains an accept/reject change ⇒ fable-mandatory dispatch.

      Blocked-by: (none)


      Generated by Claude Code

      Metadata

      Metadata

      Type

      No type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        Verify manifest.integrity at publish preflight — the framework leg of #11331 (does not discharge the unpack ruling) #13464

        Description

        @claude

        Part-of #11331 · step 2 (framework leg), sized from the #13455 locating read (accepted; full evidence: #13455 (comment)).

        ⚠️ Scope framing — this card does NOT discharge the #11331 ruling

        The ruled enforce leg (re-verification at unpack) has no landing surface in this repo — there is no unpack (ADR-0025 §3.5 steps 4–7 unimplemented). That leg routes to the cloud control plane and is tracked on #11331. This card lands the smaller real framework leg: today os plugin publish uploads artifacts whose bytes may no longer match their own declared manifest.integrity digests, because nothing anywhere reads that map. After this card, the publisher checks its own artifact pre-upload. The spec's promise of re-verification at unpack remains outstanding and must not be marked resolved by this PR.

        Task

        1. verifyIntegrity(files, integrity) (~40 lines + a structured rejection envelope) in packages/core/src/security/ beside plugin-artifact-signature.ts (which already declares itself byte-for-byte mirrored by cloud's package-signing.ts — keep that mirroring property in mind: pure, dependency-free, portable). Semantics: for each entry in the integrity map, hash the corresponding in-memory file and compare; refuse on mismatch, missing file for a declared entry, and (report, see decision below) extra files not in the map.
        2. One call site: os plugin publish preflight at packages/cli/src/commands/plugin/publish.ts:84 — the full tree is already in memory (packages/cli/src/utils/osplugin.ts:156) and currently discarded. Verify before upload; refuse the publish on failure with an actionable message.
        3. TSDoc prose corrections (prose only, no shape change): packages/spec/src/kernel/manifest.zod.ts:100-109 and :610-614 — stop claiming the runtime re-verifies at unpack; state what is true (computed at build, self-checked at publish; unpack-time verification is the cloud control plane's obligation, not yet implemented here).
        4. Ledger note prose: packages/spec/liveness/manifest.json integrity row (~:265-269) — statusstays dead (its verdict is a per-repo reader census; the publish self-check makes the CLI a reader, so if check:liveness semantics say a non-test reader flips it, follow the tool's verdict — never hand-edit a status the tool disagrees with). Correct the note prose either way.
        5. Tests: match / single-file mismatch / missing declared entry / extra file / absent map ⇒ permissive pass (the field is .optional()).
        6. Changeset (.changeset/*.md): os plugin publish gains a refusal — patch bump, plain sentence.

        Decision mandate (pre-answered, carry in PR description)

        Absent integrity map = permissive (publish proceeds, optionally with a one-line notice). Strict-by-default would reject every pre-computeIntegrity artifact and would be a contract change to an .optional() field — that needs its own ruling, not a rider here. Extra-files-not-in-map: refuse (a stale map is exactly the drift this check exists to catch) — if that proves controversial in review, downgrade to warn there, not here.

        Error code

        Per the precedent at packages/runtime/src/artifact-reference.ts:68-73, a pre-server-bind CLI refusal is NOT registered in ERROR_CODE_LEDGER — keep this card clear of packages/spec/src/api/error-code-ledger.zod.ts.

        Gates owed

        pnpm --filter @objectstack/cli test · pnpm --filter @objectstack/core test · pnpm --filter @objectstack/spec check:liveness · pnpm --filter @objectstack/spec check:authorable-surface (baseline measured unmoved by the prose edits — keys only, no describe text) · changeset present.

        Clause ② (pre-recorded)

        • Path limb fires: the diff touches packages/spec/src/kernel/manifest.zod.ts ⇒ the PR parks as DRAFT with needs:contract-review on both carriers (PR + card). Review chain owns enqueue.
        • Content limb fires: publish gains an accept/reject change ⇒ fable-mandatory dispatch.

        Blocked-by: (none)


        Generated by Claude Code

        Metadata

        Metadata

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          Verify manifest.integrity at publish preflight — the framework leg of #11331 (does not discharge the unpack ruling) #13464

          Description

          @claude

          Part-of #11331 · step 2 (framework leg), sized from the #13455 locating read (accepted; full evidence: #13455 (comment)).

          ⚠️ Scope framing — this card does NOT discharge the #11331 ruling

          The ruled enforce leg (re-verification at unpack) has no landing surface in this repo — there is no unpack (ADR-0025 §3.5 steps 4–7 unimplemented). That leg routes to the cloud control plane and is tracked on #11331. This card lands the smaller real framework leg: today os plugin publish uploads artifacts whose bytes may no longer match their own declared manifest.integrity digests, because nothing anywhere reads that map. After this card, the publisher checks its own artifact pre-upload. The spec's promise of re-verification at unpack remains outstanding and must not be marked resolved by this PR.

          Task

          1. verifyIntegrity(files, integrity) (~40 lines + a structured rejection envelope) in packages/core/src/security/ beside plugin-artifact-signature.ts (which already declares itself byte-for-byte mirrored by cloud's package-signing.ts — keep that mirroring property in mind: pure, dependency-free, portable). Semantics: for each entry in the integrity map, hash the corresponding in-memory file and compare; refuse on mismatch, missing file for a declared entry, and (report, see decision below) extra files not in the map.
          2. One call site: os plugin publish preflight at packages/cli/src/commands/plugin/publish.ts:84 — the full tree is already in memory (packages/cli/src/utils/osplugin.ts:156) and currently discarded. Verify before upload; refuse the publish on failure with an actionable message.
          3. TSDoc prose corrections (prose only, no shape change): packages/spec/src/kernel/manifest.zod.ts:100-109 and :610-614 — stop claiming the runtime re-verifies at unpack; state what is true (computed at build, self-checked at publish; unpack-time verification is the cloud control plane's obligation, not yet implemented here).
          4. Ledger note prose: packages/spec/liveness/manifest.json integrity row (~:265-269) — statusstays dead (its verdict is a per-repo reader census; the publish self-check makes the CLI a reader, so if check:liveness semantics say a non-test reader flips it, follow the tool's verdict — never hand-edit a status the tool disagrees with). Correct the note prose either way.
          5. Tests: match / single-file mismatch / missing declared entry / extra file / absent map ⇒ permissive pass (the field is .optional()).
          6. Changeset (.changeset/*.md): os plugin publish gains a refusal — patch bump, plain sentence.

          Decision mandate (pre-answered, carry in PR description)

          Absent integrity map = permissive (publish proceeds, optionally with a one-line notice). Strict-by-default would reject every pre-computeIntegrity artifact and would be a contract change to an .optional() field — that needs its own ruling, not a rider here. Extra-files-not-in-map: refuse (a stale map is exactly the drift this check exists to catch) — if that proves controversial in review, downgrade to warn there, not here.

          Error code

          Per the precedent at packages/runtime/src/artifact-reference.ts:68-73, a pre-server-bind CLI refusal is NOT registered in ERROR_CODE_LEDGER — keep this card clear of packages/spec/src/api/error-code-ledger.zod.ts.

          Gates owed

          pnpm --filter @objectstack/cli test · pnpm --filter @objectstack/core test · pnpm --filter @objectstack/spec check:liveness · pnpm --filter @objectstack/spec check:authorable-surface (baseline measured unmoved by the prose edits — keys only, no describe text) · changeset present.

          Clause ② (pre-recorded)

          • Path limb fires: the diff touches packages/spec/src/kernel/manifest.zod.ts ⇒ the PR parks as DRAFT with needs:contract-review on both carriers (PR + card). Review chain owns enqueue.
          • Content limb fires: publish gains an accept/reject change ⇒ fable-mandatory dispatch.

          Blocked-by: (none)


          Generated by Claude Code

          Metadata

          Metadata

          Type

          No type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            Verify manifest.integrity at publish preflight — the framework leg of #11331 (does not discharge the unpack ruling) #13464

            Description

            @claude

            Part-of #11331 · step 2 (framework leg), sized from the #13455 locating read (accepted; full evidence: #13455 (comment)).

            ⚠️ Scope framing — this card does NOT discharge the #11331 ruling

            The ruled enforce leg (re-verification at unpack) has no landing surface in this repo — there is no unpack (ADR-0025 §3.5 steps 4–7 unimplemented). That leg routes to the cloud control plane and is tracked on #11331. This card lands the smaller real framework leg: today os plugin publish uploads artifacts whose bytes may no longer match their own declared manifest.integrity digests, because nothing anywhere reads that map. After this card, the publisher checks its own artifact pre-upload. The spec's promise of re-verification at unpack remains outstanding and must not be marked resolved by this PR.

            Task

            1. verifyIntegrity(files, integrity) (~40 lines + a structured rejection envelope) in packages/core/src/security/ beside plugin-artifact-signature.ts (which already declares itself byte-for-byte mirrored by cloud's package-signing.ts — keep that mirroring property in mind: pure, dependency-free, portable). Semantics: for each entry in the integrity map, hash the corresponding in-memory file and compare; refuse on mismatch, missing file for a declared entry, and (report, see decision below) extra files not in the map.
            2. One call site: os plugin publish preflight at packages/cli/src/commands/plugin/publish.ts:84 — the full tree is already in memory (packages/cli/src/utils/osplugin.ts:156) and currently discarded. Verify before upload; refuse the publish on failure with an actionable message.
            3. TSDoc prose corrections (prose only, no shape change): packages/spec/src/kernel/manifest.zod.ts:100-109 and :610-614 — stop claiming the runtime re-verifies at unpack; state what is true (computed at build, self-checked at publish; unpack-time verification is the cloud control plane's obligation, not yet implemented here).
            4. Ledger note prose: packages/spec/liveness/manifest.json integrity row (~:265-269) — statusstays dead (its verdict is a per-repo reader census; the publish self-check makes the CLI a reader, so if check:liveness semantics say a non-test reader flips it, follow the tool's verdict — never hand-edit a status the tool disagrees with). Correct the note prose either way.
            5. Tests: match / single-file mismatch / missing declared entry / extra file / absent map ⇒ permissive pass (the field is .optional()).
            6. Changeset (.changeset/*.md): os plugin publish gains a refusal — patch bump, plain sentence.

            Decision mandate (pre-answered, carry in PR description)

            Absent integrity map = permissive (publish proceeds, optionally with a one-line notice). Strict-by-default would reject every pre-computeIntegrity artifact and would be a contract change to an .optional() field — that needs its own ruling, not a rider here. Extra-files-not-in-map: refuse (a stale map is exactly the drift this check exists to catch) — if that proves controversial in review, downgrade to warn there, not here.

            Error code

            Per the precedent at packages/runtime/src/artifact-reference.ts:68-73, a pre-server-bind CLI refusal is NOT registered in ERROR_CODE_LEDGER — keep this card clear of packages/spec/src/api/error-code-ledger.zod.ts.

            Gates owed

            pnpm --filter @objectstack/cli test · pnpm --filter @objectstack/core test · pnpm --filter @objectstack/spec check:liveness · pnpm --filter @objectstack/spec check:authorable-surface (baseline measured unmoved by the prose edits — keys only, no describe text) · changeset present.

            Clause ② (pre-recorded)

            • Path limb fires: the diff touches packages/spec/src/kernel/manifest.zod.ts ⇒ the PR parks as DRAFT with needs:contract-review on both carriers (PR + card). Review chain owns enqueue.
            • Content limb fires: publish gains an accept/reject change ⇒ fable-mandatory dispatch.

            Blocked-by: (none)


            Generated by Claude Code

            Metadata

            Metadata

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Verify manifest.integrity at publish preflight — the framework leg of #11331 (does not discharge the unpack ruling) #13464

              Description

              @claude

              Part-of #11331 · step 2 (framework leg), sized from the #13455 locating read (accepted; full evidence: #13455 (comment)).

              ⚠️ Scope framing — this card does NOT discharge the #11331 ruling

              The ruled enforce leg (re-verification at unpack) has no landing surface in this repo — there is no unpack (ADR-0025 §3.5 steps 4–7 unimplemented). That leg routes to the cloud control plane and is tracked on #11331. This card lands the smaller real framework leg: today os plugin publish uploads artifacts whose bytes may no longer match their own declared manifest.integrity digests, because nothing anywhere reads that map. After this card, the publisher checks its own artifact pre-upload. The spec's promise of re-verification at unpack remains outstanding and must not be marked resolved by this PR.

              Task

              1. verifyIntegrity(files, integrity) (~40 lines + a structured rejection envelope) in packages/core/src/security/ beside plugin-artifact-signature.ts (which already declares itself byte-for-byte mirrored by cloud's package-signing.ts — keep that mirroring property in mind: pure, dependency-free, portable). Semantics: for each entry in the integrity map, hash the corresponding in-memory file and compare; refuse on mismatch, missing file for a declared entry, and (report, see decision below) extra files not in the map.
              2. One call site: os plugin publish preflight at packages/cli/src/commands/plugin/publish.ts:84 — the full tree is already in memory (packages/cli/src/utils/osplugin.ts:156) and currently discarded. Verify before upload; refuse the publish on failure with an actionable message.
              3. TSDoc prose corrections (prose only, no shape change): packages/spec/src/kernel/manifest.zod.ts:100-109 and :610-614 — stop claiming the runtime re-verifies at unpack; state what is true (computed at build, self-checked at publish; unpack-time verification is the cloud control plane's obligation, not yet implemented here).
              4. Ledger note prose: packages/spec/liveness/manifest.json integrity row (~:265-269) — statusstays dead (its verdict is a per-repo reader census; the publish self-check makes the CLI a reader, so if check:liveness semantics say a non-test reader flips it, follow the tool's verdict — never hand-edit a status the tool disagrees with). Correct the note prose either way.
              5. Tests: match / single-file mismatch / missing declared entry / extra file / absent map ⇒ permissive pass (the field is .optional()).
              6. Changeset (.changeset/*.md): os plugin publish gains a refusal — patch bump, plain sentence.

              Decision mandate (pre-answered, carry in PR description)

              Absent integrity map = permissive (publish proceeds, optionally with a one-line notice). Strict-by-default would reject every pre-computeIntegrity artifact and would be a contract change to an .optional() field — that needs its own ruling, not a rider here. Extra-files-not-in-map: refuse (a stale map is exactly the drift this check exists to catch) — if that proves controversial in review, downgrade to warn there, not here.

              Error code

              Per the precedent at packages/runtime/src/artifact-reference.ts:68-73, a pre-server-bind CLI refusal is NOT registered in ERROR_CODE_LEDGER — keep this card clear of packages/spec/src/api/error-code-ledger.zod.ts.

              Gates owed

              pnpm --filter @objectstack/cli test · pnpm --filter @objectstack/core test · pnpm --filter @objectstack/spec check:liveness · pnpm --filter @objectstack/spec check:authorable-surface (baseline measured unmoved by the prose edits — keys only, no describe text) · changeset present.

              Clause ② (pre-recorded)

              • Path limb fires: the diff touches packages/spec/src/kernel/manifest.zod.ts ⇒ the PR parks as DRAFT with needs:contract-review on both carriers (PR + card). Review chain owns enqueue.
              • Content limb fires: publish gains an accept/reject change ⇒ fable-mandatory dispatch.

              Blocked-by: (none)


              Generated by Claude Code

              Metadata

              Metadata

              Type

              No type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                Verify manifest.integrity at publish preflight — the framework leg of #11331 (does not discharge the unpack ruling) #13464

                Description

                @claude

                Part-of #11331 · step 2 (framework leg), sized from the #13455 locating read (accepted; full evidence: #13455 (comment)).

                ⚠️ Scope framing — this card does NOT discharge the #11331 ruling

                The ruled enforce leg (re-verification at unpack) has no landing surface in this repo — there is no unpack (ADR-0025 §3.5 steps 4–7 unimplemented). That leg routes to the cloud control plane and is tracked on #11331. This card lands the smaller real framework leg: today os plugin publish uploads artifacts whose bytes may no longer match their own declared manifest.integrity digests, because nothing anywhere reads that map. After this card, the publisher checks its own artifact pre-upload. The spec's promise of re-verification at unpack remains outstanding and must not be marked resolved by this PR.

                Task

                1. verifyIntegrity(files, integrity) (~40 lines + a structured rejection envelope) in packages/core/src/security/ beside plugin-artifact-signature.ts (which already declares itself byte-for-byte mirrored by cloud's package-signing.ts — keep that mirroring property in mind: pure, dependency-free, portable). Semantics: for each entry in the integrity map, hash the corresponding in-memory file and compare; refuse on mismatch, missing file for a declared entry, and (report, see decision below) extra files not in the map.
                2. One call site: os plugin publish preflight at packages/cli/src/commands/plugin/publish.ts:84 — the full tree is already in memory (packages/cli/src/utils/osplugin.ts:156) and currently discarded. Verify before upload; refuse the publish on failure with an actionable message.
                3. TSDoc prose corrections (prose only, no shape change): packages/spec/src/kernel/manifest.zod.ts:100-109 and :610-614 — stop claiming the runtime re-verifies at unpack; state what is true (computed at build, self-checked at publish; unpack-time verification is the cloud control plane's obligation, not yet implemented here).
                4. Ledger note prose: packages/spec/liveness/manifest.json integrity row (~:265-269) — statusstays dead (its verdict is a per-repo reader census; the publish self-check makes the CLI a reader, so if check:liveness semantics say a non-test reader flips it, follow the tool's verdict — never hand-edit a status the tool disagrees with). Correct the note prose either way.
                5. Tests: match / single-file mismatch / missing declared entry / extra file / absent map ⇒ permissive pass (the field is .optional()).
                6. Changeset (.changeset/*.md): os plugin publish gains a refusal — patch bump, plain sentence.

                Decision mandate (pre-answered, carry in PR description)

                Absent integrity map = permissive (publish proceeds, optionally with a one-line notice). Strict-by-default would reject every pre-computeIntegrity artifact and would be a contract change to an .optional() field — that needs its own ruling, not a rider here. Extra-files-not-in-map: refuse (a stale map is exactly the drift this check exists to catch) — if that proves controversial in review, downgrade to warn there, not here.

                Error code

                Per the precedent at packages/runtime/src/artifact-reference.ts:68-73, a pre-server-bind CLI refusal is NOT registered in ERROR_CODE_LEDGER — keep this card clear of packages/spec/src/api/error-code-ledger.zod.ts.

                Gates owed

                pnpm --filter @objectstack/cli test · pnpm --filter @objectstack/core test · pnpm --filter @objectstack/spec check:liveness · pnpm --filter @objectstack/spec check:authorable-surface (baseline measured unmoved by the prose edits — keys only, no describe text) · changeset present.

                Clause ② (pre-recorded)

                • Path limb fires: the diff touches packages/spec/src/kernel/manifest.zod.ts ⇒ the PR parks as DRAFT with needs:contract-review on both carriers (PR + card). Review chain owns enqueue.
                • Content limb fires: publish gains an accept/reject change ⇒ fable-mandatory dispatch.

                Blocked-by: (none)


                Generated by Claude Code

                Metadata

                Metadata

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions