Make changing engine-run code superuser-only

Writing node source, installing modules and any flow save that adds,
retypes or repoints a code node (Function, Command), edits a Command
node's settings or changes a node's resources.env now answer 403 for
everyone but a superuser. MCP tools and the assistant reach the same
routes, so they inherit the gate. Node types report runs_code.

The editor shows code read-only with a note, disables a Command node's
settings, leaves code nodes and reusable nodes out of the palette and
paste, and the Modules page offers no install.
This commit is contained in:
2026-10-06 11:36:30 +02:00
parent 2914d860b1
commit 6802cd2203
19 changed files with 391 additions and 26 deletions
+51 -10
View File
@@ -42,6 +42,7 @@ import {
SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import useAuth from "@/hooks/useAuth"
import useCustomToast from "@/hooks/useCustomToast"
import { inCodeEditor, useShortcuts } from "@/lib/shortcuts"
import { cn } from "@/lib/utils"
@@ -1204,6 +1205,9 @@ const PLACEHOLDER: Record<DType, string> = {
video: 'fluksio.save_artifact(b"", "clip.mp4", media_type="video/mp4")',
}
/** Why a code editor or an exec node's settings are read-only. */
const CODE_LOCKED = "Only a superuser can change code the engine runs."
const SCAFFOLD_DOC =
'"""A new node. Return a dict keyed by your output ports."""'
@@ -1293,6 +1297,12 @@ function PanelBody({
...nodeSourceQueryOptions(flow, node.id),
enabled: hasSource,
})
// Code the engine runs is a superuser's to change; the server refuses
// anyone else, so the panel shows it read-only rather than failing a save.
const { user } = useAuth()
const mayEditCode = Boolean(user?.is_superuser)
const settingsAreCode =
!mayEditCode && Boolean(nodeType?.runs_code) && !hasSource
const [code, setCode] = useState<string | null>(null)
// The port the X is dropping or its name field renaming, noted just before
@@ -1333,7 +1343,14 @@ function PanelBody({
const editNode = (next: NodeDef_Input) => {
onChange(next)
const current = code ?? source?.code
if (!hasSource || next.source_ref || origin || current === undefined) return
if (
!hasSource ||
!mayEditCode ||
next.source_ref ||
origin ||
current === undefined
)
return
const wanted = scaffoldFor(next)
if (current !== wanted && SCAFFOLD_SHAPE.test(current)) editCode(wanted)
}
@@ -1389,6 +1406,16 @@ function PanelBody({
}
}, [])
const paramsForm = (
<ParamsForm
type={node.type}
schema={nodeType?.params_schema}
params={node.params ?? {}}
provides={node.provides ?? []}
onChange={(params) => editNode({ ...node, params })}
/>
)
return (
<div
className={cn(
@@ -1434,13 +1461,20 @@ function PanelBody({
portMove.current = [portName(spec), null]
}}
/>
<ParamsForm
type={node.type}
schema={nodeType?.params_schema}
params={node.params ?? {}}
provides={node.provides ?? []}
onChange={(params) => editNode({ ...node, params })}
/>
{settingsAreCode ? (
/* An exec node's command is its code, so the whole form is. */
<fieldset disabled className="grid min-w-0 gap-2">
{paramsForm}
<p
className="text-xs text-muted-foreground"
data-testid="node-settings-locked"
>
{CODE_LOCKED}
</p>
</fieldset>
) : (
paramsForm
)}
{nodeType?.free_params ? (
<FreeParamsForm
params={node.params ?? {}}
@@ -1524,7 +1558,14 @@ function PanelBody({
<span className="font-mono">{origin.repo || "a repository"}</span>
. Edit it there and sync again.
</p>
) : null}
) : mayEditCode ? null : (
<p
className="text-xs text-muted-foreground"
data-testid="node-source-locked"
>
{CODE_LOCKED}
</p>
)}
<div className="min-h-0 flex-1 overflow-hidden rounded-md border border-border">
<Suspense
fallback={
@@ -1534,7 +1575,7 @@ function PanelBody({
<NodeEditor
value={code ?? source?.code ?? ""}
onChange={editCode}
readOnly={Boolean(origin)}
readOnly={Boolean(origin) || !mayEditCode}
/>
</Suspense>
</div>