mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[sandbox-ext] Phase 4: panel extension picker + allowlist docs
Project edit dialog (Sandbox section) exposes a per-service extension
picker — Switches from the allowlist grouped under each enabled service
(postgres: pgvector/PostGIS/pg_trgm/citext/uuid-ossp; redis: RediSearch/
RedisJSON/RedisBloom; mongo has none), mirroring the backend
SANDBOX_ENGINE_FEATURES allowlist. State holds a per-service Set; payload
builds sandbox_extensions only for enabled services with non-empty picks
(empty {} clears the column, mirroring sandbox_services' always-send —
exclude_unset + no exclude_none means an explicit {} writes NULL). The
picker renders only for opted-in services with activatable features.
Types: Project.sandbox_extensions (Record<string,string[]> | null),
ProjectUpdate.sandbox_extensions? (not on ProjectCreate, mirroring
sandbox_services). Mock create seeds null.
Docs name the allowlist (the security containment — no plpython3u), the
no-default-set rule (opters set explicitly, existing opters stay bare), the
standing-vs-per-call union, cache-by-features, kitchen-sink image selection,
and the recommendation to set the full set in project settings so agents
request subsets. sandbox-db.md gains an Extensions section; task-tools.md
and config-reference.md updated; CLAUDE.md sandbox paragraph extended.
Gate: panel typecheck + lint + prettier clean, 516 tests pass.
This commit is contained in:
@@ -40,6 +40,25 @@ const SANDBOX_SERVICES = [
|
||||
{ id: "mongo", label: "MongoDB" },
|
||||
] as const;
|
||||
|
||||
// Activatable extensions/modules per service, mirroring the backend allowlist
|
||||
// (roboco/models/sandbox.py SANDBOX_ENGINE_FEATURES). The allowlist is the
|
||||
// security containment — a plpython3u (superuser-RCE) is absent by design.
|
||||
// Mongo has no activatable features and is intentionally absent here.
|
||||
const SANDBOX_EXTENSIONS: Record<string, { id: string; label: string }[]> = {
|
||||
postgres: [
|
||||
{ id: "vector", label: "pgvector" },
|
||||
{ id: "postgis", label: "PostGIS" },
|
||||
{ id: "pg_trgm", label: "pg_trgm" },
|
||||
{ id: "citext", label: "citext" },
|
||||
{ id: "uuid-ossp", label: "uuid-ossp" },
|
||||
],
|
||||
redis: [
|
||||
{ id: "search", label: "RediSearch" },
|
||||
{ id: "json", label: "RedisJSON" },
|
||||
{ id: "bloom", label: "RedisBloom" },
|
||||
],
|
||||
};
|
||||
|
||||
interface EditProjectDialogProps {
|
||||
projectId: string;
|
||||
open: boolean;
|
||||
@@ -95,6 +114,28 @@ function EditProjectForm({
|
||||
const [sandboxSet, setSandboxSet] = useState<Set<string>>(
|
||||
new Set(sandboxServices),
|
||||
);
|
||||
// Per-service extension picks (only meaningful for services in sandboxSet).
|
||||
const [sandboxExtensions, setSandboxExtensions] = useState<
|
||||
Record<string, Set<string>>
|
||||
>(() => {
|
||||
const init: Record<string, Set<string>> = {};
|
||||
for (const [svc, feats] of Object.entries(
|
||||
project.sandbox_extensions || {},
|
||||
)) {
|
||||
init[svc] = new Set(feats);
|
||||
}
|
||||
return init;
|
||||
});
|
||||
const toggleExtension = (svc: string, feat: string, checked: boolean) => {
|
||||
setSandboxExtensions((prev) => {
|
||||
const next = { ...prev };
|
||||
const set = new Set(next[svc] ?? []);
|
||||
if (checked) set.add(feat);
|
||||
else set.delete(feat);
|
||||
next[svc] = set;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Token handling
|
||||
const [newToken, setNewToken] = useState("");
|
||||
@@ -135,6 +176,14 @@ function EditProjectForm({
|
||||
.filter(Boolean)
|
||||
: undefined,
|
||||
sandbox_services: [...sandboxSet],
|
||||
sandbox_extensions: (() => {
|
||||
const extObj: Record<string, string[]> = {};
|
||||
for (const svc of sandboxSet) {
|
||||
const feats = sandboxExtensions[svc];
|
||||
if (feats && feats.size > 0) extObj[svc] = [...feats].sort();
|
||||
}
|
||||
return extObj;
|
||||
})(),
|
||||
};
|
||||
|
||||
// Handle token update
|
||||
@@ -486,6 +535,38 @@ function EditProjectForm({
|
||||
project instead of the production credentials.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{SANDBOX_SERVICES.filter(
|
||||
(svc) => sandboxSet.has(svc.id) && SANDBOX_EXTENSIONS[svc.id],
|
||||
).map((svc) => (
|
||||
<div key={`ext_${svc.id}`} className="grid gap-2">
|
||||
<Label>{svc.label} Extensions</Label>
|
||||
{SANDBOX_EXTENSIONS[svc.id].map((ext) => (
|
||||
<div
|
||||
key={ext.id}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<Label
|
||||
htmlFor={`ext_${svc.id}_${ext.id}`}
|
||||
className="text-sm font-normal"
|
||||
>
|
||||
{ext.label}
|
||||
</Label>
|
||||
<Switch
|
||||
id={`ext_${svc.id}_${ext.id}`}
|
||||
checked={sandboxExtensions[svc.id]?.has(ext.id) ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
toggleExtension(svc.id, ext.id, checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Activated on-demand in the sandbox {svc.label} container. Set
|
||||
the full set here so agents can request subsets.
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -95,6 +95,7 @@ export const projectsApi = {
|
||||
dep_update_command: null,
|
||||
dep_update_paths: null,
|
||||
sandbox_services: null,
|
||||
sandbox_extensions: null,
|
||||
workspace_path: null,
|
||||
last_synced_at: null,
|
||||
head_commit: null,
|
||||
|
||||
@@ -1032,6 +1032,7 @@ export interface Project {
|
||||
dep_update_command: string | null;
|
||||
dep_update_paths: string[] | null;
|
||||
sandbox_services: string[] | null;
|
||||
sandbox_extensions: Record<string, string[]> | null;
|
||||
// Runtime state
|
||||
workspace_path: string | null;
|
||||
last_synced_at: string | null;
|
||||
@@ -1081,6 +1082,7 @@ export interface ProjectUpdate {
|
||||
dep_update_command?: string;
|
||||
dep_update_paths?: string[];
|
||||
sandbox_services?: string[];
|
||||
sandbox_extensions?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export interface ProjectSummary {
|
||||
|
||||
Reference in New Issue
Block a user