[{"data":1,"prerenderedAt":534},["ShallowReactive",2],{"search-en":3,"content-en-developer\u002Fplugin-sandboxed-jobs":4,"surround-en-\u002Fdeveloper\u002Fplugin-sandboxed-jobs":525},[],{"id":5,"title":6,"body":7,"description":517,"extension":518,"meta":519,"navigation":520,"path":521,"seo":522,"stem":523,"__hash__":524},"content_en\u002F3.developer\u002F47.plugin-sandboxed-jobs.md","Sandboxed Plugin Jobs",{"type":8,"value":9,"toc":508},"minimark",[10,19,22,27,38,58,62,129,132,136,146,197,204,208,219,263,270,387,395,399,406,440,443,447,488,491,504],[11,12,13,14,18],"p",{},"Tier 3 specifies compute that should not run inside Convex: untrusted input parsing, heavy analysis, anything that could hang or blow up. The worker-side polling, lease and outcome protocol remains implemented, but ",[15,16,17],"strong",{},"no Convex enqueue adapter or operator job UI is shipped",", so plugins cannot start jobs today.",[11,20,21],{},"The same container actively serves the coding-agent queue. Its dormant plugin poller uses the same hardened executor rather than creating a parallel security boundary.",[23,24,26],"h2",{"id":25},"what-a-plugin-can-and-cannot-do","What a plugin can and cannot do",[11,28,29,33,34,37],{},[30,31,32],"code",{},"worker:enqueue"," is reserved. A future host adapter may grant ",[15,35,36],{},"enqueue only","; claiming, cancelling, reclaiming, and reading must remain host and operator operations.",[11,39,40,41,44,45,48,49,53,54,57],{},"A job kind is ",[30,42,43],{},"plugin.\u003CpluginId>.\u003ClocalId>",". The host checks kind ownership from the string itself, so a plugin attempting to enqueue ",[30,46,47],{},"plugin.other-plugin.job"," is denied. The plugin chooses ",[50,51,52],"em",{},"which"," built-in job kind to run; the ",[15,55,56],{},"command is host-controlled"," and is never built from the payload. The untrusted payload is always passed as a discrete argv element, never interpolated into a shell string.",[23,59,61],{"id":60},"enqueue-limits","Enqueue limits",[63,64,65,78],"table",{},[66,67,68],"thead",{},[69,70,71,75],"tr",{},[72,73,74],"th",{},"Limit",[72,76,77],{},"Value",[79,80,81,90,98,106,114],"tbody",{},[69,82,83,87],{},[84,85,86],"td",{},"Payload",[84,88,89],{},"≤ 64 KiB (oversized ⇒ rejected at enqueue)",[69,91,92,95],{},[84,93,94],{},"Stored result",[84,96,97],{},"≤ 64 KiB (oversized ⇒ truncated before persisting)",[69,99,100,103],{},[84,101,102],{},"Attempts",[84,104,105],{},"clamped to 1 … 5",[69,107,108,111],{},[84,109,110],{},"Single-execution wall clock",[84,112,113],{},"clamped to 1 s … 15 min",[69,115,116,119],{},[84,117,118],{},"In-flight jobs per (organization, plugin)",[84,120,121,122,125,126],{},"100 ",[30,123,124],{},"queued"," + ",[30,127,128],{},"running",[11,130,131],{},"A future enqueue adapter must fail closed for a disabled, ungranted, or undeclared plugin, a cross-plugin job kind, an oversized payload, or an exhausted in-flight budget. Terminal jobs do not count against the in-flight cap.",[23,133,135],{"id":134},"job-lifecycle","Job lifecycle",[137,138,144],"pre",{"className":139,"code":141,"language":142,"meta":143},[140],"language-text","queued ──claim──▶ running ──complete──▶ succeeded\n   │                 │\n   │                 ├──fail (attempts \u003C max)──▶ queued\n   │                 ├──fail (attempts = max)──▶ failed\n   │                 └──cancel requested──────▶ cancelled\n   └──cancel requested, then claim───────────▶ cancelled (never runs)\n","text","",[30,145,141],{"__ignoreMap":143},[147,148,149,156,166,172,186],"ul",{},[150,151,152,155],"li",{},[15,153,154],{},"Heartbeats"," prove liveness and are how a worker learns of an operator cancel. A cancelled running job's next heartbeat tells the worker to kill the whole process group.",[150,157,158,161,162,165],{},[15,159,160],{},"Cancellation cannot be escaped"," and a cancelled job is ",[15,163,164],{},"never"," retried.",[150,167,168,171],{},[15,169,170],{},"Lease reclaim"," returns jobs abandoned by a crashed or restarted worker. The lease window is generously beyond the maximum execution budget, so a slow-but-alive job that keeps heartbeating is never reclaimed out from under itself; each sweep is bounded.",[150,173,174,177,178,181,182,185],{},[15,175,176],{},"Retries are bounded"," by the host-clamped ",[30,179,180],{},"maxAttempts",", so a poison job terminates as ",[30,183,184],{},"failed"," instead of looping forever.",[150,187,188,189,192,193,196],{},"Failure reasons are the fixed codes ",[30,190,191],{},"worker_failed"," and ",[30,194,195],{},"worker_timeout","; any error message is clamped.",[11,198,199,200,203],{},"Enqueue and every terminal outcome write a ",[30,201,202],{},"pluginId","-attributed audit row.",[23,205,207],{"id":206},"the-sandbox","The sandbox",[11,209,210,211,214,215,218],{},"The worker orchestrator holds the deployment admin key (and, for the coding-agent queue, ",[30,212,213],{},"GITHUB_TOKEN"," and the LLM key) and runs as ",[15,216,217],{},"confined root"," purely so it can drop privileges. Every untrusted child is spawned through one seam that always:",[147,220,221,232,239,250,260],{},[150,222,223,224,227,228,231],{},"drops to the unprivileged ",[30,225,226],{},"sandbox"," uid\u002Fgid (10001), a cross-uid kernel boundary the child cannot cross to read ",[30,229,230],{},"\u002Fproc\u002F\u003Corchestrator>\u002Fenviron"," or ptrace the orchestrator;",[150,233,234,235,238],{},"uses ",[30,236,237],{},"shell: false"," argv — no shell interpolation;",[150,240,241,242,245,246,249],{},"runs ",[30,243,244],{},"detached",", so the child leads its own process group and a timeout or cancel reaps ",[15,247,248],{},"the whole group",", not just the direct child;",[150,251,252,253,256,257,259],{},"runs with ",[15,254,255],{},"every ambient credential stripped from the job environment",". A plugin job never sees the admin key, ",[30,258,213],{},", or the LLM key. Any credentialed capability is mediated by the host over Convex, never by leaking env into the sandbox;",[150,261,262],{},"is killed at the host-clamped wall-clock deadline (fail-closed).",[11,264,265,266,269],{},"Container-level ceilings, from ",[30,267,268],{},"docker-compose.yml",":",[63,271,272,282],{},[66,273,274],{},[69,275,276,279],{},[72,277,278],{},"Control",[72,280,281],{},"Default",[79,283,284,298,311,324,345,364,375],{},[69,285,286,291],{},[84,287,288],{},[30,289,290],{},"mem_limit",[84,292,293,294,297],{},"2 g (",[30,295,296],{},"CODE_WORKER_MEM_LIMIT",")",[69,299,300,305],{},[84,301,302],{},[30,303,304],{},"cpus",[84,306,307,308,297],{},"1.0 (",[30,309,310],{},"CODE_WORKER_CPUS",[69,312,313,318],{},[84,314,315],{},[30,316,317],{},"pids_limit",[84,319,320,321,297],{},"512 (",[30,322,323],{},"CODE_WORKER_PIDS_LIMIT",[69,325,326,329],{},[84,327,328],{},"Capabilities",[84,330,331,334,335,338,339,338,342],{},[30,332,333],{},"cap_drop: ALL",", then only ",[30,336,337],{},"SETUID",", ",[30,340,341],{},"SETGID",[30,343,344],{},"CHOWN",[69,346,347,350],{},[84,348,349],{},"Filesystem",[84,351,352,355,356,359,360,363],{},[30,353,354],{},"read_only: true",", writes confined to the ",[30,357,358],{},"code-workspace"," volume and a 256 MiB ",[30,361,362],{},"\u002Ftmp"," tmpfs",[69,365,366,369],{},[84,367,368],{},"Privilege escalation",[84,370,371,374],{},[30,372,373],{},"no-new-privileges: true","; the default Docker seccomp profile stays applied",[69,376,377,380],{},[84,378,379],{},"Network",[84,381,382,383,386],{},"The isolated ",[30,384,385],{},"code-worker"," bridge, shared only with Convex — no route to the MTA admin port, IMAP, Redis, mail-sync, clamd, the updater, or the docker socket proxy",[388,389,392],"callout",{"title":390,"type":391},"Do not put a plugin job on the default network","danger",[11,393,394],{},"The worker executes attacker-influenced code. Its network isolation is what cuts lateral movement into the mail and infrastructure tier. Adding it to the shared bridge would undo the whole boundary.",[23,396,398],{"id":397},"built-in-job-kinds","Built-in job kinds",[11,400,401,402,405],{},"The job-kind registry lives in the worker and is host-controlled, keyed by the ",[15,403,404],{},"local"," id:",[63,407,408,418],{},[66,409,410],{},[69,411,412,415],{},[72,413,414],{},"Local id",[72,416,417],{},"Command",[79,419,420,430],{},[69,421,422,427],{},[84,423,424],{},[30,425,426],{},"selftest",[84,428,429],{},"A no-op that exits 0 — proves the uid drop, env stripping, timeout, and cancellation wiring end to end without any plugin code in the image",[69,431,432,437],{},[84,433,434],{},[30,435,436],{},"seed-test",[84,438,439],{},"The Deliverability Lab seed-list placement test; the compiled shim is the command and the untrusted payload arrives as a discrete argv element",[11,441,442],{},"A new Tier-3 job kind is a change to the worker image, reviewed like any other host code — not something a plugin package can introduce on its own.",[23,444,446],{"id":445},"operating-the-worker","Operating the worker",[137,448,452],{"className":449,"code":450,"language":451,"meta":143,"style":143},"language-sh shiki shiki-themes github-light github-dark-dimmed","COMPOSE_PROFILES=plugin-tasks docker compose up -d code-worker\n","sh",[30,453,454],{"__ignoreMap":143},[455,456,459,463,467,471,475,478,481,485],"span",{"class":457,"line":458},"line",1,[455,460,462],{"class":461},"sYgZi","COMPOSE_PROFILES",[455,464,466],{"class":465},"s7YZ4","=",[455,468,470],{"class":469},"s-HuK","plugin-tasks",[455,472,474],{"class":473},"sOLd2"," docker",[455,476,477],{"class":469}," compose",[455,479,480],{"class":469}," up",[455,482,484],{"class":483},"sviXB"," -d",[455,486,487],{"class":469}," code-worker\n",[11,489,490],{},"The worker can authenticate to Convex with the deployment admin key and poll the dormant queue protocol, but current host code creates no rows. There is no recent-jobs query or cancellation UI.",[388,492,495],{"title":493,"type":494},"Protocol only: no enqueue adapter","warning",[11,496,497,498,500,501,503],{},"Enabling the ",[30,499,470],{}," profile starts a poller, but no shipped host path can enqueue work. Declaring ",[30,502,32],{}," has no runtime effect until an adapter lands with a concrete producer and operator surface.",[505,506,507],"style",{},"html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}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);}",{"title":143,"searchDepth":509,"depth":509,"links":510},2,[511,512,513,514,515,516],{"id":25,"depth":509,"text":26},{"id":60,"depth":509,"text":61},{"id":134,"depth":509,"text":135},{"id":206,"depth":509,"text":207},{"id":397,"depth":509,"text":398},{"id":445,"depth":509,"text":446},"Tier 3: untrusted or heavy plugin compute behind an OS process boundary with no ambient credentials.","md",{},true,"\u002Fdeveloper\u002Fplugin-sandboxed-jobs",{"title":6,"description":517},"3.developer\u002F47.plugin-sandboxed-jobs","d86Zw8DVaplxdYkomy_1gFzHt8WK8RxOnyUNVxlsDc0",[526,530],{"title":527,"path":528,"stem":529,"children":-1},"Connected Apps & Signed Hooks","\u002Fdeveloper\u002Fplugin-connected-apps","3.developer\u002F46.plugin-connected-apps",{"title":531,"path":532,"stem":533,"children":-1},"Plugin Troubleshooting","\u002Fdeveloper\u002Fplugin-troubleshooting","3.developer\u002F48.plugin-troubleshooting",1786915092856]