From c7dca8f806838f628aa25fab91b28a855594f63e Mon Sep 17 00:00:00 2001 From: Arda Erzin Date: Mon, 29 Jun 2026 03:02:12 +0200 Subject: [PATCH 1/4] feat(entity-ui): make agent template panel read more from the schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After #4913 landed the typed agent-template catalog type, the panel can infer more from the template shape instead of hardcoding it. This closes the schema-driven gaps that are implementable on today's schema without regression: - G1 — section header titles for the per-field sections (Instructions / Tools / MCP servers / Skills) now come from props..title, so a field rename in the template propagates without editing the section array. Composite sections (Model & harness, Advanced) and Triggers keep their FE labels; icons aren't in the schema. - G3 — harness capabilities resolve from the schema's declared x-ag-harness-ref on harness.kind (its target is the harnesses catalog) and are only applied when the ref is declared, instead of assuming the endpoint. revisionId drops from useModelHarness (capabilities key on the ref, not the revision). - G4 — the Claude permission-mode option set + field label/description come from the typed harness.permissions.default_mode sub-schema (enum + title), keeping the FE display labels, so a backend mode change propagates. G2 (discriminator-aware item classification) is documented as gated: the FE tool classifier is built on the legacy {function:{name}} shape absent from ToolConfig's discriminated union, so full adoption belongs with the schema-driven-config redesign (CHANGE-3). Tracked in the design doc checklist. Verified: entity-ui + entities tsc and eslint clean; OSS tsc at baseline 593; live in the playground (titles, harness/model summary, Claude permission options all render; no console errors). --- .../agent-config-section-drawers/design.md | 34 ++++++++++++++++ .../src/workflow/state/inspectMeta.ts | 5 ++- .../SchemaControls/AgentTemplateControl.tsx | 22 ++++++++--- .../ClaudePermissionsControl.tsx | 39 ++++++++++++++++--- .../agentTemplate/useModelHarness.tsx | 28 +++++++++---- 5 files changed, 108 insertions(+), 20 deletions(-) diff --git a/docs/design/agent-config-section-drawers/design.md b/docs/design/agent-config-section-drawers/design.md index d69098f2be..4be4cc1261 100644 --- a/docs/design/agent-config-section-drawers/design.md +++ b/docs/design/agent-config-section-drawers/design.md @@ -102,6 +102,40 @@ switch, mirroring the model warning. Tracked as the harness-gating follow-up. first-class via `ModelSpec` (`../agent-workflows/scratch/notes-model-auth.md`, R2), re-bind or clear the slug when the derived provider changes. Raised from PR #4923 review. +## Schema-driven section work package + +After #4913 landed the nested `agent-template` catalog type, the panel can read more from the +template shape instead of hardcoding it. The control is schema-*gated* (which sections/options +exist) but not yet schema-*driven* (identity, structure, controls, discrimination). This package +closes that to the extent #4913's schema supports; the gated tail waits on later schema steps. + +What #4913 makes available: every field has a `title`/`description`; `harness.kind` carries +`x-ag-harness-ref: "harness"` plus a `oneOf` of `{const, title}`; `ToolConfig` is a real +`discriminator: "type"` union (builtin/gateway/code/client/reference/platform) exposed in the JSON +schema; `harness.permissions` / `sandbox.permissions` are fully typed sub-schemas; `instructions` +→ `x-ag-type: "textarea"`, `llm.model` → `x-parameter: "grouped_choice"`. + +- [ ] **G1 — Section identity from schema.** Derive each per-field section's title + description + (tooltip) from `props..title` / `.description` instead of literals; order the list sections + by schema property order. Composite sections (Model & harness = `llm`+`harness`, Advanced = + `runner`+`sandbox`+`harness.extras`/`permissions`) keep their FE labels, and Triggers (non-schema) + stays FE. Icons stay FE (not in schema). +- [ ] **G2 — Discriminator-aware item classification.** Read the declared discriminator (`ToolConfig` + `discriminator: "type"`) from the schema where present, falling back to today's sniffing. MCP + (`transport` presence) and embed/skill refs (`@ag.embed`, `x-ag-type-ref: "skill-template"`) have + no discriminator declared, so they stay sniffed. *Gated tail:* fully adopting the six-kind tool + union (the FE's inline `type:"function"` shape is not in `ToolConfig`) belongs with the + schema-driven-config redesign (CHANGE-3). +- [ ] **G3 — Follow `x-ag-harness-ref`.** Resolve the harness capability catalog from the schema's + `harness.kind["x-ag-harness-ref"]` declaration rather than the hardcoded `/catalog/harnesses/` + assumption; behavior is identical today (one catalog) but the dependency is now declared, not + assumed. +- [ ] **G4 — Schema-sourced permission editor.** Source the harness-permissions editor's option set + (`default_mode` enum) and field labels/tooltips from the typed `_HarnessPermissionsSchema` instead + of hardcoded literals, keeping the rich control. *Gated tail:* per-harness show/hide still keys off + the harness value (`=== "claude"`) because neither the schema nor the harness catalog yet carries a + per-harness "is-gating" capability flag; make it schema-driven when that flag exists. + ## Verification `tsc` + `eslint` clean on `@agenta/ui` and `@agenta/entity-ui`; package unit tests for the new diff --git a/web/packages/agenta-entities/src/workflow/state/inspectMeta.ts b/web/packages/agenta-entities/src/workflow/state/inspectMeta.ts index a6bfd73172..07784d12c4 100644 --- a/web/packages/agenta-entities/src/workflow/state/inspectMeta.ts +++ b/web/packages/agenta-entities/src/workflow/state/inspectMeta.ts @@ -48,9 +48,10 @@ export const harnessCatalogQueryAtom = atomWithQuery(() /** * The per-harness capability map from the `harnesses` catalog. `null` until the catalog resolves. - * Keyed for signature compatibility with consumers; the data itself is not revision-scoped. + * Keyed by the harness ref (a template's `x-ag-harness-ref` value) that selects this catalog; the + * catalog data itself is global, so the key only documents which ref drove the lookup. */ -export const harnessCapabilitiesAtomFamily = atomFamily((_revisionId: string) => +export const harnessCapabilitiesAtomFamily = atomFamily((_harnessRef: string) => atom((get) => { const query = get(harnessCatalogQueryAtom) return query.data ?? null diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx index 9293b6e42b..7cf138f533 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx @@ -159,7 +159,7 @@ export function AgentTemplateControl({ // Model & harness + Advanced own a lot of coupled, stateful logic (the model/connection state // feeds both sections), so they live in their own hook that returns the summaries + bodies. - const mh = useModelHarness({schema, config, onChange, revisionId, disabled, withTooltip}) + const mh = useModelHarness({schema, config, onChange, disabled, withTooltip}) // Tool add/remove (inline function, builtin, gateway, workflow reference) lives in its own hook. const { @@ -204,6 +204,18 @@ export function AgentTemplateControl({ const hasMcp = Boolean(props.mcps) const hasSkills = Boolean(props.skills) + // Per-field section headers read their label from the template schema (`props..title`), + // so a field rename propagates without editing this file; the literal is a fallback. Composite + // sections (Model & harness, Advanced) and Triggers keep their FE labels, and icons aren't in + // the schema. + const fieldTitle = useCallback( + (field: string, fallback: string): string => { + const t = (props[field] as {title?: unknown} | undefined)?.title + return typeof t === "string" && t.trim() ? t : fallback + }, + [props], + ) + // Shared props for the tool picker, so the in-body popover and the header quick-add trigger // drive the same add flow. const toolSelectorProps = { @@ -242,7 +254,7 @@ export function AgentTemplateControl({ hasInstructions && { key: "instructions", icon: , - title: "Instructions", + title: fieldTitle("instructions", "Instructions"), summary: countSummary(1, "file"), // The + is inert until the backend stores multiple instruction files; the section is // already a list so it lights up with no rework when that lands. @@ -272,7 +284,7 @@ export function AgentTemplateControl({ hasTools && { key: "tools", icon: , - title: "Tools", + title: fieldTitle("tools", "Tools"), summary: countSummary(tools.length, "tool"), extra: !disabled ? ( , - title: "MCP servers", + title: fieldTitle("mcps", "MCP servers"), summary: countSummary(mcpServers.length, "server"), extra: !disabled ? headerAddButton("Add MCP server", handleAddMcpServer) : undefined, defaultOpen: mcpServers.length > 0, @@ -330,7 +342,7 @@ export function AgentTemplateControl({ hasSkills && { key: "skills", icon: , - title: "Skills", + title: fieldTitle("skills", "Skills"), summary: countSummary(skills.length, "skill"), extra: !disabled ? headerAddButton("Add skill", handleAddSkill) : undefined, defaultOpen: skills.length > 0, diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ClaudePermissionsControl.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ClaudePermissionsControl.tsx index b24dd70c2c..9fbedf983e 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ClaudePermissionsControl.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ClaudePermissionsControl.tsx @@ -36,8 +36,16 @@ export interface ClaudePermissionsControlProps { disabled?: boolean /** Additional CSS classes */ className?: string + /** + * The `default_mode` field schema (its `enum` + `title`/`description`). When provided, the mode + * option set and the field label/description follow the template schema instead of the + * hardcoded fallback, so a backend mode change propagates without editing this control. + */ + modeSchema?: {enum?: unknown; title?: unknown; description?: unknown} | null } +// FE display copy per mode value. The set of values is the schema's `default_mode` enum (passed via +// `modeSchema`); this only supplies the friendlier label, falling back to the raw value. const MODE_OPTIONS: {value: ClaudePermissionMode; label: string}[] = [ {value: "default", label: "Default (prompt on each gated tool)"}, {value: "acceptEdits", label: "Accept edits (auto-accept file edits)"}, @@ -74,9 +82,32 @@ export const ClaudePermissionsControl = memo(function ClaudePermissionsControl({ onChange, disabled = false, className, + modeSchema, }: ClaudePermissionsControlProps) { const current = useMemo(() => readValue(value), [value]) + // The mode option set comes from the schema's `default_mode` enum when available (FE labels by + // value, falling back to the raw value); otherwise the hardcoded MODE_OPTIONS. The field + // label/description likewise prefer the schema's title/description. + const modeOptions = useMemo(() => { + const en = modeSchema?.enum + if (Array.isArray(en) && en.length) { + const labelOf = (v: string) => MODE_OPTIONS.find((o) => o.value === v)?.label ?? v + return en + .filter((v): v is string => typeof v === "string") + .map((v) => ({value: v as ClaudePermissionMode, label: labelOf(v)})) + } + return MODE_OPTIONS + }, [modeSchema]) + const modeLabel = + typeof modeSchema?.title === "string" && modeSchema.title + ? modeSchema.title + : "Permission mode" + const modeDescription = + typeof modeSchema?.description === "string" && modeSchema.description + ? modeSchema.description + : "Claude Code's default permission mode for this headless run." + // Compose the full permissions object, overriding one slice. `default_mode` is only written // when set so an author who only edits rules never emits a null mode. const write = useCallback( @@ -100,15 +131,11 @@ export const ClaudePermissionsControl = memo(function ClaudePermissionsControl({ return (
- + value={current.defaultMode ?? undefined} onChange={(v) => write({defaultMode: v ?? null})} - options={MODE_OPTIONS} + options={modeOptions} disabled={disabled} placeholder="Claude default" allowClear diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx index ec1b2696ff..b5799244e2 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx @@ -39,14 +39,12 @@ export function useModelHarness({ schema, config, onChange, - revisionId, disabled, withTooltip, }: { schema?: SchemaProperty | null config: Record onChange: (next: Record) => void - revisionId: string | null disabled?: boolean withTooltip?: boolean }) { @@ -105,12 +103,19 @@ export function useModelHarness({ const modelId = useMemo(() => modelIdFromConfig(llm), [llm]) const connection = useMemo(() => connectionFromConfig(llm), [llm]) - // Per-harness capability map from the `/inspect` response meta, keyed by the open revision. - // Null when inspect hasn't resolved or the agent didn't publish it (older agents / standalone), - // in which case the connectionUtils helpers fall back permissively. - const capabilities = useAtomValue( - useMemo(() => harnessCapabilitiesAtomFamily(revisionId ?? ""), [revisionId]), + // Harness capability map, resolved from the schema's declared `x-ag-harness-ref` on the harness + // `kind` field (its target is the `harnesses` catalog). The ref is what opts this field into + // catalog-driven capabilities: we only apply the map when the schema declares it, otherwise the + // connectionUtils helpers fall back to a permissive, unfiltered picker. The catalog itself is + // global, so the ref string also keys the atom. + const harnessRef = (harnessProps.kind as Record | undefined)?.[ + "x-ag-harness-ref" + ] + const harnessRefKey = typeof harnessRef === "string" && harnessRef ? harnessRef : null + const capabilitiesFromCatalog = useAtomValue( + useMemo(() => harnessCapabilitiesAtomFamily(harnessRefKey ?? ""), [harnessRefKey]), ) + const capabilities = harnessRefKey ? capabilitiesFromCatalog : null // The project's stored connections (read-only) for the connection picker. The transformed vault // list surfaces custom-provider connections as {type, name, provider}; the resolver matches a @@ -760,6 +765,15 @@ export function useModelHarness({ value={claudePermissions} onChange={setClaudePermissions} disabled={disabled} + // Mode options + labels come from the harness `permissions` + // sub-schema (`default_mode` enum) so they follow the template. + modeSchema={ + ( + harnessProps.permissions?.properties as + | Record + | undefined + )?.default_mode + } />
) : null} From fd4c4e3fe8e09d52b6c03bc32d08a565cb162a28 Mon Sep 17 00:00:00 2001 From: Arda Erzin Date: Mon, 29 Jun 2026 04:23:34 +0200 Subject: [PATCH 2/4] fix(entity-ui): guard schema titles + read default_mode enum from anyOf Live verification (proper worktree this time) surfaced two issues in the schema-driven work the original commit claimed to have verified: - G1 regression: schema-gen emits the wrapper class name as a nested-model field's title, so the Instructions section header rendered the raw '_InstructionsSchema'. fieldTitle now rejects leading-underscore titles and falls back to the literal; the list fields (Tools/MCP servers/Skills) carry real titles and still pass through. Forward-compatible: a real backend title lights up automatically. - G4 was inert: default_mode is Optional[Literal], so its enum sits under anyOf, not on the node. ClaudePermissionsControl now reads the enum from anyOf too (it matched the hardcoded set, so options are unchanged). Confirmed against the dev server serving THIS worktree: catalog schema carries x-ag-harness-ref on harness.kind (G3 resolves), and the Instructions header now reads 'Instructions'. tsc + eslint clean. --- .../SchemaControls/AgentTemplateControl.tsx | 6 +++++- .../SchemaControls/ClaudePermissionsControl.tsx | 13 ++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx index 7cf138f533..cf89257d2c 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx @@ -208,10 +208,14 @@ export function AgentTemplateControl({ // so a field rename propagates without editing this file; the literal is a fallback. Composite // sections (Model & harness, Advanced) and Triggers keep their FE labels, and icons aren't in // the schema. + // + // Guard: schema-gen emits the wrapper class name as `title` for single nested-model fields + // (e.g. `instructions` -> "_InstructionsSchema"), so reject leading-underscore titles and fall + // back to the literal. List fields (tools/mcps/skills) carry real titles and pass through. const fieldTitle = useCallback( (field: string, fallback: string): string => { const t = (props[field] as {title?: unknown} | undefined)?.title - return typeof t === "string" && t.trim() ? t : fallback + return typeof t === "string" && t.trim() && !t.startsWith("_") ? t : fallback }, [props], ) diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ClaudePermissionsControl.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ClaudePermissionsControl.tsx index 9fbedf983e..611648aad6 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ClaudePermissionsControl.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/ClaudePermissionsControl.tsx @@ -39,9 +39,10 @@ export interface ClaudePermissionsControlProps { /** * The `default_mode` field schema (its `enum` + `title`/`description`). When provided, the mode * option set and the field label/description follow the template schema instead of the - * hardcoded fallback, so a backend mode change propagates without editing this control. + * hardcoded fallback, so a backend mode change propagates without editing this control. The enum + * may sit directly on the node or, for an `Optional[Literal]`, inside `anyOf`. */ - modeSchema?: {enum?: unknown; title?: unknown; description?: unknown} | null + modeSchema?: {enum?: unknown; anyOf?: unknown; title?: unknown; description?: unknown} | null } // FE display copy per mode value. The set of values is the schema's `default_mode` enum (passed via @@ -90,7 +91,13 @@ export const ClaudePermissionsControl = memo(function ClaudePermissionsControl({ // value, falling back to the raw value); otherwise the hardcoded MODE_OPTIONS. The field // label/description likewise prefer the schema's title/description. const modeOptions = useMemo(() => { - const en = modeSchema?.enum + // The enum is on the node for a bare Literal, or inside `anyOf` for an `Optional[Literal]` + // (`anyOf: [{enum:[...]}, {type:"null"}]`). + const direct = Array.isArray(modeSchema?.enum) ? (modeSchema!.enum as unknown[]) : null + const fromAnyOf = Array.isArray(modeSchema?.anyOf) + ? (modeSchema!.anyOf as {enum?: unknown}[]).find((a) => Array.isArray(a?.enum))?.enum + : null + const en = (direct ?? fromAnyOf) as unknown[] | null | undefined if (Array.isArray(en) && en.length) { const labelOf = (v: string) => MODE_OPTIONS.find((o) => o.value === v)?.label ?? v return en From a6b353e9f0cbd549ad5c6f3b8962a1cb9e25e04c Mon Sep 17 00:00:00 2001 From: Arda Erzin Date: Mon, 29 Jun 2026 12:52:09 +0200 Subject: [PATCH 3/4] fix(sdk): give agent-template nested schemas explicit titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catalog endpoint inlines $defs before serving, and an inlined nested model carries its own schema title — which Pydantic defaults to the class name. So the private execution sub-schemas surfaced their class names (e.g. the Instructions section header rendered "_InstructionsSchema" in the playground) once the field-level title was dropped during inlining. Set an explicit `title` on each nested sub-schema's model_config (Connection / Model / Instructions / Permissions / Harness / Interactions / Runner / Sandbox) so the inlined node carries the human label. Cosmetic only: no change to validation, the wire contract, or runtime parsing; no test asserts these titles. Unblocks the FE's schema-driven section titles (the panel's fieldTitle now reads a real label instead of falling back to the literal). ruff format + check clean. --- sdks/python/agenta/sdk/utils/types.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdks/python/agenta/sdk/utils/types.py b/sdks/python/agenta/sdk/utils/types.py index c80d0bb439..3c7be5414f 100644 --- a/sdks/python/agenta/sdk/utils/types.py +++ b/sdks/python/agenta/sdk/utils/types.py @@ -1118,7 +1118,7 @@ def _harness_field_schema_extra() -> Dict[str, Any]: class _ConnectionSchema(BaseModel): """The model credential connection (the existing ``ModelRef.connection``).""" - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="forbid", title="Connection") mode: Literal["agenta", "self_managed"] = Field( default="agenta", @@ -1139,7 +1139,7 @@ class _LlmSchema(BaseModel): carry the structured intent when authored; ``extras`` is the neutral knobs bag (was ``ModelRef.params``).""" - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="forbid", title="Model") model: str = Field( default=_DEFAULT_AGENT_MODEL, @@ -1169,7 +1169,7 @@ class _InstructionsSchema(BaseModel): file (becomes the harness ``AGENTS.md``); the object wraps it so later instruction kinds have a home.""" - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="forbid", title="Instructions") agents_md: str = Field( default=_DEFAULT_AGENTS_MD, @@ -1259,7 +1259,7 @@ class _HarnessPermissionsSchema(BaseModel): permission mode; ``allow`` / ``ask`` / ``deny`` are per-tool rule strings. A non-gating harness (Pi) leaves this empty.""" - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="forbid", title="Permissions") default_mode: Optional[ Literal["default", "acceptEdits", "plan", "bypassPermissions"] @@ -1293,7 +1293,7 @@ class _HarnessSchema(BaseModel): ``permissions`` is the gating posture for harnesses that gate tool use (Claude). ``extras`` is the per-harness escape hatch (Pi's ``system`` / ``append_system`` prompt overrides).""" - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="forbid", title="Harness") kind: str = Field( default=_DEFAULT_HARNESS, @@ -1327,7 +1327,7 @@ class _InteractionsSchema(BaseModel): The runner enforces it (``services/agent/src/responder.ts``). ``input`` and ``client_tool`` interaction kinds extend this section in a later step.""" - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="forbid", title="Interactions") headless: Literal["auto", "deny"] = Field( default=_DEFAULT_PERMISSION_POLICY, @@ -1347,7 +1347,7 @@ class _RunnerSchema(BaseModel): hatch. The rest of the runner surface (per-kind interaction handling, delivery channel, hooks, loop controls) is a later step.""" - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="forbid", title="Runner") kind: Literal["sidecar"] = Field( default="sidecar", @@ -1373,7 +1373,7 @@ class _SandboxSchema(BaseModel): ``kind`` is the sandbox provider; ``permissions`` is the security boundary folded in first-class (was ``sandbox_permission``). ``extras`` is the per-sandbox escape hatch.""" - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="forbid", title="Sandbox") kind: Literal["local", "daytona"] = Field( default=_DEFAULT_SANDBOX, From c8a941ea030c9336ae4113f2c8a7c8feda970b64 Mon Sep 17 00:00:00 2001 From: Arda Erzin Date: Mon, 29 Jun 2026 14:14:37 +0200 Subject: [PATCH 4/4] docs(agent-config): reference harness.permissions sub-schema by field path Readability tweak in the G4 checklist item: the field path `harness.permissions` reads cleaner than the private `_HarnessPermissionsSchema` class name. Same sub-schema, no content change. --- docs/design/agent-config-section-drawers/design.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design/agent-config-section-drawers/design.md b/docs/design/agent-config-section-drawers/design.md index 4be4cc1261..65f64a9e1c 100644 --- a/docs/design/agent-config-section-drawers/design.md +++ b/docs/design/agent-config-section-drawers/design.md @@ -131,8 +131,8 @@ schema; `harness.permissions` / `sandbox.permissions` are fully typed sub-schema assumption; behavior is identical today (one catalog) but the dependency is now declared, not assumed. - [ ] **G4 — Schema-sourced permission editor.** Source the harness-permissions editor's option set - (`default_mode` enum) and field labels/tooltips from the typed `_HarnessPermissionsSchema` instead - of hardcoded literals, keeping the rich control. *Gated tail:* per-harness show/hide still keys off + (`default_mode` enum) and field labels/tooltips from the typed `harness.permissions` sub-schema + instead of hardcoded literals, keeping the rich control. *Gated tail:* per-harness show/hide still keys off the harness value (`=== "claude"`) because neither the schema nor the harness catalog yet carries a per-harness "is-gating" capability flag; make it schema-driven when that flag exists.