[{"data":1,"prerenderedAt":748},["ShallowReactive",2],{"search-en":3,"content-en-developer\u002Fplugin-capabilities":4,"surround-en-\u002Fdeveloper\u002Fplugin-capabilities":740},[],{"id":5,"title":6,"body":7,"description":732,"extension":733,"meta":734,"navigation":735,"path":736,"seo":737,"stem":738,"__hash__":739},"content_en\u002F3.developer\u002F43.plugin-capabilities.md","Capabilities, Grants & Trust",{"type":8,"value":9,"toc":712},"minimark",[10,15,33,43,46,61,66,219,256,268,272,279,305,325,329,336,409,416,420,432,493,496,500,513,516,523,527,534,551,559,574,578,589,593,597,604,613,617,624,629,633,644,649,652,656,659],[11,12,14],"h2",{"id":13},"capabilities-are-requests-grants-are-permissions","Capabilities are requests, grants are permissions",[16,17,18,19,23,24,28,29,32],"p",{},"A manifest's ",[20,21,22],"code",{},"capabilities"," array is a ",[25,26,27],"strong",{},"request",": the set of host operations the plugin may ever ask for. An operator's grants are the ",[25,30,31],{},"permission",": which of those requests are allowed for this installation.",[34,35,41],"pre",{"className":36,"code":38,"language":39,"meta":40},[37],"language-text","manifest capabilities  ⊇  operator grants  ⊇  what actually runs\n","text","",[20,42,38],{"__ignoreMap":40},[16,44,45],{},"Grants can only ever restrict the manifest. There is no mechanism by which an operator, a connected app, an API key, or a plugin can widen a capability beyond what the manifest declared. Declaring a capability never grants it.",[16,47,48,49,52,53,56,57,60],{},"Capability names are lowercase ",[20,50,51],{},"domain:action"," with ",[25,54,55],{},"exact-match"," semantics. Wildcards are not part of the contract. The namespace is open — adding a new contribution seam does not require a ",[20,58,59],{},"@owlat\u002Fplugin-kit"," major just to add a capability literal — and the host remains the sole authority on which capabilities it recognizes and enforces.",[62,63,65],"h3",{"id":64},"the-vocabulary","The vocabulary",[67,68,69,82],"table",{},[70,71,72],"thead",{},[73,74,75,79],"tr",{},[76,77,78],"th",{},"Capability",[76,80,81],{},"Grants",[83,84,85,96,106,116,126,143,153,163,173,186,196,206],"tbody",{},[73,86,87,93],{},[88,89,90],"td",{},[20,91,92],{},"send:transport",[88,94,95],{},"Contribute a send transport",[73,97,98,103],{},[88,99,100],{},[20,101,102],{},"send:gate",[88,104,105],{},"Contribute a restrict-only autonomy gate",[73,107,108,113],{},[88,109,110],{},[20,111,112],{},"agent:step",[88,114,115],{},"Contribute an inbox agent-pipeline step",[73,117,118,123],{},[88,119,120],{},[20,121,122],{},"draft:strategy",[88,124,125],{},"Contribute a draft generation strategy",[73,127,128,140],{},[88,129,130,133,134,133,137],{},[20,131,132],{},"automation:trigger"," \u002F ",[20,135,136],{},"automation:step",[20,138,139],{},"automation:condition",[88,141,142],{},"Contribute to the matching automation registry",[73,144,145,150],{},[88,146,147],{},[20,148,149],{},"webhooks:publish",[88,151,152],{},"Publish namespaced webhook events",[73,154,155,160],{},[88,156,157],{},[20,158,159],{},"imports:provide",[88,161,162],{},"Contribute an import provider",[73,164,165,170],{},[88,166,167],{},[20,168,169],{},"scheduler:cron",[88,171,172],{},"Register a fixed-interval background cron",[73,174,175,183],{},[88,176,177,133,180],{},[20,178,179],{},"ui:navigation",[20,181,182],{},"ui:settings",[88,184,185],{},"Add a sidebar or settings destination",[73,187,188,193],{},[88,189,190],{},[20,191,192],{},"worker:enqueue",[88,194,195],{},"Reserved for Tier-3 sandboxed jobs; no host enqueue adapter is shipped",[73,197,198,203],{},[88,199,200],{},[20,201,202],{},"llm:invoke",[88,204,205],{},"Metered text generation through Owlat's LLM dispatch",[73,207,208,216],{},[88,209,210,133,213],{},[20,211,212],{},"plugin-storage:read",[20,214,215],{},"plugin-storage:write",[88,217,218],{},"Namespaced JSON key\u002Fvalue storage",[16,220,221,222,225,226,225,229,225,232,225,235,238,239,225,242,225,245,225,248,225,251,225,253,255],{},"Connected apps additionally request API scopes, which double as capabilities on a plugin-bound key: ",[20,223,224],{},"contacts:read",", ",[20,227,228],{},"contacts:write",[20,230,231],{},"events:write",[20,233,234],{},"transactional:send",[20,236,237],{},"topics:write"," (also valid on standalone keys) and the Tier-2-only ",[20,240,241],{},"campaigns:read",[20,243,244],{},"mail:read",[20,246,247],{},"knowledge:read",[20,249,250],{},"webhooks:manage",[20,252,212],{},[20,254,215],{},".",[257,258,261],"callout",{"title":259,"type":260},"Tier-2-only scopes fail closed at mint","info",[16,262,263,264,267],{},"A Tier-2-only scope has no standalone-key meaning. Key creation rejects it unless the key is bound to a plugin, so it can only ever exist inside the manifest-ceiling + operator-grant model. Legacy keys with no ",[20,265,266],{},"scopes"," column are deny-all at enforcement.",[62,269,271],{"id":270},"when-enforcement-happens","When enforcement happens",[16,273,274,275,278],{},"Every hosted operation revalidates the whole chain ",[25,276,277],{},"immediately before"," it runs — not once at startup:",[280,281,282,286,289,292,295,298],"ol",{},[283,284,285],"li",{},"singleton organization scope,",[283,287,288],{},"the plugin is a registered part of the build composition,",[283,290,291],{},"the feature flag is on in the resolved flag map,",[283,293,294],{},"the manifest declares the capability,",[283,296,297],{},"the operator grant for that exact capability exists,",[283,299,300,301,304],{},"every ",[20,302,303],{},"flag.requiredEnvVars"," entry is present.",[16,306,307,308,311,312,225,315,225,318,225,321,324],{},"Any failure raises a ",[20,309,310],{},"PluginHostError"," (",[20,313,314],{},"capability_not_declared",[20,316,317],{},"capability_not_granted",[20,319,320],{},"plugin_disabled",[20,322,323],{},"required_environment_missing",", …) and the contribution's declared safe fallback applies. Denial never invokes plugin code. Disabling a plugin or revoking a grant therefore takes effect on the very next operation — no redeploy, no cache to wait out.",[11,326,328],{"id":327},"services-not-framework-contexts","Services, not framework contexts",[16,330,331,332,335],{},"Hosted plugin code receives a ",[20,333,334],{},"PluginContext"," built only from host-mediated services:",[67,337,338,348],{},[70,339,340],{},[73,341,342,345],{},[76,343,344],{},"Service",[76,346,347],{},"Notes",[83,349,350,365,379,389,399],{},[73,351,352,357],{},[88,353,354],{},[20,355,356],{},"permissions",[88,358,359,133,362],{},[20,360,361],{},"has(capability)",[20,363,364],{},"require(capability)",[73,366,367,372],{},[88,368,369],{},[20,370,371],{},"storage",[88,373,374,375,378],{},"Namespaced JSON KV. Methods take ",[25,376,377],{},"no"," organization or plugin argument",[73,380,381,386],{},[88,382,383],{},[20,384,385],{},"llm",[88,387,388],{},"Attributed, budgeted dispatch",[73,390,391,396],{},[88,392,393],{},[20,394,395],{},"logger",[88,397,398],{},"Structured logging",[73,400,401,406],{},[88,402,403],{},[20,404,405],{},"scheduler",[88,407,408],{},"Named-task scheduling",[16,410,411,412,415],{},"It never exposes a Convex ",[20,413,414],{},"ctx",", a database handle, environment values, raw model objects, or arbitrary scheduler function references.",[62,417,419],{"id":418},"storage-isolation-and-limits","Storage isolation and limits",[16,421,422,423,133,425,427,428,431],{},"The host binds the storage service to the authenticated organization and the validated plugin, then rechecks the flag and the exact ",[20,424,212],{},[20,426,215],{}," grant on ",[25,429,430],{},"every"," operation. Entries are indexed by organization, plugin and key; list cursors are authenticated-encrypted and scope-bound.",[67,433,434,444],{},[70,435,436],{},[73,437,438,441],{},[76,439,440],{},"Limit",[76,442,443],{},"Value",[83,445,446,454,461,469,477,485],{},[73,447,448,451],{},[88,449,450],{},"Key",[88,452,453],{},"256 bytes",[73,455,456,458],{},[88,457,443],{},[88,459,460],{},"64 KiB",[73,462,463,466],{},[88,464,465],{},"Entries per (org, plugin)",[88,467,468],{},"1 000",[73,470,471,474],{},[88,472,473],{},"Total bytes per (org, plugin)",[88,475,476],{},"10 MiB",[73,478,479,482],{},[88,480,481],{},"List page size",[88,483,484],{},"100",[73,486,487,490],{},[88,488,489],{},"JSON depth \u002F nodes \u002F array items \u002F object fields",[88,491,492],{},"32 \u002F 4 096 \u002F 1 024 \u002F 1 024",[16,494,495],{},"Quota counters update in the same Convex transaction as each write, so the ceiling cannot be raced.",[62,497,499],{"id":498},"llm-budgets","LLM budgets",[16,501,502,504,505,508,509,512],{},[20,503,202],{}," requires an explicit flag ",[25,506,507],{},"and"," a validated ",[20,510,511],{},"llmBudget.dailyUsd",". The host authorizes before resolving tenant provider config, then rechecks registration, flag, declaration, grant, and budget in the same transaction that reserves spend immediately before dispatch.",[16,514,515],{},"Requests are bounded to 64 KiB of UTF-8 input across at most 32 messages of 32 KiB each, and to 2 048 output tokens. Only exact model identities in a provider-specific admission catalog are accepted, and pricing is trusted only for a provider's built-in endpoint — an explicit base URL, Azure deployment, compatible\u002Flocal server, or unknown provider fails closed.",[16,517,518,519,522],{},"Accounting uses integer micro-USD. A reservation covers every retry the shared dispatch policy allows; success releases unused headroom but retains a maximum for each failed attempt. Missing or malformed usage, a provider failure, an action crash, an accounting failure, or a provider reporting a different model than the one admitted all ",[25,520,521],{},"retain the conservative reservation"," for that UTC day. That availability tradeoff never reopens spend the provider may have billed.",[62,524,526],{"id":525},"untrusted-text","Untrusted text",[16,528,529,530,533],{},"Plugin-produced text is untrusted at every boundary. On the shipped Convex-side boundaries — automation step reasons and autonomy gate reasons — the host clamps the text to a code-point maximum and runs an injection scrub before it can reach a prompt or a stored field. There is deliberately no permissive default policy: each host adapter names the policy for its runtime, and a non-string value, a scrubber that throws, or a scrubber returning a non-string is rejected outright (",[20,531,532],{},"untrusted_output_rejected",").",[16,535,536,537,540,541,546,547,550],{},"Those two are the entire list. The assistant is deliberately absent: no plugin can contribute an assistant tool today — ",[20,538,539],{},"assistantTools"," is a reserved bucket (see ",[542,543,545],"a",{"href":544},"\u002Fdeveloper\u002Fplugin-contributions","Contribution reference",") — and the built-in tools' output is injection-scrubbed by the assistant's own ",[20,548,549],{},"scrubForInjection",", a core control unrelated to this policy.",[16,552,553,554,558],{},"Browser-bound plugin text is a different, narrower control: nav and settings labels are clamped to 64 UTF-16 code units (an astral character counts as two, the same unit the manifest validator bounds the name by) with control and bidi-format characters stripped when the entry is derived. That is spoofing defense — Vue's HTML escaping is what defends against markup — and the injection-scrub policy is ",[555,556,557],"em",{},"not"," applied on that path.",[16,560,561,562,565,566,569,570,573],{},"Hosted mail projections are truncated by Unicode code points: ",[20,563,564],{},"from"," 512, ",[20,567,568],{},"to"," 2 048, ",[20,571,572],{},"subject"," 1 024, each decrypted body or draft 65 536, and the classification an autonomy gate sees 128.",[62,575,577],{"id":576},"audit-attribution","Audit attribution",[16,579,580,581,584,585,588],{},"Every successful host-service operation writes a tenant audit row carrying both ",[20,582,583],{},"organizationId"," and ",[20,586,587],{},"pluginId",". Audit metadata is an allowlisted scalar snapshot: storage keys\u002Fvalues\u002Fprefixes\u002Fcursors, LLM prompts\u002Fmessages\u002Fresults, provider errors, credentials, mail content, and caller-defined metadata never enter the audit boundary. A successful hosted mutation writes its audit row in the same transaction as the state change, so a rollback discards both.",[11,590,592],{"id":591},"the-three-trust-tiers","The three trust tiers",[62,594,596],{"id":595},"tier-1-bundled-plugins","Tier 1 — bundled plugins",[16,598,599,600,603],{},"Operator-installed packages composed at build time. They may contribute backend and frontend modules and may ship an isolated Convex component under an injective ",[20,601,602],{},"plugin_\u003Cid>"," namespace. Runtime flags disable them instantly; installing or removing one requires a rebuild.",[16,605,606,609,610,612],{},[25,607,608],{},"Trust:"," the same as the operator's own deployed code. Capabilities, grants, flags, and host-mediated services reduce authority and make actions attributable. They are ",[25,611,557],{}," a sandbox.",[62,614,616],{"id":615},"tier-2-connected-apps","Tier 2 — connected apps",[16,618,619,620,255],{},"External HTTPS services reached through scoped API keys and webhooks, with a specified signed synchronous-hook protocol. They never execute code inside Convex or Nuxt. The API-key and webhook halves are live; the hook runtime adapter is not shipped. See ",[542,621,623],{"href":622},"\u002Fdeveloper\u002Fplugin-connected-apps","Connected Apps",[16,625,626,628],{},[25,627,608],{}," untrusted network peer. Responses are strictly shape-validated, scrubbed, clamped, size-capped, deadline-bounded, and replay-protected.",[62,630,632],{"id":631},"tier-3-sandboxed-worker-jobs","Tier 3 — sandboxed worker jobs",[16,634,635,636,639,640,255],{},"The worker-side protocol for untrusted or resource-heavy compute remains behind the ",[20,637,638],{},"code-worker"," OS boundary, but no host enqueue adapter is shipped. See ",[542,641,643],{"href":642},"\u002Fdeveloper\u002Fplugin-sandboxed-jobs","Sandboxed Jobs",[16,645,646,648],{},[25,647,608],{}," untrusted process. No ambient credentials, hard resource and time ceilings, host-controlled command, host-authoritative cancellation and retries.",[16,650,651],{},"The same capability vocabulary and host-service principles apply to all three tiers.",[11,653,655],{"id":654},"invariants-a-plugin-can-never-weaken","Invariants a plugin can never weaken",[16,657,658],{},"These hold across every tier and every contribution point. If a change would break one, it is not a plugin feature — it is a bug.",[280,660,661,670,676,682,688,694,700,706],{},[283,662,663,669],{},[25,664,665,666,668],{},"Capabilities are checked at codegen ",[555,667,507],{}," at runtime."," A contribution whose capability is not declared never composes; an operation whose capability is not granted never runs.",[283,671,672,675],{},[25,673,674],{},"Operator grants can only restrict the manifest."," Nothing widens them.",[283,677,678,681],{},[25,679,680],{},"A plugin may add work or caution, never remove it."," It cannot bypass the security scan, remove a safety gate, force approval, or force sending. Gate and hook result types are structurally incapable of approval.",[283,683,684,687],{},[25,685,686],{},"Gate errors and timeouts fail closed."," Missing, disabled, revoked, stale, timed-out, failed, or malformed gates route to human review.",[283,689,690,693],{},[25,691,692],{},"Plugin-produced text is untrusted"," — clamped at every boundary, and injection-scrubbed at every Convex-side boundary.",[283,695,696,699],{},[25,697,698],{},"Plugin LLM access goes through dispatch"," with plugin attribution and a hard daily budget; accounting uncertainty fails closed.",[283,701,702,705],{},[25,703,704],{},"Tenant and plugin storage are isolated",", with the scope bound by the host and rechecked per operation.",[283,707,708,711],{},[25,709,710],{},"No arbitrary runtime JavaScript"," is loaded into Convex or the browser. Instant installation exists only for connected apps and sandboxed jobs.",{"title":40,"searchDepth":713,"depth":713,"links":714},2,[715,720,726,731],{"id":13,"depth":713,"text":14,"children":716},[717,719],{"id":64,"depth":718,"text":65},3,{"id":270,"depth":718,"text":271},{"id":327,"depth":713,"text":328,"children":721},[722,723,724,725],{"id":418,"depth":718,"text":419},{"id":498,"depth":718,"text":499},{"id":525,"depth":718,"text":526},{"id":576,"depth":718,"text":577},{"id":591,"depth":713,"text":592,"children":727},[728,729,730],{"id":595,"depth":718,"text":596},{"id":615,"depth":718,"text":616},{"id":631,"depth":718,"text":632},{"id":654,"depth":713,"text":655},"The plugin permission model, the three trust tiers, and the security invariants no plugin can weaken.","md",{},true,"\u002Fdeveloper\u002Fplugin-capabilities",{"title":6,"description":732},"3.developer\u002F43.plugin-capabilities","FCsZhA53dw3kzdKS5Okaatm8Su6vgY9H1m7apxf-C3Q",[741,744],{"title":742,"path":544,"stem":743,"children":-1},"Contribution Reference","3.developer\u002F42.plugin-contributions",{"title":745,"path":746,"stem":747,"children":-1},"Installing & Operating Plugins","\u002Fdeveloper\u002Fplugin-operations","3.developer\u002F44.plugin-operations",1786915092544]