@aglyn/aglyn 1.0.0-beta.244 → 1.0.0-beta.246

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,8 +37,9 @@
37
37
  *
38
38
  * Costed honestly rather than listed as options:
39
39
  *
40
- * * **The origin.** Yes, ~15 s. The deny list and the lockdown verdict share
41
- * one TTL, so a warm process refuses within that of the write. This is the
40
+ * * **The origin.** Yes, ~15 s for a file: the quarantine deny list's TTL,
41
+ * so a warm process refuses within that of the write. A workspace or site
42
+ * lockdown's verdict is held longer, up to a minute (AGL-3741). This is the
42
43
  * whole of what the CDN gate does, and it is not nothing.
43
44
  * * **The raw Storage URL.** Yes, immediately — by rotating the object's
44
45
  * download token, which is the ONLY lever that works there, because no
@@ -73,12 +74,17 @@
73
74
  * they drift apart.
74
75
  */
75
76
  /**
76
- * How long a warm origin can keep serving after the write.
77
- *
78
- * The deny list's TTL and the lockdown verdict's cache TTL, which are
79
- * deliberately the same 15 s so both levers converge on one clock.
77
+ * How long a warm origin can keep serving a file after its takedown: the
78
+ * quarantine deny list's TTL (`MEDIA_QUARANTINE_TTL_MS`).
80
79
  */
81
80
  export declare const MEDIA_TAKEDOWN_ORIGIN_MS = 15000;
81
+ /**
82
+ * How long a warm origin can keep serving after a workspace or site
83
+ * lockdown: the scope verdict's cache TTL (`MEDIA_CDN_LOCK_TTL_MS`), a minute
84
+ * since AGL-3741 because image requests land on short-lived instances. Never
85
+ * shorter than {@link MEDIA_TAKEDOWN_ORIGIN_MS}.
86
+ */
87
+ export declare const MEDIA_LOCKDOWN_ORIGIN_MS = 60000;
82
88
  /** `max-age` on the stable CDN URL — one browser's worst-case stale read. */
83
89
  export declare const MEDIA_TAKEDOWN_BROWSER_STABLE_MS = 60000;
84
90
  /** `s-maxage` on an image response — the shared edge's worst case. */
@@ -36,8 +36,9 @@
36
36
  *
37
37
  * Costed honestly rather than listed as options:
38
38
  *
39
- * * **The origin.** Yes, ~15 s. The deny list and the lockdown verdict share
40
- * one TTL, so a warm process refuses within that of the write. This is the
39
+ * * **The origin.** Yes, ~15 s for a file: the quarantine deny list's TTL,
40
+ * so a warm process refuses within that of the write. A workspace or site
41
+ * lockdown's verdict is held longer, up to a minute (AGL-3741). This is the
41
42
  * whole of what the CDN gate does, and it is not nothing.
42
43
  * * **The raw Storage URL.** Yes, immediately — by rotating the object's
43
44
  * download token, which is the ONLY lever that works there, because no
@@ -71,11 +72,15 @@
71
72
  * real `Cache-Control` values out of `serve-media-cdn.ts` and fails when
72
73
  * they drift apart.
73
74
  */ /**
74
- * How long a warm origin can keep serving after the write.
75
- *
76
- * The deny list's TTL and the lockdown verdict's cache TTL, which are
77
- * deliberately the same 15 s so both levers converge on one clock.
75
+ * How long a warm origin can keep serving a file after its takedown: the
76
+ * quarantine deny list's TTL (`MEDIA_QUARANTINE_TTL_MS`).
78
77
  */ export const MEDIA_TAKEDOWN_ORIGIN_MS = 15000;
78
+ /**
79
+ * How long a warm origin can keep serving after a workspace or site
80
+ * lockdown: the scope verdict's cache TTL (`MEDIA_CDN_LOCK_TTL_MS`), a minute
81
+ * since AGL-3741 because image requests land on short-lived instances. Never
82
+ * shorter than {@link MEDIA_TAKEDOWN_ORIGIN_MS}.
83
+ */ export const MEDIA_LOCKDOWN_ORIGIN_MS = 60000;
79
84
  /** `max-age` on the stable CDN URL — one browser's worst-case stale read. */ export const MEDIA_TAKEDOWN_BROWSER_STABLE_MS = 60000;
80
85
  /** `s-maxage` on an image response — the shared edge's worst case. */ export const MEDIA_TAKEDOWN_EDGE_IMAGE_MS = 3600000;
