[{"data":1,"prerenderedAt":1660},["ShallowReactive",2],{"search-en":3,"content-en-developer\u002Fplugin-authoring":4,"surround-en-\u002Fdeveloper\u002Fplugin-authoring":1653},[],{"id":5,"title":6,"body":7,"description":1646,"extension":1647,"meta":1648,"navigation":148,"path":1649,"seo":1650,"stem":1651,"__hash__":1652},"content_en\u002F3.developer\u002F41.plugin-authoring.md","Building a Plugin",{"type":8,"value":9,"toc":1635},"minimark",[10,20,25,69,75,98,106,110,118,238,264,271,276,335,339,350,360,969,1019,1026,1030,1037,1249,1255,1423,1454,1466,1470,1477,1510,1525,1531,1546,1550,1576,1593,1596,1600,1631],[11,12,13,14,19],"p",{},"This is the plugin author's path from an empty directory to a contribution running in a deployment. It assumes a checkout of the Owlat workspace; see ",[15,16,18],"a",{"href":17},"\u002Fdeveloper\u002Fplugin-operations","Installing & Operating"," for the operator's side.",[21,22,24],"h2",{"id":23},"_1-scaffold-the-package","1. Scaffold the package",[26,27,32],"pre",{"className":28,"code":29,"language":30,"meta":31,"style":31},"language-sh shiki shiki-themes github-light github-dark-dimmed","bun run plugins:prepare                                  # build the plugin-kit contracts once\nbun packages\u002Fplugin-cli\u002Fsrc\u002Findex.ts create hello-owlat\n","sh","",[33,34,35,55],"code",{"__ignoreMap":31},[36,37,40,44,48,51],"span",{"class":38,"line":39},"line",1,[36,41,43],{"class":42},"sOLd2","bun",[36,45,47],{"class":46},"s-HuK"," run",[36,49,50],{"class":46}," plugins:prepare",[36,52,54],{"class":53},"sDN9O","                                  # build the plugin-kit contracts once\n",[36,56,58,60,63,66],{"class":38,"line":57},2,[36,59,43],{"class":42},[36,61,62],{"class":46}," packages\u002Fplugin-cli\u002Fsrc\u002Findex.ts",[36,64,65],{"class":46}," create",[36,67,68],{"class":46}," hello-owlat\n",[11,70,71,74],{},[33,72,73],{},"create"," writes files only — it never installs, imports, or executes anything, is idempotent on re-run, refuses to clobber a file whose content differs, and rolls back every file and directory it created if any write fails.",[11,76,77,78,81,82,85,86,89,90,93,94,97],{},"Defaults: package name ",[33,79,80],{},"@owlat\u002Fplugin-\u003Cid>",", directory ",[33,83,84],{},"examples\u002Fplugins\u002F\u003Cid>",". Override with ",[33,87,88],{},"--name"," and ",[33,91,92],{},"--dir","; preview with ",[33,95,96],{},"--dry-run",".",[26,99,104],{"className":100,"code":102,"language":103,"meta":31},[101],"language-text","examples\u002Fplugins\u002Fhello-owlat\u002F\n├── package.json          # depends on @owlat\u002Fplugin-kit (workspace:*)\n├── tsconfig.json         # extends the workspace base config\n├── vitest.config.ts      # aliases @owlat\u002Fplugin-kit to its sources\n├── README.md\n└── src\u002F\n    ├── manifest.ts       # the definePlugin declaration\n    ├── index.ts          # re-exports the manifest\n    └── __tests__\u002Fmanifest.test.ts\n","text",[33,105,102],{"__ignoreMap":31},[21,107,109],{"id":108},"_2-declare-the-manifest","2. Declare the manifest",[11,111,112,113,117],{},"The manifest is the whole contract. The host derives permissions, feature flags, the settings form, and the generated composition from this data ",[114,115,116],"strong",{},"without executing plugin code",", so keep it a static, data-only declaration.",[26,119,123],{"className":120,"code":121,"language":122,"meta":31,"style":31},"language-ts shiki shiki-themes github-light github-dark-dimmed","import { definePlugin } from '@owlat\u002Fplugin-kit';\n\nexport const helloPlugin = definePlugin({\n    id: 'hello-owlat',\n    version: '0.1.0',\n    capabilities: ['plugin-storage:read', 'plugin-storage:write'],\n    \u002F\u002F Storage capabilities REQUIRE a flag: access must be revocable at runtime.\n    flag: { default: false },\n});\n","ts",[33,124,125,144,150,173,185,196,214,220,232],{"__ignoreMap":31},[36,126,127,131,135,138,141],{"class":38,"line":39},[36,128,130],{"class":129},"s7YZ4","import",[36,132,134],{"class":133},"sYgZi"," { definePlugin } ",[36,136,137],{"class":129},"from",[36,139,140],{"class":46}," '@owlat\u002Fplugin-kit'",[36,142,143],{"class":133},";\n",[36,145,146],{"class":38,"line":57},[36,147,149],{"emptyLinePlaceholder":148},true,"\n",[36,151,153,156,159,163,166,170],{"class":38,"line":152},3,[36,154,155],{"class":129},"export",[36,157,158],{"class":129}," const",[36,160,162],{"class":161},"sviXB"," helloPlugin",[36,164,165],{"class":129}," =",[36,167,169],{"class":168},"sPO5f"," definePlugin",[36,171,172],{"class":133},"({\n",[36,174,176,179,182],{"class":38,"line":175},4,[36,177,178],{"class":133},"    id: ",[36,180,181],{"class":46},"'hello-owlat'",[36,183,184],{"class":133},",\n",[36,186,188,191,194],{"class":38,"line":187},5,[36,189,190],{"class":133},"    version: ",[36,192,193],{"class":46},"'0.1.0'",[36,195,184],{"class":133},[36,197,199,202,205,208,211],{"class":38,"line":198},6,[36,200,201],{"class":133},"    capabilities: [",[36,203,204],{"class":46},"'plugin-storage:read'",[36,206,207],{"class":133},", ",[36,209,210],{"class":46},"'plugin-storage:write'",[36,212,213],{"class":133},"],\n",[36,215,217],{"class":38,"line":216},7,[36,218,219],{"class":53},"    \u002F\u002F Storage capabilities REQUIRE a flag: access must be revocable at runtime.\n",[36,221,223,226,229],{"class":38,"line":222},8,[36,224,225],{"class":133},"    flag: { default: ",[36,227,228],{"class":161},"false",[36,230,231],{"class":133}," },\n",[36,233,235],{"class":38,"line":234},9,[36,236,237],{"class":133},"});\n",[11,239,240,243,244,247,248,251,252,255,256,259,260,263],{},[33,241,242],{},"definePlugin"," preserves literal TypeScript inference and validates at runtime, throwing ",[33,245,246],{},"PluginManifestError"," with a list of ",[33,249,250],{},"{ code, path, message }"," issues. Two sibling forms exist for tooling: ",[33,253,254],{},"validatePluginManifest(value)"," returns ",[33,257,258],{},"{ ok, manifest | issues }"," and never throws, and ",[33,261,262],{},"parsePluginManifest(value)"," throws.",[11,265,266,267,270],{},"Validation rejects unknown fields, accessor properties, malformed identifiers, duplicate capabilities or env vars, invalid budgets, unsafe export paths, and unknown contribution buckets. It inspects references as ",[114,268,269],{},"data"," and never invokes them.",[272,273,275],"h3",{"id":274},"rules-the-validator-enforces","Rules the validator enforces",[277,278,279,292,305,321,332],"ul",{},[280,281,282,283,287,288,291],"li",{},"A contribution bucket requires both its capability ",[284,285,286],"em",{},"and"," an explicit ",[33,289,290],{},"flag",". You cannot ship a contribution an operator has no way to turn off.",[280,293,294,297,298,301,302,304],{},[33,295,296],{},"plugin-storage:read"," \u002F ",[33,299,300],{},"plugin-storage:write"," require an explicit ",[33,303,290],{}," for the same reason.",[280,306,307,310,311,313,314,316,317,320],{},[33,308,309],{},"llm:invoke"," requires an explicit ",[33,312,290],{}," ",[114,315,286],{}," a valid ",[33,318,319],{},"llmBudget.dailyUsd"," (> 0, ≤ 1,000,000, at most six decimal places).",[280,322,323,324,327,328,331],{},"A feedback webhook's ",[33,325,326],{},"signature.secretEnvVar"," must also be listed in ",[33,329,330],{},"flag.requiredEnvVars",". The route it feeds cannot verify anything without the secret, so enablement — not every individual delivery — is where its absence has to be caught.",[280,333,334],{},"Ceilings: 64 capabilities, 64 required env vars, 256 entries per contribution bucket, 64 settings fields.",[21,336,338],{"id":337},"_3-add-contributions","3. Add contributions",[11,340,341,342,345,346,349],{},"Every contribution is a data descriptor whose executable half lives at one ",[114,343,344],{},"condition-independent package export",", for example ",[33,347,348],{},".\u002Fgate",". Codegen verifies the export exists without running it and emits the static import.",[11,351,352,353,356,357,359],{},"The manifest below grows the same ",[33,354,355],{},"manifest.ts",", so it adds only the capability constants — ",[33,358,242],{}," is already imported above.",[26,361,363],{"className":120,"code":362,"language":122,"meta":31,"style":31},"import {\n    PLUGIN_AUTONOMY_GATE_CAPABILITY,\n    PLUGIN_CRON_CAPABILITY,\n    PLUGIN_NAV_ITEM_CAPABILITY,\n    PLUGIN_SETTINGS_PANEL_CAPABILITY,\n    PLUGIN_WORKER_CAPABILITY,\n} from '@owlat\u002Fplugin-kit';\n\nexport const preflightPlugin = definePlugin({\n    id: 'preflight',\n    version: '1.0.0',\n    capabilities: [\n        PLUGIN_AUTONOMY_GATE_CAPABILITY,\n        PLUGIN_CRON_CAPABILITY,\n        PLUGIN_NAV_ITEM_CAPABILITY,\n        PLUGIN_SETTINGS_PANEL_CAPABILITY,\n        PLUGIN_WORKER_CAPABILITY,\n        'llm:invoke',\n    ],\n    \u002F\u002F Contributions REQUIRE an explicit flag: an operator must be able to turn\n    \u002F\u002F the plugin off without a redeploy.\n    flag: { default: false, requiredEnvVars: ['PREFLIGHT_API_KEY'] },\n    \u002F\u002F `llm:invoke` additionally requires a hard daily budget in USD.\n    llmBudget: { dailyUsd: 1.5 },\n    contributes: {\n        sendGates: [\n            {\n                id: 'preflight',\n                label: 'Pre-send preflight',\n                module: { exportPath: '.\u002Fgate' },\n                timeoutMs: 15_000,\n            },\n        ],\n        crons: [\n            {\n                id: 'refresh-rules',\n                label: 'Refresh preflight rules',\n                module: { exportPath: '.\u002Fcron' },\n                schedule: { intervalMinutes: 360 },\n                timeoutMs: 60_000,\n            },\n        ],\n        navItems: [\n            {\n                id: 'dashboard',\n                section: 'administration',\n                name: 'Preflight',\n                href: '\u002Fdashboard\u002Fadmin\u002Finstance\u002Fplugins\u002Fpreflight',\n                icon: 'lucide:radar',\n            },\n        ],\n        settingsPanels: [\n            {\n                id: 'settings',\n                name: 'Preflight',\n                href: '\u002Fdashboard\u002Fadmin\u002Finstance\u002Fplugins\u002Fpreflight',\n                icon: 'lucide:radar',\n            },\n        ],\n    },\n    settingsSchema: [\n        {\n            kind: 'boolean',\n            key: 'holdOnFail',\n            label: 'Hold sends that fail preflight',\n            default: true,\n        },\n        {\n            kind: 'secret',\n            key: 'vendorApiKey',\n            envVar: 'PLUGIN_VENDOR_API_KEY',\n            label: 'Vendor API key',\n            required: false,\n        },\n    ],\n});\n",[33,364,365,372,377,382,387,392,397,408,412,427,437,447,453,461,469,477,485,493,501,507,513,519,535,541,552,558,564,570,580,591,602,613,619,625,631,636,646,656,666,677,687,692,697,703,708,718,729,740,751,762,767,772,778,783,793,802,811,820,825,830,836,842,848,859,870,881,892,898,903,913,923,934,944,954,959,964],{"__ignoreMap":31},[36,366,367,369],{"class":38,"line":39},[36,368,130],{"class":129},[36,370,371],{"class":133}," {\n",[36,373,374],{"class":38,"line":57},[36,375,376],{"class":133},"    PLUGIN_AUTONOMY_GATE_CAPABILITY,\n",[36,378,379],{"class":38,"line":152},[36,380,381],{"class":133},"    PLUGIN_CRON_CAPABILITY,\n",[36,383,384],{"class":38,"line":175},[36,385,386],{"class":133},"    PLUGIN_NAV_ITEM_CAPABILITY,\n",[36,388,389],{"class":38,"line":187},[36,390,391],{"class":133},"    PLUGIN_SETTINGS_PANEL_CAPABILITY,\n",[36,393,394],{"class":38,"line":198},[36,395,396],{"class":133},"    PLUGIN_WORKER_CAPABILITY,\n",[36,398,399,402,404,406],{"class":38,"line":216},[36,400,401],{"class":133},"} ",[36,403,137],{"class":129},[36,405,140],{"class":46},[36,407,143],{"class":133},[36,409,410],{"class":38,"line":222},[36,411,149],{"emptyLinePlaceholder":148},[36,413,414,416,418,421,423,425],{"class":38,"line":234},[36,415,155],{"class":129},[36,417,158],{"class":129},[36,419,420],{"class":161}," preflightPlugin",[36,422,165],{"class":129},[36,424,169],{"class":168},[36,426,172],{"class":133},[36,428,430,432,435],{"class":38,"line":429},10,[36,431,178],{"class":133},[36,433,434],{"class":46},"'preflight'",[36,436,184],{"class":133},[36,438,440,442,445],{"class":38,"line":439},11,[36,441,190],{"class":133},[36,443,444],{"class":46},"'1.0.0'",[36,446,184],{"class":133},[36,448,450],{"class":38,"line":449},12,[36,451,452],{"class":133},"    capabilities: [\n",[36,454,456,459],{"class":38,"line":455},13,[36,457,458],{"class":161},"        PLUGIN_AUTONOMY_GATE_CAPABILITY",[36,460,184],{"class":133},[36,462,464,467],{"class":38,"line":463},14,[36,465,466],{"class":161},"        PLUGIN_CRON_CAPABILITY",[36,468,184],{"class":133},[36,470,472,475],{"class":38,"line":471},15,[36,473,474],{"class":161},"        PLUGIN_NAV_ITEM_CAPABILITY",[36,476,184],{"class":133},[36,478,480,483],{"class":38,"line":479},16,[36,481,482],{"class":161},"        PLUGIN_SETTINGS_PANEL_CAPABILITY",[36,484,184],{"class":133},[36,486,488,491],{"class":38,"line":487},17,[36,489,490],{"class":161},"        PLUGIN_WORKER_CAPABILITY",[36,492,184],{"class":133},[36,494,496,499],{"class":38,"line":495},18,[36,497,498],{"class":46},"        'llm:invoke'",[36,500,184],{"class":133},[36,502,504],{"class":38,"line":503},19,[36,505,506],{"class":133},"    ],\n",[36,508,510],{"class":38,"line":509},20,[36,511,512],{"class":53},"    \u002F\u002F Contributions REQUIRE an explicit flag: an operator must be able to turn\n",[36,514,516],{"class":38,"line":515},21,[36,517,518],{"class":53},"    \u002F\u002F the plugin off without a redeploy.\n",[36,520,522,524,526,529,532],{"class":38,"line":521},22,[36,523,225],{"class":133},[36,525,228],{"class":161},[36,527,528],{"class":133},", requiredEnvVars: [",[36,530,531],{"class":46},"'PREFLIGHT_API_KEY'",[36,533,534],{"class":133},"] },\n",[36,536,538],{"class":38,"line":537},23,[36,539,540],{"class":53},"    \u002F\u002F `llm:invoke` additionally requires a hard daily budget in USD.\n",[36,542,544,547,550],{"class":38,"line":543},24,[36,545,546],{"class":133},"    llmBudget: { dailyUsd: ",[36,548,549],{"class":161},"1.5",[36,551,231],{"class":133},[36,553,555],{"class":38,"line":554},25,[36,556,557],{"class":133},"    contributes: {\n",[36,559,561],{"class":38,"line":560},26,[36,562,563],{"class":133},"        sendGates: [\n",[36,565,567],{"class":38,"line":566},27,[36,568,569],{"class":133},"            {\n",[36,571,573,576,578],{"class":38,"line":572},28,[36,574,575],{"class":133},"                id: ",[36,577,434],{"class":46},[36,579,184],{"class":133},[36,581,583,586,589],{"class":38,"line":582},29,[36,584,585],{"class":133},"                label: ",[36,587,588],{"class":46},"'Pre-send preflight'",[36,590,184],{"class":133},[36,592,594,597,600],{"class":38,"line":593},30,[36,595,596],{"class":133},"                module: { exportPath: ",[36,598,599],{"class":46},"'.\u002Fgate'",[36,601,231],{"class":133},[36,603,605,608,611],{"class":38,"line":604},31,[36,606,607],{"class":133},"                timeoutMs: ",[36,609,610],{"class":161},"15_000",[36,612,184],{"class":133},[36,614,616],{"class":38,"line":615},32,[36,617,618],{"class":133},"            },\n",[36,620,622],{"class":38,"line":621},33,[36,623,624],{"class":133},"        ],\n",[36,626,628],{"class":38,"line":627},34,[36,629,630],{"class":133},"        crons: [\n",[36,632,634],{"class":38,"line":633},35,[36,635,569],{"class":133},[36,637,639,641,644],{"class":38,"line":638},36,[36,640,575],{"class":133},[36,642,643],{"class":46},"'refresh-rules'",[36,645,184],{"class":133},[36,647,649,651,654],{"class":38,"line":648},37,[36,650,585],{"class":133},[36,652,653],{"class":46},"'Refresh preflight rules'",[36,655,184],{"class":133},[36,657,659,661,664],{"class":38,"line":658},38,[36,660,596],{"class":133},[36,662,663],{"class":46},"'.\u002Fcron'",[36,665,231],{"class":133},[36,667,669,672,675],{"class":38,"line":668},39,[36,670,671],{"class":133},"                schedule: { intervalMinutes: ",[36,673,674],{"class":161},"360",[36,676,231],{"class":133},[36,678,680,682,685],{"class":38,"line":679},40,[36,681,607],{"class":133},[36,683,684],{"class":161},"60_000",[36,686,184],{"class":133},[36,688,690],{"class":38,"line":689},41,[36,691,618],{"class":133},[36,693,695],{"class":38,"line":694},42,[36,696,624],{"class":133},[36,698,700],{"class":38,"line":699},43,[36,701,702],{"class":133},"        navItems: [\n",[36,704,706],{"class":38,"line":705},44,[36,707,569],{"class":133},[36,709,711,713,716],{"class":38,"line":710},45,[36,712,575],{"class":133},[36,714,715],{"class":46},"'dashboard'",[36,717,184],{"class":133},[36,719,721,724,727],{"class":38,"line":720},46,[36,722,723],{"class":133},"                section: ",[36,725,726],{"class":46},"'administration'",[36,728,184],{"class":133},[36,730,732,735,738],{"class":38,"line":731},47,[36,733,734],{"class":133},"                name: ",[36,736,737],{"class":46},"'Preflight'",[36,739,184],{"class":133},[36,741,743,746,749],{"class":38,"line":742},48,[36,744,745],{"class":133},"                href: ",[36,747,748],{"class":46},"'\u002Fdashboard\u002Fadmin\u002Finstance\u002Fplugins\u002Fpreflight'",[36,750,184],{"class":133},[36,752,754,757,760],{"class":38,"line":753},49,[36,755,756],{"class":133},"                icon: ",[36,758,759],{"class":46},"'lucide:radar'",[36,761,184],{"class":133},[36,763,765],{"class":38,"line":764},50,[36,766,618],{"class":133},[36,768,770],{"class":38,"line":769},51,[36,771,624],{"class":133},[36,773,775],{"class":38,"line":774},52,[36,776,777],{"class":133},"        settingsPanels: [\n",[36,779,781],{"class":38,"line":780},53,[36,782,569],{"class":133},[36,784,786,788,791],{"class":38,"line":785},54,[36,787,575],{"class":133},[36,789,790],{"class":46},"'settings'",[36,792,184],{"class":133},[36,794,796,798,800],{"class":38,"line":795},55,[36,797,734],{"class":133},[36,799,737],{"class":46},[36,801,184],{"class":133},[36,803,805,807,809],{"class":38,"line":804},56,[36,806,745],{"class":133},[36,808,748],{"class":46},[36,810,184],{"class":133},[36,812,814,816,818],{"class":38,"line":813},57,[36,815,756],{"class":133},[36,817,759],{"class":46},[36,819,184],{"class":133},[36,821,823],{"class":38,"line":822},58,[36,824,618],{"class":133},[36,826,828],{"class":38,"line":827},59,[36,829,624],{"class":133},[36,831,833],{"class":38,"line":832},60,[36,834,835],{"class":133},"    },\n",[36,837,839],{"class":38,"line":838},61,[36,840,841],{"class":133},"    settingsSchema: [\n",[36,843,845],{"class":38,"line":844},62,[36,846,847],{"class":133},"        {\n",[36,849,851,854,857],{"class":38,"line":850},63,[36,852,853],{"class":133},"            kind: ",[36,855,856],{"class":46},"'boolean'",[36,858,184],{"class":133},[36,860,862,865,868],{"class":38,"line":861},64,[36,863,864],{"class":133},"            key: ",[36,866,867],{"class":46},"'holdOnFail'",[36,869,184],{"class":133},[36,871,873,876,879],{"class":38,"line":872},65,[36,874,875],{"class":133},"            label: ",[36,877,878],{"class":46},"'Hold sends that fail preflight'",[36,880,184],{"class":133},[36,882,884,887,890],{"class":38,"line":883},66,[36,885,886],{"class":133},"            default: ",[36,888,889],{"class":161},"true",[36,891,184],{"class":133},[36,893,895],{"class":38,"line":894},67,[36,896,897],{"class":133},"        },\n",[36,899,901],{"class":38,"line":900},68,[36,902,847],{"class":133},[36,904,906,908,911],{"class":38,"line":905},69,[36,907,853],{"class":133},[36,909,910],{"class":46},"'secret'",[36,912,184],{"class":133},[36,914,916,918,921],{"class":38,"line":915},70,[36,917,864],{"class":133},[36,919,920],{"class":46},"'vendorApiKey'",[36,922,184],{"class":133},[36,924,926,929,932],{"class":38,"line":925},71,[36,927,928],{"class":133},"            envVar: ",[36,930,931],{"class":46},"'PLUGIN_VENDOR_API_KEY'",[36,933,184],{"class":133},[36,935,937,939,942],{"class":38,"line":936},72,[36,938,875],{"class":133},[36,940,941],{"class":46},"'Vendor API key'",[36,943,184],{"class":133},[36,945,947,950,952],{"class":38,"line":946},73,[36,948,949],{"class":133},"            required: ",[36,951,228],{"class":161},[36,953,184],{"class":133},[36,955,957],{"class":38,"line":956},74,[36,958,897],{"class":133},[36,960,962],{"class":38,"line":961},75,[36,963,506],{"class":133},[36,965,967],{"class":38,"line":966},76,[36,968,237],{"class":133},[11,970,971,972,975,976,979,980,983,984,207,987,207,990,207,993,207,996,207,999,207,1002,207,1005,207,1008,1011,1012,1015,1016,1018],{},"A ",[33,973,974],{},"navItems"," entry's ",[33,977,978],{},"section"," must be one of the ",[114,981,982],{},"core sidebar section keys"," — ",[33,985,986],{},"inbox",[33,988,989],{},"postbox",[33,991,992],{},"chat",[33,994,995],{},"assistant",[33,997,998],{},"send",[33,1000,1001],{},"audience",[33,1003,1004],{},"knowledge",[33,1006,1007],{},"administration",[33,1009,1010],{},"preferences",". A plugin cannot create a section, and the host drops an entry naming an unknown one fail-closed, so a wrong key silently never renders. Existing plugins that still target the former ",[33,1013,1014],{},"settings"," key are attached to ",[33,1017,1007],{}," during the compatibility window.",[11,1020,1021,1022,97],{},"Every bucket and its semantics are listed in the ",[15,1023,1025],{"href":1024},"\u002Fdeveloper\u002Fplugin-contributions","Contribution Reference",[21,1027,1029],{"id":1028},"_4-write-the-modules","4. Write the modules",[11,1031,1032,1033,1036],{},"A module implements one small interface and receives ",[114,1034,1035],{},"host-mediated services only"," — never a Convex context, database handle, environment variable, credential, tenant id, or scheduler reference.",[26,1038,1040],{"className":120,"code":1039,"language":122,"meta":31,"style":31},"import type {\n    PluginAutonomyGateInput,\n    PluginAutonomyGateModule,\n    PluginAutonomyGateResult,\n    PluginAutonomyGateServices,\n} from '@owlat\u002Fplugin-kit';\n\n\u002F** A gate may object or stand aside. There is deliberately no \"approve\" result. *\u002F\nexport const gate: PluginAutonomyGateModule = {\n    async evaluate(\n        input: PluginAutonomyGateInput,\n        services: PluginAutonomyGateServices\n    ): Promise\u003CPluginAutonomyGateResult> {\n        if (services.signal.aborted) return { outcome: 'objection', reason: 'preflight cancelled' };\n        if (input.draftBody.includes('http:\u002F\u002F')) {\n            return { outcome: 'objection', reason: 'draft contains a plaintext HTTP link' };\n        }\n        return { outcome: 'no-objection' };\n    },\n};\n",[33,1041,1042,1051,1056,1061,1066,1071,1081,1085,1090,1109,1120,1133,1143,1162,1188,1207,1223,1228,1240,1244],{"__ignoreMap":31},[36,1043,1044,1046,1049],{"class":38,"line":39},[36,1045,130],{"class":129},[36,1047,1048],{"class":129}," type",[36,1050,371],{"class":133},[36,1052,1053],{"class":38,"line":57},[36,1054,1055],{"class":133},"    PluginAutonomyGateInput,\n",[36,1057,1058],{"class":38,"line":152},[36,1059,1060],{"class":133},"    PluginAutonomyGateModule,\n",[36,1062,1063],{"class":38,"line":175},[36,1064,1065],{"class":133},"    PluginAutonomyGateResult,\n",[36,1067,1068],{"class":38,"line":187},[36,1069,1070],{"class":133},"    PluginAutonomyGateServices,\n",[36,1072,1073,1075,1077,1079],{"class":38,"line":198},[36,1074,401],{"class":133},[36,1076,137],{"class":129},[36,1078,140],{"class":46},[36,1080,143],{"class":133},[36,1082,1083],{"class":38,"line":216},[36,1084,149],{"emptyLinePlaceholder":148},[36,1086,1087],{"class":38,"line":222},[36,1088,1089],{"class":53},"\u002F** A gate may object or stand aside. There is deliberately no \"approve\" result. *\u002F\n",[36,1091,1092,1094,1096,1099,1102,1105,1107],{"class":38,"line":234},[36,1093,155],{"class":129},[36,1095,158],{"class":129},[36,1097,1098],{"class":161}," gate",[36,1100,1101],{"class":129},":",[36,1103,1104],{"class":42}," PluginAutonomyGateModule",[36,1106,165],{"class":129},[36,1108,371],{"class":133},[36,1110,1111,1114,1117],{"class":38,"line":429},[36,1112,1113],{"class":129},"    async",[36,1115,1116],{"class":168}," evaluate",[36,1118,1119],{"class":133},"(\n",[36,1121,1122,1126,1128,1131],{"class":38,"line":439},[36,1123,1125],{"class":1124},"stnAF","        input",[36,1127,1101],{"class":129},[36,1129,1130],{"class":42}," PluginAutonomyGateInput",[36,1132,184],{"class":133},[36,1134,1135,1138,1140],{"class":38,"line":449},[36,1136,1137],{"class":1124},"        services",[36,1139,1101],{"class":129},[36,1141,1142],{"class":42}," PluginAutonomyGateServices\n",[36,1144,1145,1148,1150,1153,1156,1159],{"class":38,"line":455},[36,1146,1147],{"class":133},"    )",[36,1149,1101],{"class":129},[36,1151,1152],{"class":42}," Promise",[36,1154,1155],{"class":133},"\u003C",[36,1157,1158],{"class":42},"PluginAutonomyGateResult",[36,1160,1161],{"class":133},"> {\n",[36,1163,1164,1167,1170,1173,1176,1179,1182,1185],{"class":38,"line":463},[36,1165,1166],{"class":129},"        if",[36,1168,1169],{"class":133}," (services.signal.aborted) ",[36,1171,1172],{"class":129},"return",[36,1174,1175],{"class":133}," { outcome: ",[36,1177,1178],{"class":46},"'objection'",[36,1180,1181],{"class":133},", reason: ",[36,1183,1184],{"class":46},"'preflight cancelled'",[36,1186,1187],{"class":133}," };\n",[36,1189,1190,1192,1195,1198,1201,1204],{"class":38,"line":471},[36,1191,1166],{"class":129},[36,1193,1194],{"class":133}," (input.draftBody.",[36,1196,1197],{"class":168},"includes",[36,1199,1200],{"class":133},"(",[36,1202,1203],{"class":46},"'http:\u002F\u002F'",[36,1205,1206],{"class":133},")) {\n",[36,1208,1209,1212,1214,1216,1218,1221],{"class":38,"line":479},[36,1210,1211],{"class":129},"            return",[36,1213,1175],{"class":133},[36,1215,1178],{"class":46},[36,1217,1181],{"class":133},[36,1219,1220],{"class":46},"'draft contains a plaintext HTTP link'",[36,1222,1187],{"class":133},[36,1224,1225],{"class":38,"line":487},[36,1226,1227],{"class":133},"        }\n",[36,1229,1230,1233,1235,1238],{"class":38,"line":495},[36,1231,1232],{"class":129},"        return",[36,1234,1175],{"class":133},[36,1236,1237],{"class":46},"'no-objection'",[36,1239,1187],{"class":133},[36,1241,1242],{"class":38,"line":503},[36,1243,835],{"class":133},[36,1245,1246],{"class":38,"line":509},[36,1247,1248],{"class":133},"};\n",[11,1250,1251,1252,1254],{},"Background work gets an abort signal, a logger, and — when ",[33,1253,309],{}," is declared, granted, and within budget — the attributed host LLM dispatch:",[26,1256,1258],{"className":120,"code":1257,"language":122,"meta":31,"style":31},"import type { PluginCronModule, PluginCronServices } from '@owlat\u002Fplugin-kit';\n\nexport const cron: PluginCronModule = {\n    async run(services: PluginCronServices): Promise\u003Cvoid> {\n        \u002F\u002F Cancellation is cooperative: the host aborts `signal` at the declared\n        \u002F\u002F timeout and stops waiting either way.\n        if (services.signal.aborted) return;\n        const summary = await services.llm.generate({\n            tier: 'fast',\n            prompt: 'Summarise this week of deliverability tips in one sentence.',\n        });\n        services.logger.info('refreshed preflight rules', { length: summary.text.length });\n    },\n};\n",[33,1259,1260,1275,1279,1297,1327,1332,1337,1347,1368,1378,1388,1393,1415,1419],{"__ignoreMap":31},[36,1261,1262,1264,1266,1269,1271,1273],{"class":38,"line":39},[36,1263,130],{"class":129},[36,1265,1048],{"class":129},[36,1267,1268],{"class":133}," { PluginCronModule, PluginCronServices } ",[36,1270,137],{"class":129},[36,1272,140],{"class":46},[36,1274,143],{"class":133},[36,1276,1277],{"class":38,"line":57},[36,1278,149],{"emptyLinePlaceholder":148},[36,1280,1281,1283,1285,1288,1290,1293,1295],{"class":38,"line":152},[36,1282,155],{"class":129},[36,1284,158],{"class":129},[36,1286,1287],{"class":161}," cron",[36,1289,1101],{"class":129},[36,1291,1292],{"class":42}," PluginCronModule",[36,1294,165],{"class":129},[36,1296,371],{"class":133},[36,1298,1299,1301,1303,1305,1308,1310,1313,1316,1318,1320,1322,1325],{"class":38,"line":175},[36,1300,1113],{"class":129},[36,1302,47],{"class":168},[36,1304,1200],{"class":133},[36,1306,1307],{"class":1124},"services",[36,1309,1101],{"class":129},[36,1311,1312],{"class":42}," PluginCronServices",[36,1314,1315],{"class":133},")",[36,1317,1101],{"class":129},[36,1319,1152],{"class":42},[36,1321,1155],{"class":133},[36,1323,1324],{"class":161},"void",[36,1326,1161],{"class":133},[36,1328,1329],{"class":38,"line":187},[36,1330,1331],{"class":53},"        \u002F\u002F Cancellation is cooperative: the host aborts `signal` at the declared\n",[36,1333,1334],{"class":38,"line":198},[36,1335,1336],{"class":53},"        \u002F\u002F timeout and stops waiting either way.\n",[36,1338,1339,1341,1343,1345],{"class":38,"line":216},[36,1340,1166],{"class":129},[36,1342,1169],{"class":133},[36,1344,1172],{"class":129},[36,1346,143],{"class":133},[36,1348,1349,1352,1355,1357,1360,1363,1366],{"class":38,"line":222},[36,1350,1351],{"class":129},"        const",[36,1353,1354],{"class":161}," summary",[36,1356,165],{"class":129},[36,1358,1359],{"class":129}," await",[36,1361,1362],{"class":133}," services.llm.",[36,1364,1365],{"class":168},"generate",[36,1367,172],{"class":133},[36,1369,1370,1373,1376],{"class":38,"line":234},[36,1371,1372],{"class":133},"            tier: ",[36,1374,1375],{"class":46},"'fast'",[36,1377,184],{"class":133},[36,1379,1380,1383,1386],{"class":38,"line":429},[36,1381,1382],{"class":133},"            prompt: ",[36,1384,1385],{"class":46},"'Summarise this week of deliverability tips in one sentence.'",[36,1387,184],{"class":133},[36,1389,1390],{"class":38,"line":439},[36,1391,1392],{"class":133},"        });\n",[36,1394,1395,1398,1401,1403,1406,1409,1412],{"class":38,"line":449},[36,1396,1397],{"class":133},"        services.logger.",[36,1399,1400],{"class":168},"info",[36,1402,1200],{"class":133},[36,1404,1405],{"class":46},"'refreshed preflight rules'",[36,1407,1408],{"class":133},", { length: summary.text.",[36,1410,1411],{"class":161},"length",[36,1413,1414],{"class":133}," });\n",[36,1416,1417],{"class":38,"line":455},[36,1418,835],{"class":133},[36,1420,1421],{"class":38,"line":463},[36,1422,1248],{"class":133},[11,1424,1425,1426,1429,1430,207,1433,207,1436,207,1439,207,1442,1445,1446,1449,1450,1453],{},"Longer-running hosted code receives a ",[33,1427,1428],{},"PluginContext"," with ",[33,1431,1432],{},"pluginId",[33,1434,1435],{},"permissions",[33,1437,1438],{},"storage",[33,1440,1441],{},"llm",[33,1443,1444],{},"logger",", and ",[33,1447,1448],{},"scheduler",". Storage methods take ",[114,1451,1452],{},"no"," organization or plugin argument: the host has already bound the service to the authenticated organization and the validated plugin, and rechecks the flag and the exact storage grant on every call.",[1455,1456,1459],"callout",{"title":1457,"type":1458},"Design for denial","warning",[11,1460,1461,1462,1465],{},"Any hosted call can be denied at the last moment — the flag may have flipped, the grant may have been revoked, an env var may be missing. Write modules so that a ",[33,1463,1464],{},"PluginHostError"," or a timeout leaves no half-applied state; the host applies the contribution's declared safe fallback regardless.",[21,1467,1469],{"id":1468},"_5-test-the-plugin","5. Test the plugin",[11,1471,1472,1473,1476],{},"Contribution modules are ordinary TypeScript, so they test as units with vitest. Always assert the manifest itself: ",[33,1474,1475],{},"parsePluginManifest"," is the same validator codegen runs, so a manifest test fails the build before a broken package can be composed.",[26,1478,1480],{"className":28,"code":1479,"language":30,"meta":31,"style":31},"bun run --cwd examples\u002Fplugins\u002Fhello-owlat test\nbun run --cwd examples\u002Fplugins\u002Fhello-owlat typecheck\n",[33,1481,1482,1497],{"__ignoreMap":31},[36,1483,1484,1486,1488,1491,1494],{"class":38,"line":39},[36,1485,43],{"class":42},[36,1487,47],{"class":46},[36,1489,1490],{"class":161}," --cwd",[36,1492,1493],{"class":46}," examples\u002Fplugins\u002Fhello-owlat",[36,1495,1496],{"class":46}," test\n",[36,1498,1499,1501,1503,1505,1507],{"class":38,"line":57},[36,1500,43],{"class":42},[36,1502,47],{"class":46},[36,1504,1490],{"class":161},[36,1506,1493],{"class":46},[36,1508,1509],{"class":46}," typecheck\n",[11,1511,1512,1513,1516,1517,1520,1521,1524],{},"The reference plugins are the worked examples, one per tier: ",[33,1514,1515],{},"examples\u002Fplugins\u002Fescalation-guard"," (Tier 1 — agent step, draft strategy, automation trigger\u002Fstep\u002Fcondition, webhook event, nav and settings entries), ",[33,1518,1519],{},"examples\u002Fplugins\u002Fslack-approvals"," (Tier 2 — a connected app with a restrict-only hold gate), and ",[33,1522,1523],{},"examples\u002Fplugins\u002Fdeliverability-lab"," (Tier 3 — a sandboxed seed-list job, plus a Tier-1 gate, cron and UI).",[11,1526,1527,1530],{},[33,1528,1529],{},"examples\u002Fconformance"," then replays all three through the real host, codegen and CLI — clean install, add, remove, disable, upgrade, and a full pipeline replay — so a contract change breaks the gallery in the same commit.",[1455,1532,1535],{"title":1533,"type":1534},"Never use vitest's globals via bun test","danger",[11,1536,1537,1538,1541,1542,1545],{},"Run tests with ",[33,1539,1540],{},"vitest",", not ",[33,1543,1544],{},"bun test",". The workspace's vitest setup is what makes the suites meaningful.",[21,1547,1549],{"id":1548},"_6-bundle-it","6. Bundle it",[26,1551,1553],{"className":28,"code":1552,"language":30,"meta":31,"style":31},"bun packages\u002Fplugin-cli\u002Fsrc\u002Findex.ts add @owlat\u002Fplugin-hello-owlat\nbun run plugins:codegen\n",[33,1554,1555,1567],{"__ignoreMap":31},[36,1556,1557,1559,1561,1564],{"class":38,"line":39},[36,1558,43],{"class":42},[36,1560,62],{"class":46},[36,1562,1563],{"class":46}," add",[36,1565,1566],{"class":46}," @owlat\u002Fplugin-hello-owlat\n",[36,1568,1569,1571,1573],{"class":38,"line":57},[36,1570,43],{"class":42},[36,1572,47],{"class":46},[36,1574,1575],{"class":46}," plugins:codegen\n",[11,1577,1578,1581,1582,1585,1586,1589,1590,1592],{},[33,1579,1580],{},"add"," edits the checked-in ",[33,1583,1584],{},"plugins.config.ts"," and prints the capability diff the change introduces. ",[33,1587,1588],{},"codegen"," verifies package identity, lockfile integrity and realpath containment, imports only the manifest entry, and rewrites the generated composition files. Commit the config ",[114,1591,286],{}," the generated files together, then rebuild and deploy.",[11,1594,1595],{},"The plugin now exists in the deployment but does nothing: its feature flag is at its declared default and its capabilities are ungranted until an operator acts.",[21,1597,1599],{"id":1598},"publishing-checklist","Publishing checklist",[277,1601,1602,1605,1612,1619,1624],{},[280,1603,1604],{},"The manifest's default export path is one condition-independent root export, so Bun, Convex, Nuxt SSR, and the browser build all resolve the same manifest.",[280,1606,1607,1608,1611],{},"No ",[33,1609,1610],{},"postinstall"," or side effects at import time — codegen imports the manifest entry.",[280,1613,1614,1615,1618],{},"Every contribution's ",[33,1616,1617],{},"module.exportPath"," is a real, exact package export.",[280,1620,1621,1623],{},[33,1622,330],{}," lists only variables a shipped module actually needs; a missing required variable blocks enablement.",[280,1625,1626,1627,1630],{},"Version bumps follow semver; the ",[33,1628,1629],{},"@owlat\u002Fplugin-kit"," major you build against is your compatibility line.",[1632,1633,1634],"style",{},"html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}",{"title":31,"searchDepth":57,"depth":57,"links":1636},[1637,1638,1641,1642,1643,1644,1645],{"id":23,"depth":57,"text":24},{"id":108,"depth":57,"text":109,"children":1639},[1640],{"id":274,"depth":152,"text":275},{"id":337,"depth":57,"text":338},{"id":1028,"depth":57,"text":1029},{"id":1468,"depth":57,"text":1469},{"id":1548,"depth":57,"text":1549},{"id":1598,"depth":57,"text":1599},"Scaffold a plugin package, declare a manifest, write contribution modules, and bundle it into a deployment.","md",{},"\u002Fdeveloper\u002Fplugin-authoring",{"title":6,"description":1646},"3.developer\u002F41.plugin-authoring","K_aO_mdw24xprz7xmRiENHAOqfhfMw3skPw9-SBzc80",[1654,1658],{"title":1655,"path":1656,"stem":1657,"children":-1},"Plugin Platform","\u002Fdeveloper\u002Fplugin-platform","3.developer\u002F40.plugin-platform",{"title":1025,"path":1024,"stem":1659,"children":-1},"3.developer\u002F42.plugin-contributions",1786915092311]