{
  "name": "SharePoint /_html",
  "description": "How AI agents author HTML for SharePoint, choose whether it lives in OneDrive/a document library or Site Pages, and save it as the current user.",
  "important": "Anyone can read this page anonymously. Writing requires the current user’s delegated SharePoint/Graph credentials and writes as THAT user. CREATE makes a new item that inherits the destination’s permissions; OVERWRITE retains the target item’s existing permissions and sharing links. Either can be reader-visible immediately. Uploading creates no new sharing link or anonymous access. Publishing a page in Site Pages is a separate explicit step.",
  "destinations": {
    "instruction": "Choose where the HTML should live before writing it. Both destinations use the authoring contract; Site Pages also requires the sitePages creation sequence.",
    "choices": [
      {
        "name": "OneDrive or a document library",
        "description": "Save a standalone .html file here to open it in the OneUp HTML viewer."
      },
      {
        "name": "Site Pages",
        "description": "Save the .html file in the current web’s designated Site Pages library when it should render with SharePoint site page chrome or may become the site homepage. This is a distinct publishing mechanism, not a file upload: use only the sitepages/htmlpages REST calls below. A drive/driveItem upload, createUploadSession, a generic connector file-upload action (e.g. WorkIQ), or any other document-library upload path does not create a Site Page — at best it places an unrelated file in a document library, and at worst leaves a zero-byte placeholder with no HTML content."
      }
    ]
  },
  "codegenContract": {
    "renderer": "OneUp HTML viewer (sandboxed <iframe sandbox=\"allow-scripts\">)",
    "note": "The CSP + sandbox are strict and FAIL SILENTLY: non-compliant HTML renders blank/partial with no error.",
    "sandbox": "allow-scripts",
    "contentSecurityPolicy": "default-src 'none';\nscript-src 'unsafe-inline' 'unsafe-eval';\nstyle-src 'unsafe-inline';\nimg-src data: blob:;\nfont-src data: blob:;\nmedia-src data: blob:;\nconnect-src 'none';\nworker-src 'none';\nobject-src 'none';\nbase-uri 'none';\nform-action 'none';",
    "wrapperAddsDirective": "frame-src 'none'",
    "rules": [
      "No caller-controlled external URLs: no <script src>, <link href>, <img src=https://…>, @import, url(https://…), or remote fonts. Curated libraries are the only exception: declare allowlisted library names without versions in a ka-lib-manifest; the host selects the currently supported version, verifies it, and injects it inline. Any supplied version is ignored.",
      "Choose curated libraries by intent, even when the user does not name one: D3.js 7.9.0 for custom SVG/data visualizations, topojson-client 3.1.0 with world-countries-110m (world-atlas 2.0.2) or us-counties-10m (us-atlas 3.0.1) for maps, @floating-ui/core 1.7.3 or @floating-ui/dom 1.7.4 for anchored tooltips/popovers, and lucide 1.17.0 for SVG icons. FloatingUIDOM includes its core dependency; a separate core declaration is not required. Use topojson.feature to convert worldCountries110m.objects.countries/land or usCounties10m.objects.counties/states/nation to GeoJSON for d3.geoPath. Use inline or host-provided LiveData data, never d3.json/d3.csv or another network loader; d3.csvParse can parse supplied CSV text. Declare only used IDs in one ka-lib-manifest before all authored executable scripts, at most six entries total. Honor no-library/vanilla JavaScript/static-image preferences and avoid libraries for simple static pages or KPI/table-only dashboards.",
      "All authored script must be inline <script> blocks. No module imports, no import(), no <script src>.",
      "All styling must be inline <style> or style=\"…\" attributes. No <link rel=\"stylesheet\">.",
      "Host theming is opt-in: when requested, add <meta name=\"ms-sp-host-theme\" content=\"live\"> to the head (output.html/create_file useSiteTheme:true does this). Use Fluent CSS variables such as var(--colorBrandBackground, #0078d4), var(--colorNeutralForeground1, #242424), var(--colorNeutralBackground1, #fff), and var(--fontFamily, \"Segoe UI\") with generated fallbacks. The containing host, including embed, supplies runtime-only tokens on open and explicit refresh; do not fetch the file-owning site theme, persist host values, redefine host tokens, or migrate ordinary files or legacy snapshots. Outside the viewer or without a host theme, generated styling remains.",
      "No network APIs at runtime (fetch, XMLHttpRequest, WebSocket, sendBeacon, EventSource). For SharePoint file OR list data at view time, use the LiveData manifest pattern (a file item, or a kind:\"list\" item).",
      "No storage primitives (localStorage, sessionStorage, indexedDB, document.cookie) — opaque-origin sandbox. For cross-reload state, set location.hash directly (e.g. location.hash = \"#slide=3\"); do NOT use history.pushState/replaceState — the sandboxed document’s base URL is inherited from the wrapper’s blob: origin, so any url argument throws SecurityError.",
      "No top-level navigation, popups, forms submission, workers, or plugins. Sandbox is allow-scripts only; alert/confirm/prompt are no-ops.",
      "Inline on*= handlers are supported (rewritten to nonced scripts) but are an XSS sink if they template non-author-controlled data — use addEventListener for computed/user/fetched data.",
      "Save the file as UTF-8 with a BOM (the ODSP file-creation API does this automatically)."
    ],
    "selfCheck": [
      "Verify before publishing (the top failure is \"renders blank with no error\"):",
      "No external resources: no <script src>, <link>, @import, url(https:...), remote fonts/CDNs.",
      "No runtime network: no fetch/XMLHttpRequest/WebSocket/sendBeacon/EventSource.",
      "No storage: no localStorage/sessionStorage/indexedDB/document.cookie.",
      "Untrusted/LiveData values rendered via textContent/createElement, never innerHTML.",
      "If using LiveData: every manifest item has spItemUrl AND fileName, and the code keys results by a value derived from the manifest (not a duplicated string)."
    ],
    "liveData": {
      "purpose": "The only sanctioned way to read SharePoint file content at view time (runtime fetch/XHR/WebSocket are blocked). The host prefetches each declared item server-side with the viewing user’s credentials and injects window.__LD_RESULTS__ before any iframe script runs.",
      "manifestTag": "<script type=\"application/json\" class=\"ka-livedata-manifest\">[{ \"spItemUrl\": \"https://{host}/_api/v2.0/drives/{driveId}/items/{itemId}\", \"fileName\": \"data.csv\" }]</script>",
      "manifestNotes": "Multiple manifest blocks are allowed; the host unions and de-dupes them by spItemUrl.",
      "manifestItemFields": {
        "kind": "Optional. Omit or \"file\" for a file item (default); \"list\" for a SharePoint list item (see listItemFields).",
        "spItemUrl": "REQUIRED. The driveItem URL of the file; MUST contain /drives/{driveId}/items/{itemId}. Only driveId+itemId are read; the URL host/origin is ignored and rebuilt from the viewer session (a manifest cannot point at another tenant to leak the token).",
        "fileName": "REQUIRED in practice. File name WITH extension; the host picks the decoder (csv/xlsx-sheets/text) from this extension. Without it the file is decoded as plain text (an xlsx becomes unreadable, a csv is not parsed as csv).",
        "filePath": "Optional. Server-relative path; extension fallback when fileName is absent.",
        "uniqueId": "Optional. SharePoint item GUID, forwarded for attribution.",
        "label": "Optional. Display label passed through to your code."
      },
      "listItemFields": {
        "_note": "For a kind:\"list\" item the host renders a SharePoint LIST server-side (the same engine the Lists UI uses), flattens every column to a display string, and injects it as format:\"rows\". spItemUrl is a SYNTHETIC key \"list:{webUrl}/{listId}\" (dedupe + __LD_RESULTS__ key, never fetched); the fetch identity is webUrl + listId.",
        "kind": "REQUIRED. Must be \"list\".",
        "spItemUrl": "REQUIRED. Synthetic key \"list:{webUrl}/{listId}\"; also the __LD_RESULTS__ key.",
        "webUrl": "REQUIRED. Absolute web URL of the site the list lives on. Host-validated against the tenant multi-geo allowlist before any credentialed fetch (the trust boundary).",
        "listId": "REQUIRED. The list GUID.",
        "listServerRelativeUrl": "Optional. Server-relative list URL; preferred fetch identity when known.",
        "viewId": "Optional. Render a specific view’s columns.",
        "select": "Optional. Array of INTERNAL column names to fetch (never display names); they become the result headers verbatim.",
        "top": "Optional. Row-count hint; the host clamps it to its own ceiling.",
        "label": "Optional. Display label passed through to your code."
      },
      "howToGetListIdentity": "To fill webUrl/listId/select as the signed-in user. The site (web) URL is the list URL up to (not including) \"/Lists/\". SharePoint REST (one hop): GET {webUrl}/_api/web/lists/getbytitle('{ListDisplayName}')?$select=Id&$expand=RootFolder for the list GUID (Id) + RootFolder/ServerRelativeUrl, and GET {webUrl}/_api/web/lists/getbytitle('{ListDisplayName}')/fields?$select=InternalName,Title,TypeAsString&$filter=Hidden eq false for the columns (use InternalName in select; it can differ from the Title - e.g. Title \"City\" with InternalName \"Ciity\"). Add header Accept: application/json;odata=nometadata. Docs: https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/working-with-lists-and-list-items-with-rest . Microsoft Graph equivalent: GET /sites/{hostname}:/{server-relative-path}?$select=id then GET /sites/{site-id}/lists/{list-id}/columns (name = internal name) and GET /sites/{site-id}/lists/{list-id}/items?$expand=fields. Docs: https://learn.microsoft.com/en-us/graph/api/list-list-columns?view=graph-rest-1.0",
      "howToGetSpItemUrl": "Resolve the file once as the signed-in user: GET /shares/{u!token}/driveItem?$select=id,name,file,parentReference (u!token = \"u!\" + unpadded base64url of the file/sharing URL) and read parentReference.driveId + id (and name/file for the extension); or GET /me/drive/root:/path/file.csv?$select=id,name,file,parentReference. Then build https://{host}/_api/v2.0/drives/{driveId}/items/{itemId} (or the graph.microsoft.com/v1.0 equivalent). GOTCHAS: (1) set fileName from the RESOLVED name/extension, NOT from the sharing-URL prefix - :x:/:w:/:p: indicates the Office app, not the file type (an :x: link is often a real .csv). (2) If you download the file to inspect it, GET .../items/{itemId}/content returns a 302 to a pre-authenticated URL; follow redirects (fetch does automatically; curl needs -L).",
      "resultsGlobal": "window.__LD_RESULTS__",
      "resultShape": {
        "_note": "window.__LD_RESULTS__ is an object keyed by the EXACT spItemUrl you declared. Each value has these fields:",
        "spItemUrl": "string (echo of the key)",
        "format": "\"csv\" | \"text\" | \"xlsx-sheets\" (files; decoder derived from the fileName extension) | \"rows\" (SharePoint lists)",
        "content": "string - the decoded content on SUCCESS (see contentByFormat)",
        "metadata": "{ fileName?, extension? }",
        "error": "string - present on FAILURE instead of content",
        "errorCode": "one of: invalid-request | parse-failed | fetch-failed | decode-failed | access-denied | not-found | protected",
        "warning": "string - optional non-blocking caution; content is still present (best-effort)"
      },
      "readingRules": [
        "Key by the spItemUrl - derive it from the injected map (Object.keys(window.__LD_RESULTS__)[0] for a single item, or read the manifest <script> JSON) rather than duplicating the manifest string in code.",
        "If window.__LD_RESULTS__ or your entry is missing, you are not inside the viewer (the page is also opened directly while authoring) - render a placeholder, do not crash.",
        "If entry.error is set, show it (access-denied is sometimes reported as not-found).",
        "Otherwise parse entry.content per entry.format."
      ],
      "contentByFormat": {
        "csv": "Raw CSV text exactly as stored. Parse it yourself (split lines, split on commas, handle quoted fields).",
        "text": "Raw file text.",
        "xlsx-sheets": "All sheets as text; each sheet is wrapped as <sheet name=\"NAME\"> ... </sheet> with the sheet CSV between the tags (or the literal \"(empty sheet)\"), and blocks are separated by a blank line. Split on the <sheet ...>...</sheet> blocks to get per-sheet CSV.",
        "rows": "SharePoint LIST data. content is a JSON string { \"headers\": string[], \"rows\": string[][] } already flattened to display strings host-side (headers are the INTERNAL column names; each rows[i] is aligned to headers). JSON.parse(content) - no CSV parsing needed."
      },
      "limits": {
        "itemLimit": 32,
        "itemLimitBehavior": "Union of all manifests. Exceeding it drops ALL results (fail-closed) - keep manifests small (5 or fewer files typical).",
        "maxItemBytes": 52428800,
        "maxItemBytesHuman": "50 MiB",
        "maxItemBytesBehavior": "An item with no Content-Length or over the cap is refused (errorCode fetch-failed).",
        "maxListRows": 100000,
        "maxListRowsBehavior": "SharePoint list items only. A list larger than this is truncated to the cap and STILL renders, with a non-blocking warning (not an error). Files are bounded by maxItemBytes instead.",
        "caching": "Fetches bypass the HTTP cache, so a refresh always reflects the latest saved file."
      },
      "formats": [
        "text",
        "csv",
        "xlsx-sheets",
        "rows"
      ],
      "refresh": {
        "message": "parent.postMessage({ type: \"ka-html-viewer-refresh\" }, \"*\")",
        "debounceMs": 5000,
        "behavior": "On an accepted refresh the host re-prefetches and RELOADS your iframe, so the page re-runs with fresh window.__LD_RESULTS__. There is NO inbound \"updated\" message; do not wait for one."
      },
      "safety": "LiveData content is UNTRUSTED: never inject it via innerHTML or into on*= handlers (XSS). Build DOM with textContent/createElement, or escape. Runtime network APIs stay blocked; never put secrets in the page."
    },
    "externalLibraries": {
      "manifestTag": "<script type=\"application/json\" class=\"ka-lib-manifest\">[\"chart.js\"]</script>",
      "available": [
        {
          "id": "babylon",
          "globals": [
            "BABYLON"
          ]
        },
        {
          "id": "chart.js",
          "globals": [
            "Chart"
          ]
        },
        {
          "id": "d3",
          "globals": [
            "d3"
          ]
        },
        {
          "id": "topojson-client",
          "globals": [
            "topojson"
          ]
        },
        {
          "id": "world-countries-110m",
          "globals": [
            "worldCountries110m"
          ]
        },
        {
          "id": "us-counties-10m",
          "globals": [
            "usCounties10m"
          ]
        },
        {
          "id": "@floating-ui/core",
          "globals": [
            "FloatingUICore"
          ]
        },
        {
          "id": "@floating-ui/dom",
          "globals": [
            "FloatingUIDOM"
          ]
        },
        {
          "id": "lucide",
          "globals": [
            "lucide"
          ]
        },
        {
          "id": "mermaid",
          "globals": [
            "mermaid"
          ]
        },
        {
          "id": "react",
          "globals": [
            "React",
            "ReactDOM"
          ]
        },
        {
          "id": "fluentui",
          "globals": [
            "FluentUI",
            "React",
            "ReactDOM"
          ]
        }
      ],
      "rules": [
        "Declare library names without versions; unknown names are ignored. The host always uses its currently supported version, ignoring any supplied version.",
        "Place the manifest before user scripts. The declared globals are available when later scripts run.",
        "Never emit <script src> or a library URL. The host fetches first-party bytes, verifies pinned integrity, and injects them inline.",
        "Maximum 6 manifest entries, 8 MiB per library, and 16 MiB total."
      ]
    },
    "externalLinkAllowlist": [
      "microsoft.ghe.com",
      "onedrive.cloud.microsoft",
      "1drv.ms"
    ],
    "refreshDebounceMs": 5000
  },
  "publish": {
    "auth": "Authenticated as the current user (delegated OAuth). Scope: Files.ReadWrite for the user’s own OneDrive, or Sites.ReadWrite.All for a SharePoint site library. Never fabricate a token.",
    "confirmation": "Before uploading, LIST the destination folder (e.g. GET .../drive/root:/<folder>:/children or /drives/{driveId}/items/{folderId}/children) to detect name collisions. For CREATE, inspect and disclose the destination’s existing audience because the new item inherits those permissions. For OVERWRITE, inspect and disclose the target item’s own existing audience, permissions, and sharing links because its identity, unique permissions, and existing anonymous or organization links remain in place for the replacement content. If either audience cannot be inspected, state that uncertainty. Tell the user CREATE vs OVERWRITE and CONFIRM every operation. Uploading creates no new sharing link or anonymous access.",
    "folder": "Documents/Agent HTML/ in the user’s OneDrive (the drive root is the Documents library, so the API upload path is root-relative: root:/Agent HTML/).",
    "graph": "PUT https://graph.microsoft.com/v1.0/me/drive/root:/Agent HTML/<name>.html:/content (Content-Type: text/html; ≤ 4 MB simple upload, else upload session). Primary path: graph.microsoft.com routes to whichever SPO farm holds the caller’s OneDrive — including dogfood rings like *.sharepoint-df.com — based on the token, so it just works.",
    "graphSite": "PUT https://graph.microsoft.com/v1.0/sites/{site-id}/drive/root:/Agent HTML/<name>.html:/content (publish into a SharePoint site’s default document library instead of your OneDrive; scope Sites.ReadWrite.All).",
    "vroom": "PUT https://{tenant}-my.sharepoint.com/_api/v2.0/me/drive/root:/Agent HTML/<name>.html:/content (SharePoint-audience token; alternative when you already hold a SharePoint OAuth token via SPFx/add-in/on-behalf-of, or want to target a specific SharePoint host directly).",
    "vroomSite": "PUT {siteUrl}/_api/v2.0/drive/root:/Agent HTML/<name>.html:/content (site library via Vroom; the site drive root is its default “Shared Documents” library, so keep the path root-relative).",
    "byFolderUrl": "Fewest calls, and works for any OneDrive/site/library when you only have the destination folder’s URL: (1) encode the folder URL as a sharing token — `u!` + unpadded base64url (base64, strip `=`, `/`→`_`, `+`→`-`); (2) GET https://graph.microsoft.com/v1.0/shares/u!{base64url(folderUrl)}/driveItem resolves it to the folder’s driveItem (read `id` + `parentReference.driveId`); (3) PUT https://graph.microsoft.com/v1.0/drives/{drive-id}/items/{folder-id}:/<name>.html:/content. No drive enumeration or site/library id needed. Vroom: same paths under {host}/_api/v2.0.",
    "getUrl": "Read `webUrl` from the driveItem response, then give the user `webUrl` with `?web=1` appended (`<webUrl>?web=1`) — that opens it in the OneUp HTML viewer, whereas the bare `webUrl` downloads the .html file. The URL is stable across updates.",
    "update": "Overwrite by PUT-ing the same path or `/me/drive/items/{item-id}/content`; PATCH the item to rename/move."
  },
  "sitePages": {
    "summary": "HTML pages in Site Pages are .html files whose ContentTypeId starts with the HTML Page base prefix. The three-call flow creates through the sanctioned Site Pages API with that content type, verifies the returned identity and concurrency token, then writes through HtmlPage.Save.",
    "important": [
      "Use the ordered create, verify, and save steps below. The supported creation service must provision the HTML Page type; there is no generic upload or content-type mutation fallback.",
      "A Site Page is created and written only via POST <current-web-url>/_api/sitepages/pages and POST <current-web-url>/_api/htmlpages/pages(<itemId>)/save — never via a drive/driveItem upload, a content PUT, createUploadSession, a generic connector file-upload action (e.g. WorkIQ), or any other OneDrive/document-library upload mechanism. Those write a different item in a different library; they do not create or update a Site Page, and can leave a zero-byte placeholder with no HTML content. If the only tool available to a non-browser transport is a file-upload helper, it cannot perform this flow and is not a valid substitute — report that and do not use it to \"complete\" the request.",
      "The HTML Pages REST service is a beta API. If it is unavailable, HtmlPage.Save returns HTTP 503 with “The HTML Pages API is unavailable.” Stop and report that result; do not invent a fallback. A transport failure before a response from the selected web’s relevant endpoint is not this condition; see creationTransportRecovery before the first mutating request.",
      "Creation ends with an author-visible draft. Publication, approval, homepage changes, and deletion remain explicit advanced operations."
    ],
    "htmlSitePageContentTypePrefix": "0x0101009D1CB255DA76424F860D91F20E6C411800EE3B4FE2C29A4DCDB55229ADEB3DBFE4",
    "maxHtmlBytes": 10485760,
    "maxHtmlBytesHuman": "10 MiB",
    "requiredCallCount": 3,
    "sharedGuidance": {
      "authentication": [
        "Use the selected web’s absolute URL as <current-web-url> for every request. The lifecycle requires authentication accepted by that target SharePoint web; do not claim support for an external caller until its real token and tenant have been tested.",
        "For cookie authentication, obtain a current form digest from <current-web-url>/_api/contextinfo and send it as X-RequestDigest on every state-changing POST. OAuth bearer-token callers do not send a request digest."
      ],
      "locationAndOData": [
        "Prefer Path.DecodedUrl, then Url, as the create response’s decoded web-relative path. Do not call decodeURIComponent on either value. Strip only leading slashes and join it to the selected web’s decoded server-relative pathname with one slash. For example, web /sites/Team/Subweb plus /SitePages/Director's plan.html becomes /sites/Team/Subweb/SitePages/Director's plan.html.",
        "Use AbsoluteUrl only when neither decoded path field is present. Require it to begin with the exact selected web absolute URL plus a path separator, remove that prefix by string slicing rather than URL parsing, and treat the remainder as an already-decoded web-relative path. Stop on a prefix mismatch. Never pass decoded names such as Plan#1.html, Plan%20one.html, or Plan%.html through new URL or decodeURIComponent.",
        "Build @filePath exactly as encodeURIComponent(`'${serverRelativeUrl.replace(/'/g, \"''\")}'`): double embedded apostrophes, wrap the complete normalized path in single quotes, then pass that complete OData literal to encodeURIComponent. Never interpolate a raw path into the query string."
      ],
      "identityAndContentType": [
        "Capture the create response’s Id, UniqueId, FileName, AbsoluteUrl, and Path.DecodedUrl (or Url). Require FileName to equal the requested exact .html filename.",
        "SharePoint may conflict-rename the page (for example requesting plan.html but receiving plan(2).html) instead of erroring. Treat a returned FileName/FileLeafRef that differs from the requested exact filename the same as any other verification failure: perform no mutation. Stop and report the exact returned Id, UniqueId, AbsoluteUrl, ServerRelativeUrl, and FileLeafRef; explicitly warn that the renamed item remains. Never save HTML into this or any other guessed/assumed item. Before recycling or deleting the conflict-created item, require explicit user confirmation that names that exact returned identity (UniqueId plus ListItemAllFields/Id). Never infer a cleanup target from the requested filename.",
        "Immediately before Save, read the normalized exact location and require UniqueId, ListItemAllFields/Id, ListItemAllFields/FileLeafRef, and normalized ServerRelativeUrl to match the create response. Treat Path.DecodedUrl, Url, and ServerRelativeUrl as decoded paths; do not decode them again. Convert backslashes to forward slashes, collapse duplicate slashes, and require one leading slash; never lowercase names or resolve traversal segments.",
        "Require ListItemAllFields/ContentTypeId to equal or start with 0x0101009D1CB255DA76424F860D91F20E6C411800EE3B4FE2C29A4DCDB55229ADEB3DBFE4; child content types are valid, unrelated content types are not."
      ],
      "saveOutcomes": [
        "Before creation, require an exact .html filename, reject path traversal, and send the complete filename including its final .html extension as Name. This preserves dotted basenames: release.v2.html, report.2026.09.html, and index.html must be sent unchanged. Require the final UTF-8 HTML including its BOM to be no larger than 10 MiB.",
        "Use only the verified ListItemAllFields/Id as <verified-item-id> and the immediately read top-level file UIVersion as <fresh-UIVersion>. Never use currentVersion=-1.",
        "Prefix the complete final document with U+FEFF so the body begins with UTF-8 BOM bytes EF BB BF; count those three bytes toward the 10 MiB limit.",
        "After Save is dispatched, treat HTTP 409 as a concurrency conflict and an unavailable response as an unknown outcome. Never automatically recycle, delete, or retry after either result; report the exact page URL and IDs because another session or a successful hidden write may exist.",
        "If Save rejects the content type or the HTML Pages API is unavailable, stop and report the failure. Do not mutate the content type, provision a library, use a generic upload fallback, or add an internal retry loop. A connector or transport failure prior to a response from the selected web is not this condition; see creationTransportRecovery.",
        "On success, require HtmlPageSaveResult.UIVersion and preserve it as <saved-UIVersion>. The saved result is an author-visible draft until a separately requested publication succeeds, and approval completes when required."
      ],
      "advancedOperations": [
        "Publishing, submitting for approval, setting the homepage, and deletion are separate operations that require explicit caller consent.",
        "Before changing visibility, re-read the exact identity, checkout owner, moderation state, MajorVersion, MinorVersion, and UIVersion; require UIVersion=<saved-UIVersion> under the checkout that will perform the transition.",
        "Never undo a pre-existing or unknown checkout. A publication flow may undo only a checkout that it confirmed it acquired, using a fresh cleanup signal with its own 10,000 ms timeout without replacing the original failure.",
        "Build <encoded-comment-odata-literal> as `%27${encodeURIComponent(comment.replace(/'/g, \"''\"))}%27`: double embedded apostrophes, encode the comment, and include the encoded surrounding single quotes.",
        "Prefer the supported set_site_homepage capability when it is available. For an HTML page, require the set-homepage tool flight and HTML-homepage feature to be enabled and the HTML-homepage kill switch not to block the scenario.",
        "Expose the raw RootFolder.WelcomePage MERGE only as a fallback after explicit user consent and caller-side verification of ManageWeb permission, same-web containment, the exact existing target path and identity, Web Page Library membership, and reader-visible publication."
      ]
    },
    "creationTransportRecovery": [
      "Distinguish transport/tool failure from target API failure. If one authorized transport cannot reach or execute against the target site, but another authenticated transport is available, agents should try the alternate transport before concluding Site Pages publishing is blocked. Do not switch the destination or publishing semantics; only switch the transport.",
      "A connector/tool diagnostic that the call itself never dispatched or never received any response from the selected web — for example its own executed:false, a pre-routing/pre-dispatch failure, or a connector-internal error raised before a request left the connector — is evidence about that connector only, not about the target site or the HTML Pages API. By contrast, a status such as HTTP 404, accessDenied, a tenant-routing failure, missing content-upload support, an auth-context mismatch, or CLI/device-code auth blocked by Conditional Access policy is not, by itself, evidence of either: if the connector actually dispatched the request and the selected web returned that result, it is an endpoint-produced result and must be reported as-is — the same confirmed-result handling as any other stop condition (see important/saveOutcomes) — never discarded or recharacterized as transport-layer noise to justify switching transport or destination. Before treating any such status as connector-only, confirm the connector itself reports it never dispatched the request or never got a response from the selected web; absent that, treat the status as the selected web’s own result. This applies to any connector (e.g. Graph, Power Platform, WorkIQ, or another connector).",
      "An alternate transport is usable for this recovery only if it can issue the sanctioned Site Pages REST calls (sitepages/pages create, the identity/version verify read, and htmlpages/pages save) with delegated auth against the selected web. A file-upload-only helper — a drive/driveItem upload, createUploadSession, or a generic connector file-upload action (e.g. WorkIQ) — cannot issue those calls and is not a valid alternate transport; do not use one to substitute for, or \"complete,\" Site Pages publishing. If CLI/device-code/connector auth is blocked (e.g. by Conditional Access) or lacks delegated SharePoint REST access, but an authenticated browser session can open the selected web, same-origin SharePoint REST issued through that browser context is a valid transport: use the browser only to supply the authenticated session (cookies) and a form digest for the same REST calls, never to perform the creation, verification, or save by manually operating the browser UI (no address-bar/dev-console workarounds, no manual file upload through the page).",
      "Before any mutation, select at most one already-authorized alternate transport — e.g. an agent runtime explicitly authorized to automate the current user’s own already-authenticated browser session, acting same-origin against the exact selected web — and use it once for a single harmless read (GET <current-web-url>/_api/web?$select=Id). Success proves only that this transport reaches the selected web under that session; it does not prove the Site Pages creation or Save endpoint is available or authorized — treat that request’s own result independently. Do not fabricate or relay credentials, cookies, tokens, or digests across origins or processes, and do not try a second alternate transport if the first probe is inconclusive.",
      "If no already-authorized alternate transport exists, report the original client’s exact failure, state that availability could not be determined, and ask the caller to provide or authorize a supported execution context if they want to continue. Do not claim the site or API is unavailable, and do not fall back to OneDrive or a document library.",
      "If the alternate transport is cookie-authenticated, the existing authentication rule still applies unchanged: a current X-RequestDigest from <current-web-url>/_api/contextinfo on each state-changing POST.",
      "Once a create or Save request has actually been dispatched, never retry it, including via a different transport. Existing identity/version reads may be used as diagnostic state only — never to search for or clean up a presumed created item by filename. With no confirmed endpoint result, report the outcome as unknown with whatever URL and IDs are known, per saveOutcomes.",
      "A confirmed endpoint-produced result is the only definitive outcome — for example HtmlPage.Save returning HTTP 503 (see important), or, per saveOutcomes, Save’s HTTP 409 as a concurrency conflict. A 409 or other error from the creation endpoint is its own confirmed result; do not assume it is the same condition. Caller-specific denials — authorization, and, governed solely by advancedOperations, checkout, moderation, or approval — are not site or API unavailability. This is not an exhaustive list: report any other confirmed endpoint response (400, 401, 429, etc.) the same way — stop, report, never retry."
    ],
    "lifecycle": [
      {
        "title": "Create the HTML Page",
        "details": "Create through the supported Site Pages service and capture the exact filename, URL, file GUID, and item ID returned by SharePoint."
      },
      {
        "title": "Verify the exact created page",
        "details": "Normalize the returned location, verify the exact file and list-item identity plus the HTML Page content-type prefix, and read a fresh top-level UIVersion."
      },
      {
        "title": "Save the final HTML as a draft",
        "details": "Save the complete BOM-prefixed document with the fresh version token, then report the successful result as an author-visible draft."
      }
    ],
    "calls": [
      {
        "title": "Create the HTML Page",
        "details": "Create through the supported Site Pages service and capture the exact filename, URL, file GUID, and item ID returned by SharePoint.",
        "call": {
          "method": "POST",
          "endpoint": "<current-web-url>/_api/sitepages/pages",
          "headers": {
            "Accept": "application/json;odata=nometadata",
            "Content-Type": "application/json;odata=nometadata"
          },
          "body": "{ \"Name\": \"<requested-exact-.html-filename>\", \"ContentTypeId\": \"0x0101009D1CB255DA76424F860D91F20E6C411800EE3B4FE2C29A4DCDB55229ADEB3DBFE4\" }",
          "notes": [
            "This sanctioned creation path provisions the requested HTML Page content type while creating the file. Stop if it is unavailable; a transport failure before any response from the selected web is not this condition.",
            "Apply sharedGuidance.authentication, sharedGuidance.identityAndContentType, and sharedGuidance.saveOutcomes before continuing."
          ]
        }
      },
      {
        "title": "Verify the exact created page",
        "details": "Normalize the returned location, verify the exact file and list-item identity plus the HTML Page content-type prefix, and read a fresh top-level UIVersion.",
        "call": {
          "method": "GET",
          "endpoint": "<current-web-url>/_api/web/GetFileByServerRelativePath(DecodedUrl=@filePath)?$select=UniqueId,ServerRelativeUrl,UIVersion,ListItemAllFields/Id,ListItemAllFields/ContentTypeId,ListItemAllFields/FileLeafRef&$expand=ListItemAllFields&@filePath=<encodeURIComponent-of-quoted-created-file-server-relative-url>",
          "headers": {
            "Accept": "application/json;odata=nometadata"
          },
          "notes": [
            "Run immediately before HtmlPage.Save.",
            "Apply sharedGuidance.locationAndOData and sharedGuidance.identityAndContentType; use only the verified item ID and fresh top-level file UIVersion in Save."
          ]
        }
      },
      {
        "title": "Save the final HTML as a draft",
        "details": "Save the complete BOM-prefixed document with the fresh version token, then report the successful result as an author-visible draft.",
        "call": {
          "method": "POST",
          "endpoint": "<current-web-url>/_api/htmlpages/pages(<verified-item-id>)/save(currentVersion=<fresh-UIVersion>,generateNewVersion=true)",
          "headers": {
            "Accept": "application/json;odata=nometadata",
            "Content-Type": "text/html; charset=utf-8"
          },
          "body": "Complete final UTF-8 HTML document bytes beginning with UTF-8 BOM bytes EF BB BF (max 10 MiB including the BOM).",
          "notes": [
            "HtmlPage.Save updates the verified existing item; it does not create an item.",
            "Apply sharedGuidance.authentication and sharedGuidance.saveOutcomes. Preserve the successful <saved-UIVersion> for any later advanced operation."
          ]
        }
      }
    ],
    "optionalPublish": {
      "consent": "Only after the caller explicitly asks to publish or submit for approval; never bundle either action with creation.",
      "state": {
        "method": "GET",
        "endpoint": "<current-web-url>/_api/web/GetFileById(guid'<created-file-unique-id>')?$select=UniqueId,ServerRelativeUrl,CheckOutType,CheckedOutByUser/Id,CheckedOutByUser/Title,MajorVersion,MinorVersion,UIVersion,ListItemAllFields/Id,ListItemAllFields/OData__ModerationStatus,ListItemAllFields/ParentList/EnableModeration,ListItemAllFields/ParentList/EnableMinorVersions&$expand=CheckedOutByUser,ListItemAllFields,ListItemAllFields/ParentList",
        "headers": {
          "Accept": "application/json;odata=nometadata"
        },
        "notes": [
          "Use only after the caller explicitly asks to publish or submit for approval; do not bundle either action into creation.",
          "Read this state after HtmlPage.Save and immediately before choosing a publication branch, then re-read the same fields after the action before reporting its result. Before changing visibility, require this exact file’s UIVersion to equal <saved-UIVersion>; if it differs or is missing, another write may have advanced the draft, so stop without publishing. The post-action UIVersion is diagnostic only and does not replace this pre-action concurrency gate.",
          "When EnableModeration=false, use the major-check-in branch below. When EnableModeration=true, OData__ModerationStatus 3 is Draft and can be submitted; 2 is already Pending; 0 is Approved; 1 is Denied and must be edited/saved back to Draft before resubmission.",
          "When EnableModeration=false, interpret versions as production does: MinorVersion>0 means the current content is still a draft (MajorVersion>=1 means readers see an older major version; MajorVersion=0 means no published version exists). MinorVersion=0 with MajorVersion>=1 means the current content is reader-visible. If MajorVersion or MinorVersion is missing, report publication state as unknown. Report UIVersion from the post-action read so intervening edits are visible.",
          "Use CheckOutType and CheckedOutByUser.Id to establish exclusive ownership before the final version gate. Compare CheckedOutByUser.Id with the current user: stop and report CheckedOutByUser.Title for another user’s checkout; reuse a checkout already held by the current caller, otherwise acquire checkout on this exact file. After checkout is established, re-read identity and UIVersion and require the same created identity plus UIVersion=<saved-UIVersion> before continuing."
        ]
      },
      "withoutContentApproval": {
        "method": "POST",
        "endpoint": "<current-web-url>/_api/web/GetFileById(guid'<created-file-unique-id>')/CheckIn(comment=@comment,checkintype=1)?@comment=<encoded-comment-odata-literal>",
        "headers": {
          "Accept": "application/json;odata=nometadata"
        },
        "notes": [
          "Use only when the publication-state read returned EnableModeration=false. This is the reader-visible publish or republish transition for a library without content approval.",
          "Before this MAJOR check-in, establish checkout ownership as described by the publication-state gate, then re-read the exact file under that checkout and require its identity plus UIVersion=<saved-UIVersion>. Do not check in when the version differs, even if the moderation and version-state fields otherwise look publishable.",
          "Run POST <current-web-url>/_api/web/GetFileById(guid'<created-file-unique-id>')/CheckOut when needed, the locked identity/version re-read, and the MAJOR check-in under one AbortSignal with a 60,000 ms overall timeout. Pass that same signal to every request in the primary transition and stop when it aborts.",
          "Track whether CheckOut confirmed that this publication flow acquired the checkout. If the primary transition fails or aborts after confirmed acquisition, best-effort POST <current-web-url>/_api/web/GetFileById(guid'<created-file-unique-id>')/UndoCheckOut only for that checkout; never undo a checkout that predated this attempt. Give cleanup a separate fresh AbortSignal with its own 10,000 ms timeout. If checkout ownership or the CheckOut outcome is unknown, do not undo automatically; report the possible remaining checkout. Cleanup failure or abort must not replace the original publication failure.",
          "After check-in, re-read the publication-state fields. Report success for the current content only when MinorVersion=0 and MajorVersion>=1; if MinorVersion>0, report that the current content is a draft and whether readers see an older major version.",
          "Apply sharedGuidance.authentication, sharedGuidance.locationAndOData, and sharedGuidance.advancedOperations."
        ]
      },
      "withContentApproval": {
        "method": "POST",
        "endpoint": "<current-web-url>/_api/web/GetFileById(guid'<created-file-unique-id>')/publish(comment=@comment)?@comment=<encoded-comment-odata-literal>",
        "headers": {
          "Accept": "application/json;odata=nometadata"
        },
        "notes": [
          "Use only when the publication-state read returned EnableModeration=true and OData__ModerationStatus=3 (Draft). A major check-in alone does not submit a moderated draft.",
          "Before this call, establish checkout ownership and re-read the exact file under that checkout. Require its identity, UIVersion=<saved-UIVersion>, and EnableMinorVersions=true; stop if another user owns the checkout, the version differs, or the library cannot produce the exact next minor version.",
          "Check in the verified content as a MINOR draft with checkintype=0, then immediately re-read the checked-in file before SP.File.publish. Require the same identity, EnableModeration=true, EnableMinorVersions=true, OData__ModerationStatus=3, and UIVersion exactly equal to <saved-UIVersion> + 1. Preserve that exact value as <checked-in-UIVersion>. If any requirement fails, stop without submitting because another write may have replaced this flow’s checked-in draft.",
          "Run checkout when needed, the locked re-read, minor check-in, final checked-in re-read, and SP.File.publish under one AbortSignal with a 60,000 ms overall timeout. Pass that same signal to every request in the primary transition and stop when it aborts. If this flow confirmed that it acquired checkout and fails before minor check-in releases it, use a separate fresh AbortSignal with its own 10,000 ms timeout for best-effort UndoCheckOut; cleanup failure or abort must not replace the original failure.",
          "On success, re-read OData__ModerationStatus and require 2 (Pending). Treat the post-action UIVersion as diagnostic only; approval remains a separate operation requiring ApproveItems, so do not call approve or write _ModerationStatus directly.",
          "Apply sharedGuidance.authentication, sharedGuidance.locationAndOData, and sharedGuidance.advancedOperations."
        ]
      }
    },
    "optionalSetHomepage": {
      "consent": "Only after the caller explicitly asks to set the homepage. Prefer the supported set_site_homepage capability; expose the raw WelcomePage MERGE only as a fully verified fallback.",
      "preferredCapability": "set_site_homepage",
      "availability": "For HTML pages, require the set-homepage tool flight and HTML-homepage feature to be enabled and the HTML-homepage kill switch not to block the scenario.",
      "requiredPermission": "ManageWeb",
      "call": {
        "method": "POST",
        "endpoint": "<current-web-url>/_api/web/rootfolder",
        "headers": {
          "Accept": "application/json;odata=nometadata",
          "Content-Type": "application/json;odata=nometadata",
          "IF-MATCH": "*",
          "X-HTTP-Method": "MERGE"
        },
        "body": "{ \"WelcomePage\": \"<web-relative path to the .html file>\" }",
        "notes": [
          "Prefer the supported set_site_homepage capability. Use this raw MERGE fallback only after the caller explicitly consents to setting the site homepage and the supported capability is unavailable.",
          "For an HTML page, first require the set-homepage tool flight and HTML-homepage feature to be enabled and the HTML-homepage kill switch not to block the scenario.",
          "Before exposing this fallback, verify ManageWeb permission, SAME-web containment, the exact target file identity and normalized path, and that the file exists in a Web Page Library.",
          "Require the exact HTML target to be already published and reader-visible. Do not publish it implicitly or treat the homepage request itself as publication consent.",
          "WelcomePage must be a web-relative path in the SAME web, not a server-relative or absolute URL.",
          "Apply sharedGuidance.authentication and sharedGuidance.advancedOperations."
        ]
      }
    }
  },
  "skill": {
    "description": "Agent skill + CSP-compliant templates for authoring and publishing SharePoint HTML.",
    "repo": "https://github.com/SharePoint/sharepoint-html",
    "install": "curl -fsSL https://raw.githubusercontent.com/SharePoint/sharepoint-html/main/install.sh | sh",
    "installViaSkillsCli": "npx skills add SharePoint/sharepoint-html"
  },
  "sourceOfTruth": {
    "CSP + sandbox": "odsp-common/high-value-components/html-viewer/src/security/HtmlViewerSecurity.ts",
    "LiveData item cap": "odsp-common/high-value-components/html-viewer/src/liveData/LiveDataManifest.ts",
    "Per-item byte cap + formats": "odsp-common/high-value-components/html-viewer/src/handlers/useGetDataHandler.ts",
    "Refresh debounce": "odsp-common/high-value-components/html-viewer/src/handlers/useRefreshHandler.ts",
    "External link allowlist": "odsp-common/high-value-components/html-viewer/src/handlers/linkSafety.ts",
    "External libraries": "odsp-common/high-value-components/html-viewer/src/externalLibraries/ExternalLibraries.ts",
    "SharePoint list live data": "odsp-next/app-cores/oneup-lightspeed/src/viewers/htmlViewer/fetchListRowsForOneUp.ts"
  },
  "generated": "This contract mirrors the OneUp HTML viewer source (see sourceOfTruth) and is the authoritative, current spec."
}