81
86
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/media-takedown-reach.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT A MEDIA TAKEDOWN ACTUALLY REACHES (AGL-1615).\n *\n * ## The problem this module exists to fix\n *\n * AGL-1615 is only half a code issue. The mechanism behaves exactly as\n * designed — quarantine and lockdown both refuse at the ORIGIN — and the\n * runbook has said so accurately since AGL-1520. What was wrong is what the\n * PRODUCT said. The staff quarantine page told an operator that a takedown\n * \"takes ONE uploaded file off the CDN worldwide\", full stop, and the abuse\n * triage queue repeated it. A rights-holder given that sentence, or a\n * merchant told their leaked pricing sheet is down, reasonably concludes the\n * file is unreachable. It is not.\n *\n * A takedown that is described as a recall, and is not one, is not a\n * technical shortfall — it is a false assurance handed to someone making a\n * legal or safety decision on the strength of it. That is the part worth\n * fixing, and the fix is precision, not a purge that does not exist.\n *\n * ## What can genuinely be invalidated, and what cannot\n *\n * Costed honestly rather than listed as options:\n *\n * * **The origin.** Yes, ~15 s. The deny list and the lockdown verdict share\n * one TTL, so a warm process refuses within that of the write. This is the\n * whole of what the CDN gate does, and it is not nothing.\n * * **The raw Storage URL.** Yes, immediately — by rotating the object's\n * download token, which is the ONLY lever that works there, because no\n * code of ours runs on `firebasestorage.googleapis.com`. AGL-1526 built it\n * for security lockdowns; AGL-1615 wires it to per-asset quarantine, where\n * the exposure is sharper: a free-tier org, a private asset and every\n * pre-AGL-829 embed are all delivered from that URL, and for those the CDN\n * deny list is not merely slow, it is never consulted.\n * * **A Vercel edge purge.** Rejected, and this is the option AGL-1615\n * floated first. Purge is path-based and the key set is enumerable, but it\n * would put a credentialled call to a third-party API on the takedown\n * path — so a Vercel outage becomes a takedown outage unless it fails\n * soft, and a purge that fails soft is a purge you cannot rely on, which\n * is the same position as not having one while believing you do. The\n * window it would close is at most an hour, on IMAGES only, and only\n * those already edge-cached: non-image types have been `private` since\n * AGL-1515 and are never edge-held.\n * * **Shortening the image `s-maxage`.** Rejected. AGL-1515 measured that\n * hit rate as real on the DAM grid's hot path; trading it for a faster\n * worst case buys an hour in a rare event at a cost paid on every request.\n * * **Bytes already delivered.** No. Not by any mechanism, at any price.\n * A browser cache, a corporate proxy, a downstream CDN, a scraper's disk,\n * an archive snapshot — nothing we can build reaches those, and no vendor\n * sells the ability either. This is the sentence the console was missing.\n *\n * ## Why the numbers live here\n *\n * Copy that quotes a cache window goes stale the moment someone tunes a\n * header, and specific-and-wrong is worse than vague. So the windows are\n * constants here, one place, and `media-takedown-reach.spec.ts` reads the\n * real `Cache-Control` values out of `serve-media-cdn.ts` and fails when\n * they drift apart.\n */\n\n/**\n * How long a warm origin can keep serving after the write.\n *\n * The deny list's TTL and the lockdown verdict's cache TTL, which are\n * deliberately the same 15 s so both levers converge on one clock.\n */\nexport const MEDIA_TAKEDOWN_ORIGIN_MS = 15_000\n\n/** `max-age` on the stable CDN URL — one browser's worst-case stale read. */\nexport const MEDIA_TAKEDOWN_BROWSER_STABLE_MS = 60_000\n\n/** `s-maxage` on an image response — the shared edge's worst case. */\nexport const MEDIA_TAKEDOWN_EDGE_IMAGE_MS = 3_600_000\n\n/**\n * `max-age` on the immutable content-hashed URL and on the versioned URL a\n * published page names (AGL-3485): a year, in one browser.\n */\nexport const MEDIA_TAKEDOWN_BROWSER_IMMUTABLE_MS = 31_536_000_000\n\nexport type MediaTakedownSurface =\n | 'origin'\n | 'browser-stable'\n | 'edge-image'\n | 'raw-url'\n | 'browser-immutable'\n | 'delivered'\n\nexport interface MediaTakedownReachEntry {\n surface: MediaTakedownSurface\n /** Does a takedown stop this surface serving at all? */\n stopped: boolean\n /**\n * Worst-case delay before it stops, or `null` when it never does.\n *\n * `null` rather than a very large number on purpose. Writing a year here\n * for the immutable URL would read as \"and then it is fine\", which is a\n * different and false claim — a copy on a scraper's disk has no expiry at\n * all, and the immutable URL's year is per-client and unbounded in\n * aggregate.\n */\n worstCaseMs: number | null\n /** The sentence an operator reads. Plain, specific, no hedging. */\n statement: string\n}\n\n/**\n * Every surface an asset can be delivered from, including the ones a\n * takedown cannot touch.\n *\n * Completeness is the property that matters: an omitted surface is\n * indistinguishable from a covered one, and the omissions are exactly what\n * made the old copy misleading.\n */\nexport const MEDIA_TAKEDOWN_REACH: readonly MediaTakedownReachEntry[] = [\n {\n surface: 'origin',\n stopped: true,\n worstCaseMs: MEDIA_TAKEDOWN_ORIGIN_MS,\n statement:\n 'Our origin stops serving the file within about 15 seconds. Every new ' +\n 'request that reaches us after that is refused, and the refusal itself ' +\n 'is never cached, so lifting the takedown is just as fast.',\n },\n {\n surface: 'raw-url',\n stopped: true,\n worstCaseMs: 1,\n statement:\n \"The raw Storage download URL is killed immediately, by rotating the \" +\n 'object’s token. That URL is served by Google, not by us, so nothing ' +\n 'else could have stopped it — and it is how free-tier workspaces, ' +\n 'private files and older embeds are delivered. Rotation is permanent: ' +\n 'lifting the takedown does not bring that particular link back.',\n },\n {\n surface: 'browser-stable',\n stopped: true,\n worstCaseMs: MEDIA_TAKEDOWN_BROWSER_STABLE_MS,\n statement:\n 'A browser that fetched the ordinary file URL in the last minute may ' +\n 'keep showing it for up to 60 seconds before it asks us again.',\n },\n {\n surface: 'edge-image',\n stopped: true,\n worstCaseMs: MEDIA_TAKEDOWN_EDGE_IMAGE_MS,\n statement:\n 'For IMAGES, the CDN edge may keep serving a copy it already stored ' +\n 'for up to an hour, plus one further serve while it revalidates. ' +\n 'Video, PDFs and other files are never held at the edge, so they stop ' +\n 'as soon as the origin does.',\n },\n {\n surface: 'browser-immutable',\n stopped: false,\n worstCaseMs: null,\n statement:\n 'An image as a published page shows it, and the content-hashed ' +\n 'permanent URL, stay pinned for up to a year in any browser that ' +\n 'already fetched them — those URLs are promised never to change, so ' +\n 'nothing can expire them early. There is no per-file purge for them.',\n },\n {\n surface: 'delivered',\n stopped: false,\n worstCaseMs: null,\n statement:\n 'Anything already downloaded is gone from our reach entirely: a ' +\n 'browser cache, a corporate proxy, a downstream CDN, a scraper, an ' +\n 'archive snapshot. No mechanism we have or could buy recalls those.',\n },\n]\n\n/** The reach, as lines to render. Order is the order operators need it in. */\nexport function mediaTakedownReachLines(): string[] {\n return MEDIA_TAKEDOWN_REACH.map((entry) => entry.statement)\n}\n\n/**\n * The one sentence that must never be dropped, whatever surface renders the\n * rest. If a page has room for a single line about this, it is this one.\n */\nexport function mediaTakedownUnreachableLine(): string {\n return (\n 'A takedown stops new delivery; it is NOT a recall. Bytes that have ' +\n 'already been served — into a browser cache, a downstream CDN, a ' +\n 'scraper or an archive — stay where they are. Treat a public file as ' +\n 'already distributed and act accordingly.'\n )\n}\n\n/**\n * The short form, for a surface with no room for six lines: the two bounded\n * numbers and the limit, in one sentence.\n */\nexport function mediaTakedownReachSummary(): string {\n return (\n 'Stops at our origin in ~15s and kills the raw download link ' +\n 'immediately; an already-cached image can serve from the CDN edge for ' +\n 'up to an hour, and stays in a browser that already loaded the page. ' +\n 'It is not a recall — anything already downloaded stays downloaded.'\n )\n}\n"],"names":["MEDIA_TAKEDOWN_ORIGIN_MS","MEDIA_TAKEDOWN_BROWSER_STABLE_MS","MEDIA_TAKEDOWN_EDGE_IMAGE_MS","MEDIA_TAKEDOWN_BROWSER_IMMUTABLE_MS","MEDIA_TAKEDOWN_REACH","surface","stopped","worstCaseMs","statement","mediaTakedownReachLines","map","entry","mediaTakedownUnreachableLine","mediaTakedownReachSummary"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyDC,GAED;;;;;CAKC,GACD,OAAO,MAAMA,2BAA2B,MAAM;AAE9C,2EAA2E,GAC3E,OAAO,MAAMC,mCAAmC,MAAM;AAEtD,oEAAoE,GACpE,OAAO,MAAMC,+BAA+B,QAAS;AAErD;;;CAGC,GACD,OAAO,MAAMC,sCAAsC,YAAc;AA4BjE;;;;;;;CAOC,GACD,OAAO,MAAMC,uBAA2D;IACtE;QACEC,SAAS;QACTC,SAAS;QACTC,aAAaP;QACbQ,WACE,0EACA,2EACA;IACJ;IACA;QACEH,SAAS;QACTC,SAAS;QACTC,aAAa;QACbC,WACE,yEACA,yEACA,sEACA,0EACA;IACJ;IACA;QACEH,SAAS;QACTC,SAAS;QACTC,aAAaN;QACbO,WACE,yEACA;IACJ;IACA;QACEH,SAAS;QACTC,SAAS;QACTC,aAAaL;QACbM,WACE,wEACA,qEACA,0EACA;IACJ;IACA;QACEH,SAAS;QACTC,SAAS;QACTC,aAAa;QACbC,WACE,mEACA,qEACA,wEACA;IACJ;IACA;QACEH,SAAS;QACTC,SAAS;QACTC,aAAa;QACbC,WACE,oEACA,uEACA;IACJ;CACD,CAAA;AAED,4EAA4E,GAC5E,OAAO,SAASC;IACd,OAAOL,qBAAqBM,GAAG,CAAC,CAACC,QAAUA,MAAMH,SAAS;AAC5D;AAEA;;;CAGC,GACD,OAAO,SAASI;IACd,OACE,wEACA,qEACA,yEACA;AAEJ;AAEA;;;CAGC,GACD,OAAO,SAASC;IACd,OACE,iEACA,0EACA,yEACA;AAEJ"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/media-takedown-reach.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT A MEDIA TAKEDOWN ACTUALLY REACHES (AGL-1615).\n *\n * ## The problem this module exists to fix\n *\n * AGL-1615 is only half a code issue. The mechanism behaves exactly as\n * designed — quarantine and lockdown both refuse at the ORIGIN — and the\n * runbook has said so accurately since AGL-1520. What was wrong is what the\n * PRODUCT said. The staff quarantine page told an operator that a takedown\n * \"takes ONE uploaded file off the CDN worldwide\", full stop, and the abuse\n * triage queue repeated it. A rights-holder given that sentence, or a\n * merchant told their leaked pricing sheet is down, reasonably concludes the\n * file is unreachable. It is not.\n *\n * A takedown that is described as a recall, and is not one, is not a\n * technical shortfall — it is a false assurance handed to someone making a\n * legal or safety decision on the strength of it. That is the part worth\n * fixing, and the fix is precision, not a purge that does not exist.\n *\n * ## What can genuinely be invalidated, and what cannot\n *\n * Costed honestly rather than listed as options:\n *\n * * **The origin.** Yes, ~15 s for a file: the quarantine deny list's TTL,\n * so a warm process refuses within that of the write. A workspace or site\n * lockdown's verdict is held longer, up to a minute (AGL-3741). This is the\n * whole of what the CDN gate does, and it is not nothing.\n * * **The raw Storage URL.** Yes, immediately — by rotating the object's\n * download token, which is the ONLY lever that works there, because no\n * code of ours runs on `firebasestorage.googleapis.com`. AGL-1526 built it\n * for security lockdowns; AGL-1615 wires it to per-asset quarantine, where\n * the exposure is sharper: a free-tier org, a private asset and every\n * pre-AGL-829 embed are all delivered from that URL, and for those the CDN\n * deny list is not merely slow, it is never consulted.\n * * **A Vercel edge purge.** Rejected, and this is the option AGL-1615\n * floated first. Purge is path-based and the key set is enumerable, but it\n * would put a credentialled call to a third-party API on the takedown\n * path — so a Vercel outage becomes a takedown outage unless it fails\n * soft, and a purge that fails soft is a purge you cannot rely on, which\n * is the same position as not having one while believing you do. The\n * window it would close is at most an hour, on IMAGES only, and only\n * those already edge-cached: non-image types have been `private` since\n * AGL-1515 and are never edge-held.\n * * **Shortening the image `s-maxage`.** Rejected. AGL-1515 measured that\n * hit rate as real on the DAM grid's hot path; trading it for a faster\n * worst case buys an hour in a rare event at a cost paid on every request.\n * * **Bytes already delivered.** No. Not by any mechanism, at any price.\n * A browser cache, a corporate proxy, a downstream CDN, a scraper's disk,\n * an archive snapshot — nothing we can build reaches those, and no vendor\n * sells the ability either. This is the sentence the console was missing.\n *\n * ## Why the numbers live here\n *\n * Copy that quotes a cache window goes stale the moment someone tunes a\n * header, and specific-and-wrong is worse than vague. So the windows are\n * constants here, one place, and `media-takedown-reach.spec.ts` reads the\n * real `Cache-Control` values out of `serve-media-cdn.ts` and fails when\n * they drift apart.\n */\n\n/**\n * How long a warm origin can keep serving a file after its takedown: the\n * quarantine deny list's TTL (`MEDIA_QUARANTINE_TTL_MS`).\n */\nexport const MEDIA_TAKEDOWN_ORIGIN_MS = 15_000\n\n/**\n * How long a warm origin can keep serving after a workspace or site\n * lockdown: the scope verdict's cache TTL (`MEDIA_CDN_LOCK_TTL_MS`), a minute\n * since AGL-3741 because image requests land on short-lived instances. Never\n * shorter than {@link MEDIA_TAKEDOWN_ORIGIN_MS}.\n */\nexport const MEDIA_LOCKDOWN_ORIGIN_MS = 60_000\n\n/** `max-age` on the stable CDN URL — one browser's worst-case stale read. */\nexport const MEDIA_TAKEDOWN_BROWSER_STABLE_MS = 60_000\n\n/** `s-maxage` on an image response — the shared edge's worst case. */\nexport const MEDIA_TAKEDOWN_EDGE_IMAGE_MS = 3_600_000\n\n/**\n * `max-age` on the immutable content-hashed URL and on the versioned URL a\n * published page names (AGL-3485): a year, in one browser.\n */\nexport const MEDIA_TAKEDOWN_BROWSER_IMMUTABLE_MS = 31_536_000_000\n\nexport type MediaTakedownSurface =\n | 'origin'\n | 'browser-stable'\n | 'edge-image'\n | 'raw-url'\n | 'browser-immutable'\n | 'delivered'\n\nexport interface MediaTakedownReachEntry {\n surface: MediaTakedownSurface\n /** Does a takedown stop this surface serving at all? */\n stopped: boolean\n /**\n * Worst-case delay before it stops, or `null` when it never does.\n *\n * `null` rather than a very large number on purpose. Writing a year here\n * for the immutable URL would read as \"and then it is fine\", which is a\n * different and false claim — a copy on a scraper's disk has no expiry at\n * all, and the immutable URL's year is per-client and unbounded in\n * aggregate.\n */\n worstCaseMs: number | null\n /** The sentence an operator reads. Plain, specific, no hedging. */\n statement: string\n}\n\n/**\n * Every surface an asset can be delivered from, including the ones a\n * takedown cannot touch.\n *\n * Completeness is the property that matters: an omitted surface is\n * indistinguishable from a covered one, and the omissions are exactly what\n * made the old copy misleading.\n */\nexport const MEDIA_TAKEDOWN_REACH: readonly MediaTakedownReachEntry[] = [\n {\n surface: 'origin',\n stopped: true,\n worstCaseMs: MEDIA_TAKEDOWN_ORIGIN_MS,\n statement:\n 'Our origin stops serving the file within about 15 seconds. Every new ' +\n 'request that reaches us after that is refused, and the refusal itself ' +\n 'is never cached, so lifting the takedown is just as fast.',\n },\n {\n surface: 'raw-url',\n stopped: true,\n worstCaseMs: 1,\n statement:\n \"The raw Storage download URL is killed immediately, by rotating the \" +\n 'object’s token. That URL is served by Google, not by us, so nothing ' +\n 'else could have stopped it — and it is how free-tier workspaces, ' +\n 'private files and older embeds are delivered. Rotation is permanent: ' +\n 'lifting the takedown does not bring that particular link back.',\n },\n {\n surface: 'browser-stable',\n stopped: true,\n worstCaseMs: MEDIA_TAKEDOWN_BROWSER_STABLE_MS,\n statement:\n 'A browser that fetched the ordinary file URL in the last minute may ' +\n 'keep showing it for up to 60 seconds before it asks us again.',\n },\n {\n surface: 'edge-image',\n stopped: true,\n worstCaseMs: MEDIA_TAKEDOWN_EDGE_IMAGE_MS,\n statement:\n 'For IMAGES, the CDN edge may keep serving a copy it already stored ' +\n 'for up to an hour, plus one further serve while it revalidates. ' +\n 'Video, PDFs and other files are never held at the edge, so they stop ' +\n 'as soon as the origin does.',\n },\n {\n surface: 'browser-immutable',\n stopped: false,\n worstCaseMs: null,\n statement:\n 'An image as a published page shows it, and the content-hashed ' +\n 'permanent URL, stay pinned for up to a year in any browser that ' +\n 'already fetched them — those URLs are promised never to change, so ' +\n 'nothing can expire them early. There is no per-file purge for them.',\n },\n {\n surface: 'delivered',\n stopped: false,\n worstCaseMs: null,\n statement:\n 'Anything already downloaded is gone from our reach entirely: a ' +\n 'browser cache, a corporate proxy, a downstream CDN, a scraper, an ' +\n 'archive snapshot. No mechanism we have or could buy recalls those.',\n },\n]\n\n/** The reach, as lines to render. Order is the order operators need it in. */\nexport function mediaTakedownReachLines(): string[] {\n return MEDIA_TAKEDOWN_REACH.map((entry) => entry.statement)\n}\n\n/**\n * The one sentence that must never be dropped, whatever surface renders the\n * rest. If a page has room for a single line about this, it is this one.\n */\nexport function mediaTakedownUnreachableLine(): string {\n return (\n 'A takedown stops new delivery; it is NOT a recall. Bytes that have ' +\n 'already been served — into a browser cache, a downstream CDN, a ' +\n 'scraper or an archive — stay where they are. Treat a public file as ' +\n 'already distributed and act accordingly.'\n )\n}\n\n/**\n * The short form, for a surface with no room for six lines: the two bounded\n * numbers and the limit, in one sentence.\n */\nexport function mediaTakedownReachSummary(): string {\n return (\n 'Stops at our origin in ~15s and kills the raw download link ' +\n 'immediately; an already-cached image can serve from the CDN edge for ' +\n 'up to an hour, and stays in a browser that already loaded the page. ' +\n 'It is not a recall — anything already downloaded stays downloaded.'\n )\n}\n"],"names":["MEDIA_TAKEDOWN_ORIGIN_MS","MEDIA_LOCKDOWN_ORIGIN_MS","MEDIA_TAKEDOWN_BROWSER_STABLE_MS","MEDIA_TAKEDOWN_EDGE_IMAGE_MS","MEDIA_TAKEDOWN_BROWSER_IMMUTABLE_MS","MEDIA_TAKEDOWN_REACH","surface","stopped","worstCaseMs","statement","mediaTakedownReachLines","map","entry","mediaTakedownUnreachableLine","mediaTakedownReachSummary"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0DC,GAED;;;CAGC,GACD,OAAO,MAAMA,2BAA2B,MAAM;AAE9C;;;;;CAKC,GACD,OAAO,MAAMC,2BAA2B,MAAM;AAE9C,2EAA2E,GAC3E,OAAO,MAAMC,mCAAmC,MAAM;AAEtD,oEAAoE,GACpE,OAAO,MAAMC,+BAA+B,QAAS;AAErD;;;CAGC,GACD,OAAO,MAAMC,sCAAsC,YAAc;AA4BjE;;;;;;;CAOC,GACD,OAAO,MAAMC,uBAA2D;IACtE;QACEC,SAAS;QACTC,SAAS;QACTC,aAAaR;QACbS,WACE,0EACA,2EACA;IACJ;IACA;QACEH,SAAS;QACTC,SAAS;QACTC,aAAa;QACbC,WACE,yEACA,yEACA,sEACA,0EACA;IACJ;IACA;QACEH,SAAS;QACTC,SAAS;QACTC,aAAaN;QACbO,WACE,yEACA;IACJ;IACA;QACEH,SAAS;QACTC,SAAS;QACTC,aAAaL;QACbM,WACE,wEACA,qEACA,0EACA;IACJ;IACA;QACEH,SAAS;QACTC,SAAS;QACTC,aAAa;QACbC,WACE,mEACA,qEACA,wEACA;IACJ;IACA;QACEH,SAAS;QACTC,SAAS;QACTC,aAAa;QACbC,WACE,oEACA,uEACA;IACJ;CACD,CAAA;AAED,4EAA4E,GAC5E,OAAO,SAASC;IACd,OAAOL,qBAAqBM,GAAG,CAAC,CAACC,QAAUA,MAAMH,SAAS;AAC5D;AAEA;;;CAGC,GACD,OAAO,SAASI;IACd,OACE,wEACA,qEACA,yEACA;AAEJ;AAEA;;;CAGC,GACD,OAAO,SAASC;IACd,OACE,iEACA,0EACA,yEACA;AAEJ"}
@@ -147,6 +147,13 @@ export interface PluginAiCapability {
147
147
  * Exactly one of this and `draftResource` is set.
148
148
  */
149
149
  runnerKind?: string;
150
+ /**
151
+ * The writer's write takes effect at once, because the resource has no
152
+ * draft state (a redirect rule, AGL-3745; a CRM pipeline or custom field,
153
+ * AGL-3746), so the job reports the item as live in `where` rather than as
154
+ * a draft. Absent: the writer writes a draft.
155
+ */
156
+ live?: boolean;
150
157
  /** The most this item can cost, in AI credits; a draft written without a model costs 0. */
151
158
  estimateCredits(args: PluginAiCapabilityArgs): number;
152
159
  /** Operations an item of this one may depend on. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-ai-capabilities.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * What an AI build can make, contributed by the plugin that owns each thing\n * (AGL-3616).\n *\n * A build turns one request — \"a few pages, a contact form and a way to book\n * me\" — into a plan of ITEMS, each an operation (`op`) with arguments, and\n * runs them in dependency order. The AI plugin plans and meters the build;\n * it does not know what a plugin's resource is, and the package map forbids\n * it importing that plugin. So every operation is a CAPABILITY the owner\n * registers from its server entry, and the build asks for one by `op`:\n *\n * - the planner offers the model only the operations registered here, in\n * the owner's own words (`noun`, `intents`, `argsSchema`);\n * - admission removes an item whose capability this site, plan or member\n * cannot use (`feature`, `permission`, `quota`, `freeAllowed`), with a\n * sentence, so the rest of the plan still runs;\n * - the item is executed one of two ways: `runnerKind` hands it to an\n * existing AI job runner (the AI plugin's own operations), and\n * `draftResource` hands its content to the owner's writer on\n * `plugin-resource-drafts`, which checks it, meets the allowance and\n * writes a draft that nothing publishes.\n *\n * An operation lights up the moment its owner registers it; nothing in the\n * AI plugin names it. Register by CALLING `registerPluginAiCapability` from a\n * function the server entry runs — a top-level side effect is dropped by a\n * bundler that sees no import of its result.\n *\n * Server-only: import this file by its own path, never from the barrel that\n * every published page loads.\n *\n * ## One capability per operation\n *\n * An operation has one owner. A second plugin registering an `op` another\n * plugin already owns is refused, naming both, and the incumbent keeps\n * serving; the same plugin registering again replaces its own.\n */\n\n/** A value an item's arguments may hold: scalars and lists of strings only. */\nexport type PluginAiCapabilityArgValue = string | number | boolean | readonly string[]\n\n/** An item's arguments, in the shape its capability's `argsSchema` declares. */\nexport type PluginAiCapabilityArgs = Readonly<Record<string, PluginAiCapabilityArgValue>>\n\n/**\n * One argument, as a JSON Schema property the model fills in directly. The\n * subset is deliberately small: a flat object of scalars and string lists is\n * what a planner can be held to, and what `pluginAiCapabilityArgsProblems`\n * checks without a schema library.\n */\nexport interface PluginAiCapabilityArgProperty {\n type: 'string' | 'integer' | 'number' | 'boolean' | 'array'\n /** Read by the model: what the value means and how to choose it. */\n description: string\n /** For `string`: the allowed values. */\n enum?: readonly string[]\n /** For `integer`/`number`. */\n minimum?: number\n maximum?: number\n /** For `string`, and each item of an `array`. */\n maxLength?: number\n /** For `array`: always a list of strings. */\n items?: { type: 'string'; enum?: readonly string[]; maxLength?: number }\n maxItems?: number\n}\n\n/** A flat JSON Schema object: no nesting, no extra keys. */\nexport interface PluginAiCapabilityArgsSchema {\n type: 'object'\n properties: Readonly<Record<string, PluginAiCapabilityArgProperty>>\n required?: readonly string[]\n additionalProperties: false\n}\n\n/** How the items that depend on a failed item of this capability behave. */\nexport type PluginAiCapabilityDegrade =\n /** Dependents are built without it (a page loses the block that used it). */\n | 'omit'\n /** Dependents use something that already exists in its place (a site's own layout). */\n | 'fallback'\n\n/** One planned item, as a capability sees it. */\nexport interface PluginAiCapabilityItem {\n /** The item's name in the plan, unique within it; other items cite it as `new:<name>`. */\n name: string\n args: PluginAiCapabilityArgs\n}\n\n/** What a capability's `draftContent` is told beside the item. */\nexport interface PluginAiCapabilityDraftContext {\n hostId: string\n /**\n * The drafts written for the items this one depends on, keyed by the\n * reference the plan used (`new:<name>`), each with its operation and the\n * id its own writer or runner gave it. A dependency that did not succeed is\n * absent.\n */\n dependencies: Readonly<Record<string, { op: string; id: string }>>\n}\n\nexport interface PluginAiCapability {\n /** The operation's name in a plan, stable across releases: `page`, `note`. */\n op: string\n /** What one is called in a sentence a person reads: \"contact form\". */\n noun: string\n /** Where a person finds the draft afterwards: \"Notes → Drafts\". */\n where: string\n /**\n * What a person might ask for that this makes, as short phrases the chat's\n * instructions list. True of the shipped product: never a promise the\n * writer cannot keep.\n */\n intents: readonly string[]\n /** The item's arguments, which the planner fills in. */\n argsSchema: PluginAiCapabilityArgsSchema\n /** How many items of this operation one plan may hold. */\n maxPerPlan: number\n /** Whether a workspace on the free plan may have one made. */\n freeAllowed: boolean\n /** The entitlement a workspace needs, checked with the platform's entitlement read. */\n feature?: string\n /** The site permission the member needs. */\n permission?: string\n /** The allowance one counts against, which the owner's writer meets. */\n quota?: string\n /**\n * The resource whose writer on `plugin-resource-drafts` makes the draft.\n * Exactly one of this and `runnerKind` is set.\n */\n draftResource?: string\n /**\n * The AI job kind whose runner makes it — the AI plugin's own operations.\n * Exactly one of this and `draftResource` is set.\n */\n runnerKind?: string\n /** The most this item can cost, in AI credits; a draft written without a model costs 0. */\n estimateCredits(args: PluginAiCapabilityArgs): number\n /** Operations an item of this one may depend on. */\n dependsOnOps?: readonly string[]\n degrade: PluginAiCapabilityDegrade\n /**\n * Blocks a page places when it uses an item of this operation (the block\n * names the page palette knows). When the item fails, a dependent page is\n * built without them, and says so.\n */\n pageBlocks?: readonly string[]\n /**\n * The writer's content for an item, from its arguments and its built\n * dependencies. Pure. Absent means the arguments ARE the content.\n */\n draftContent?(\n item: PluginAiCapabilityItem,\n context: PluginAiCapabilityDraftContext,\n ): Readonly<Record<string, unknown>>\n}\n\nexport const PLUGIN_AI_CAPABILITIES = definePluginServiceContract<PluginAiCapability>(\n 'core.ai-capabilities',\n { multiple: true },\n)\n\nconst OP_PATTERN = /^[a-z][a-z0-9-]{0,39}$/\n\n/** Why a capability cannot be registered, or `null`. */\nexport function pluginAiCapabilityProblem(capability: PluginAiCapability): string | null {\n if (!OP_PATTERN.test(capability.op)) {\n return `ai capability op \"${capability.op}\" must be lowercase letters, digits and dashes`\n }\n const executors = [capability.draftResource, capability.runnerKind].filter(\n (one) => typeof one === 'string' && one.trim(),\n )\n if (executors.length !== 1) {\n return `ai capability \"${capability.op}\" needs exactly one of draftResource and runnerKind`\n }\n if (!Number.isInteger(capability.maxPerPlan) || capability.maxPerPlan < 1) {\n return `ai capability \"${capability.op}\" needs a maxPerPlan of at least 1`\n }\n if (capability.argsSchema?.type !== 'object' || capability.argsSchema.additionalProperties !== false) {\n return `ai capability \"${capability.op}\" needs a flat object argsSchema with additionalProperties: false`\n }\n for (const required of capability.argsSchema.required ?? []) {\n if (!(required in capability.argsSchema.properties)) {\n return `ai capability \"${capability.op}\" requires \"${required}\", which its schema does not declare`\n }\n }\n if (!capability.noun.trim() || !capability.where.trim()) {\n return `ai capability \"${capability.op}\" needs a noun and a where`\n }\n return null\n}\n\n/**\n * Registers what a plugin's AI builds can make. The owner is the loader's\n * marker when a register fn is running, else `options.pluginId`. An `op`\n * another plugin owns throws naming both; a malformed capability throws.\n */\nexport function registerPluginAiCapability(\n capability: PluginAiCapability,\n options?: { pluginId?: string },\n): void {\n const problem = pluginAiCapabilityProblem(capability)\n if (problem) throw new Error(problem)\n const key = capability.op\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_AI_CAPABILITIES).find(\n (entry) => entry.key === key,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `ai capability \"${key}\" is already registered by \"${incumbent.pluginId}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n registerPluginService(PLUGIN_AI_CAPABILITIES, capability, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n })\n}\n\nexport interface ResolvedPluginAiCapability {\n /** The plugin that owns the operation. */\n pluginId: string\n capability: PluginAiCapability\n}\n\n/** Every registered capability with its owner, in registration order. */\nexport function pluginAiCapabilities(): ResolvedPluginAiCapability[] {\n return resolvePluginServices(PLUGIN_AI_CAPABILITIES).map((entry) => ({\n pluginId: entry.pluginId,\n capability: entry.impl,\n }))\n}\n\n/** The capability for an operation, with its owner, or `null` when nothing registered it. */\nexport function pluginAiCapability(op: string): ResolvedPluginAiCapability | null {\n const key = op.trim()\n const entry = resolvePluginServices(PLUGIN_AI_CAPABILITIES).find((one) => one.key === key)\n return entry ? { pluginId: entry.pluginId, capability: entry.impl } : null\n}\n\n/**\n * What is wrong with an item's arguments against its schema; empty when\n * nothing is. Pure, and the same answer on both sides of the seam: the\n * planner holds the model to it, and an owner may hold its content to it.\n */\nexport function pluginAiCapabilityArgsProblems(\n schema: PluginAiCapabilityArgsSchema,\n args: Readonly<Record<string, unknown>>,\n): string[] {\n const problems: string[] = []\n for (const name of schema.required ?? []) {\n const value = args[name]\n if (value === undefined || value === null || value === '') problems.push(`\"${name}\" is required`)\n }\n for (const [name, value] of Object.entries(args)) {\n const property = schema.properties[name]\n if (!property) {\n problems.push(`\"${name}\" is not an argument`)\n continue\n }\n if (value === undefined || value === null) continue\n switch (property.type) {\n case 'string':\n if (typeof value !== 'string') problems.push(`\"${name}\" must be text`)\n else {\n if (property.enum && !property.enum.includes(value)) {\n problems.push(`\"${name}\" must be one of ${property.enum.join(', ')}`)\n }\n if (property.maxLength !== undefined && value.length > property.maxLength) {\n problems.push(`\"${name}\" must be at most ${property.maxLength} characters`)\n }\n }\n break\n case 'integer':\n case 'number':\n if (\n typeof value !== 'number' ||\n !Number.isFinite(value) ||\n (property.type === 'integer' && !Number.isInteger(value))\n ) {\n problems.push(`\"${name}\" must be ${property.type === 'integer' ? 'a whole number' : 'a number'}`)\n } else {\n if (property.minimum !== undefined && value < property.minimum) {\n problems.push(`\"${name}\" must be at least ${property.minimum}`)\n }\n if (property.maximum !== undefined && value > property.maximum) {\n problems.push(`\"${name}\" must be at most ${property.maximum}`)\n }\n }\n break\n case 'boolean':\n if (typeof value !== 'boolean') problems.push(`\"${name}\" must be true or false`)\n break\n case 'array':\n if (!Array.isArray(value) || value.some((one) => typeof one !== 'string')) {\n problems.push(`\"${name}\" must be a list of text`)\n } else {\n if (property.maxItems !== undefined && value.length > property.maxItems) {\n problems.push(`\"${name}\" must hold at most ${property.maxItems}`)\n }\n const items = property.items\n for (const one of value as string[]) {\n if (items?.enum && !items.enum.includes(one)) {\n problems.push(`\"${name}\" holds \"${one}\", which is not one of ${items.enum.join(', ')}`)\n break\n }\n if (items?.maxLength !== undefined && one.length > items.maxLength) {\n problems.push(`\"${name}\" holds an entry longer than ${items.maxLength} characters`)\n break\n }\n }\n }\n break\n }\n }\n return problems\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_AI_CAPABILITIES","multiple","OP_PATTERN","pluginAiCapabilityProblem","capability","test","op","executors","draftResource","runnerKind","filter","one","trim","length","Number","isInteger","maxPerPlan","argsSchema","type","additionalProperties","required","properties","noun","where","registerPluginAiCapability","options","problem","Error","key","pluginId","incumbent","find","entry","pluginAiCapabilities","map","impl","pluginAiCapability","pluginAiCapabilityArgsProblems","schema","args","problems","name","value","undefined","push","Object","entries","property","enum","includes","join","maxLength","isFinite","minimum","maximum","Array","isArray","some","maxItems","items"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-ai-capabilities.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * What an AI build can make, contributed by the plugin that owns each thing\n * (AGL-3616).\n *\n * A build turns one request — \"a few pages, a contact form and a way to book\n * me\" — into a plan of ITEMS, each an operation (`op`) with arguments, and\n * runs them in dependency order. The AI plugin plans and meters the build;\n * it does not know what a plugin's resource is, and the package map forbids\n * it importing that plugin. So every operation is a CAPABILITY the owner\n * registers from its server entry, and the build asks for one by `op`:\n *\n * - the planner offers the model only the operations registered here, in\n * the owner's own words (`noun`, `intents`, `argsSchema`);\n * - admission removes an item whose capability this site, plan or member\n * cannot use (`feature`, `permission`, `quota`, `freeAllowed`), with a\n * sentence, so the rest of the plan still runs;\n * - the item is executed one of two ways: `runnerKind` hands it to an\n * existing AI job runner (the AI plugin's own operations), and\n * `draftResource` hands its content to the owner's writer on\n * `plugin-resource-drafts`, which checks it, meets the allowance and\n * writes a draft that nothing publishes.\n *\n * An operation lights up the moment its owner registers it; nothing in the\n * AI plugin names it. Register by CALLING `registerPluginAiCapability` from a\n * function the server entry runs — a top-level side effect is dropped by a\n * bundler that sees no import of its result.\n *\n * Server-only: import this file by its own path, never from the barrel that\n * every published page loads.\n *\n * ## One capability per operation\n *\n * An operation has one owner. A second plugin registering an `op` another\n * plugin already owns is refused, naming both, and the incumbent keeps\n * serving; the same plugin registering again replaces its own.\n */\n\n/** A value an item's arguments may hold: scalars and lists of strings only. */\nexport type PluginAiCapabilityArgValue = string | number | boolean | readonly string[]\n\n/** An item's arguments, in the shape its capability's `argsSchema` declares. */\nexport type PluginAiCapabilityArgs = Readonly<Record<string, PluginAiCapabilityArgValue>>\n\n/**\n * One argument, as a JSON Schema property the model fills in directly. The\n * subset is deliberately small: a flat object of scalars and string lists is\n * what a planner can be held to, and what `pluginAiCapabilityArgsProblems`\n * checks without a schema library.\n */\nexport interface PluginAiCapabilityArgProperty {\n type: 'string' | 'integer' | 'number' | 'boolean' | 'array'\n /** Read by the model: what the value means and how to choose it. */\n description: string\n /** For `string`: the allowed values. */\n enum?: readonly string[]\n /** For `integer`/`number`. */\n minimum?: number\n maximum?: number\n /** For `string`, and each item of an `array`. */\n maxLength?: number\n /** For `array`: always a list of strings. */\n items?: { type: 'string'; enum?: readonly string[]; maxLength?: number }\n maxItems?: number\n}\n\n/** A flat JSON Schema object: no nesting, no extra keys. */\nexport interface PluginAiCapabilityArgsSchema {\n type: 'object'\n properties: Readonly<Record<string, PluginAiCapabilityArgProperty>>\n required?: readonly string[]\n additionalProperties: false\n}\n\n/** How the items that depend on a failed item of this capability behave. */\nexport type PluginAiCapabilityDegrade =\n /** Dependents are built without it (a page loses the block that used it). */\n | 'omit'\n /** Dependents use something that already exists in its place (a site's own layout). */\n | 'fallback'\n\n/** One planned item, as a capability sees it. */\nexport interface PluginAiCapabilityItem {\n /** The item's name in the plan, unique within it; other items cite it as `new:<name>`. */\n name: string\n args: PluginAiCapabilityArgs\n}\n\n/** What a capability's `draftContent` is told beside the item. */\nexport interface PluginAiCapabilityDraftContext {\n hostId: string\n /**\n * The drafts written for the items this one depends on, keyed by the\n * reference the plan used (`new:<name>`), each with its operation and the\n * id its own writer or runner gave it. A dependency that did not succeed is\n * absent.\n */\n dependencies: Readonly<Record<string, { op: string; id: string }>>\n}\n\nexport interface PluginAiCapability {\n /** The operation's name in a plan, stable across releases: `page`, `note`. */\n op: string\n /** What one is called in a sentence a person reads: \"contact form\". */\n noun: string\n /** Where a person finds the draft afterwards: \"Notes → Drafts\". */\n where: string\n /**\n * What a person might ask for that this makes, as short phrases the chat's\n * instructions list. True of the shipped product: never a promise the\n * writer cannot keep.\n */\n intents: readonly string[]\n /** The item's arguments, which the planner fills in. */\n argsSchema: PluginAiCapabilityArgsSchema\n /** How many items of this operation one plan may hold. */\n maxPerPlan: number\n /** Whether a workspace on the free plan may have one made. */\n freeAllowed: boolean\n /** The entitlement a workspace needs, checked with the platform's entitlement read. */\n feature?: string\n /** The site permission the member needs. */\n permission?: string\n /** The allowance one counts against, which the owner's writer meets. */\n quota?: string\n /**\n * The resource whose writer on `plugin-resource-drafts` makes the draft.\n * Exactly one of this and `runnerKind` is set.\n */\n draftResource?: string\n /**\n * The AI job kind whose runner makes it — the AI plugin's own operations.\n * Exactly one of this and `draftResource` is set.\n */\n runnerKind?: string\n /**\n * The writer's write takes effect at once, because the resource has no\n * draft state (a redirect rule, AGL-3745; a CRM pipeline or custom field,\n * AGL-3746), so the job reports the item as live in `where` rather than as\n * a draft. Absent: the writer writes a draft.\n */\n live?: boolean\n /** The most this item can cost, in AI credits; a draft written without a model costs 0. */\n estimateCredits(args: PluginAiCapabilityArgs): number\n /** Operations an item of this one may depend on. */\n dependsOnOps?: readonly string[]\n degrade: PluginAiCapabilityDegrade\n /**\n * Blocks a page places when it uses an item of this operation (the block\n * names the page palette knows). When the item fails, a dependent page is\n * built without them, and says so.\n */\n pageBlocks?: readonly string[]\n /**\n * The writer's content for an item, from its arguments and its built\n * dependencies. Pure. Absent means the arguments ARE the content.\n */\n draftContent?(\n item: PluginAiCapabilityItem,\n context: PluginAiCapabilityDraftContext,\n ): Readonly<Record<string, unknown>>\n}\n\nexport const PLUGIN_AI_CAPABILITIES = definePluginServiceContract<PluginAiCapability>(\n 'core.ai-capabilities',\n { multiple: true },\n)\n\nconst OP_PATTERN = /^[a-z][a-z0-9-]{0,39}$/\n\n/** Why a capability cannot be registered, or `null`. */\nexport function pluginAiCapabilityProblem(capability: PluginAiCapability): string | null {\n if (!OP_PATTERN.test(capability.op)) {\n return `ai capability op \"${capability.op}\" must be lowercase letters, digits and dashes`\n }\n const executors = [capability.draftResource, capability.runnerKind].filter(\n (one) => typeof one === 'string' && one.trim(),\n )\n if (executors.length !== 1) {\n return `ai capability \"${capability.op}\" needs exactly one of draftResource and runnerKind`\n }\n if (!Number.isInteger(capability.maxPerPlan) || capability.maxPerPlan < 1) {\n return `ai capability \"${capability.op}\" needs a maxPerPlan of at least 1`\n }\n if (capability.argsSchema?.type !== 'object' || capability.argsSchema.additionalProperties !== false) {\n return `ai capability \"${capability.op}\" needs a flat object argsSchema with additionalProperties: false`\n }\n for (const required of capability.argsSchema.required ?? []) {\n if (!(required in capability.argsSchema.properties)) {\n return `ai capability \"${capability.op}\" requires \"${required}\", which its schema does not declare`\n }\n }\n if (!capability.noun.trim() || !capability.where.trim()) {\n return `ai capability \"${capability.op}\" needs a noun and a where`\n }\n return null\n}\n\n/**\n * Registers what a plugin's AI builds can make. The owner is the loader's\n * marker when a register fn is running, else `options.pluginId`. An `op`\n * another plugin owns throws naming both; a malformed capability throws.\n */\nexport function registerPluginAiCapability(\n capability: PluginAiCapability,\n options?: { pluginId?: string },\n): void {\n const problem = pluginAiCapabilityProblem(capability)\n if (problem) throw new Error(problem)\n const key = capability.op\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_AI_CAPABILITIES).find(\n (entry) => entry.key === key,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `ai capability \"${key}\" is already registered by \"${incumbent.pluginId}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n registerPluginService(PLUGIN_AI_CAPABILITIES, capability, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n })\n}\n\nexport interface ResolvedPluginAiCapability {\n /** The plugin that owns the operation. */\n pluginId: string\n capability: PluginAiCapability\n}\n\n/** Every registered capability with its owner, in registration order. */\nexport function pluginAiCapabilities(): ResolvedPluginAiCapability[] {\n return resolvePluginServices(PLUGIN_AI_CAPABILITIES).map((entry) => ({\n pluginId: entry.pluginId,\n capability: entry.impl,\n }))\n}\n\n/** The capability for an operation, with its owner, or `null` when nothing registered it. */\nexport function pluginAiCapability(op: string): ResolvedPluginAiCapability | null {\n const key = op.trim()\n const entry = resolvePluginServices(PLUGIN_AI_CAPABILITIES).find((one) => one.key === key)\n return entry ? { pluginId: entry.pluginId, capability: entry.impl } : null\n}\n\n/**\n * What is wrong with an item's arguments against its schema; empty when\n * nothing is. Pure, and the same answer on both sides of the seam: the\n * planner holds the model to it, and an owner may hold its content to it.\n */\nexport function pluginAiCapabilityArgsProblems(\n schema: PluginAiCapabilityArgsSchema,\n args: Readonly<Record<string, unknown>>,\n): string[] {\n const problems: string[] = []\n for (const name of schema.required ?? []) {\n const value = args[name]\n if (value === undefined || value === null || value === '') problems.push(`\"${name}\" is required`)\n }\n for (const [name, value] of Object.entries(args)) {\n const property = schema.properties[name]\n if (!property) {\n problems.push(`\"${name}\" is not an argument`)\n continue\n }\n if (value === undefined || value === null) continue\n switch (property.type) {\n case 'string':\n if (typeof value !== 'string') problems.push(`\"${name}\" must be text`)\n else {\n if (property.enum && !property.enum.includes(value)) {\n problems.push(`\"${name}\" must be one of ${property.enum.join(', ')}`)\n }\n if (property.maxLength !== undefined && value.length > property.maxLength) {\n problems.push(`\"${name}\" must be at most ${property.maxLength} characters`)\n }\n }\n break\n case 'integer':\n case 'number':\n if (\n typeof value !== 'number' ||\n !Number.isFinite(value) ||\n (property.type === 'integer' && !Number.isInteger(value))\n ) {\n problems.push(`\"${name}\" must be ${property.type === 'integer' ? 'a whole number' : 'a number'}`)\n } else {\n if (property.minimum !== undefined && value < property.minimum) {\n problems.push(`\"${name}\" must be at least ${property.minimum}`)\n }\n if (property.maximum !== undefined && value > property.maximum) {\n problems.push(`\"${name}\" must be at most ${property.maximum}`)\n }\n }\n break\n case 'boolean':\n if (typeof value !== 'boolean') problems.push(`\"${name}\" must be true or false`)\n break\n case 'array':\n if (!Array.isArray(value) || value.some((one) => typeof one !== 'string')) {\n problems.push(`\"${name}\" must be a list of text`)\n } else {\n if (property.maxItems !== undefined && value.length > property.maxItems) {\n problems.push(`\"${name}\" must hold at most ${property.maxItems}`)\n }\n const items = property.items\n for (const one of value as string[]) {\n if (items?.enum && !items.enum.includes(one)) {\n problems.push(`\"${name}\" holds \"${one}\", which is not one of ${items.enum.join(', ')}`)\n break\n }\n if (items?.maxLength !== undefined && one.length > items.maxLength) {\n problems.push(`\"${name}\" holds an entry longer than ${items.maxLength} characters`)\n break\n }\n }\n }\n break\n }\n }\n return problems\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_AI_CAPABILITIES","multiple","OP_PATTERN","pluginAiCapabilityProblem","capability","test","op","executors","draftResource","runnerKind","filter","one","trim","length","Number","isInteger","maxPerPlan","argsSchema","type","additionalProperties","required","properties","noun","where","registerPluginAiCapability","options","problem","Error","key","pluginId","incumbent","find","entry","pluginAiCapabilities","map","impl","pluginAiCapability","pluginAiCapabilityArgsProblems","schema","args","problems","name","value","undefined","push","Object","entries","property","enum","includes","join","maxLength","isFinite","minimum","maximum","Array","isArray","some","maxItems","items"],"mappings":"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"}