From fd5b777c4b5a6ead836c4a88fdf716b7a353b3fa Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 29 Jun 2026 00:21:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(agent):=20playground=20build=20kit=20?= =?UTF-8?q?=E2=80=94=20default=20agent=20config=20(#4917=20design)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01GYo3UEfvsZpncagqb28Mbc --- .../src/apis/fastapi/applications/models.py | 22 + .../src/apis/fastapi/applications/overlay.py | 47 ++ .../src/apis/fastapi/applications/router.py | 10 + .../applications/test_build_kit_overlay.py | 116 +++++ .../projects/default-agent-config/README.md | 26 ++ .../projects/default-agent-config/design.md | 432 ++++++++++++++++++ .../projects/default-agent-config/research.md | 159 +++++++ .../projects/default-agent-config/status.md | 71 +++ services/oss/src/agent/schemas.py | 20 +- .../unit/agent/test_default_agent_template.py | 42 +- .../agenta-entities/src/workflow/api/api.ts | 5 + .../agenta-entities/src/workflow/index.ts | 6 + .../src/workflow/state/commit.ts | 2 + .../src/workflow/state/index.ts | 3 + .../src/workflow/state/store.ts | 21 + .../SchemaControls/AgentTemplateControl.tsx | 303 +++++++++++- .../src/state/execution/agentRequest.ts | 149 +++++- .../src/state/execution/index.ts | 7 +- .../agenta-playground/src/state/index.ts | 7 +- .../tests/unit/agentRequest.test.ts | 142 +++++- 20 files changed, 1525 insertions(+), 65 deletions(-) create mode 100644 api/oss/src/apis/fastapi/applications/overlay.py create mode 100644 api/oss/tests/pytest/unit/applications/test_build_kit_overlay.py create mode 100644 docs/design/agent-workflows/projects/default-agent-config/README.md create mode 100644 docs/design/agent-workflows/projects/default-agent-config/design.md create mode 100644 docs/design/agent-workflows/projects/default-agent-config/research.md create mode 100644 docs/design/agent-workflows/projects/default-agent-config/status.md diff --git a/api/oss/src/apis/fastapi/applications/models.py b/api/oss/src/apis/fastapi/applications/models.py index e69015a663..4929318fd2 100644 --- a/api/oss/src/apis/fastapi/applications/models.py +++ b/api/oss/src/apis/fastapi/applications/models.py @@ -599,6 +599,24 @@ class SimpleApplicationQueryRequest(BaseModel): ) +class PlaygroundBuildKitContext(BaseModel): + """Read-only playground build-kit context for one inspect/fetch response.""" + + agent_template_overlay: Optional[dict] = Field( + default=None, + description="Partial `parameters.agent` overlay applied by the playground only.", + ) + + +class SimpleApplicationAdditionalContext(BaseModel): + """Platform-supplied read-only context for a simple-application response.""" + + playground_build_kit: Optional[PlaygroundBuildKitContext] = Field( + default=None, + description="Playground-only build kit data that is never persisted on the app.", + ) + + class SimpleApplicationResponse(BaseModel): """Simple-application single-row response envelope.""" @@ -613,6 +631,10 @@ class SimpleApplicationResponse(BaseModel): "revision's `data` merged. `data.url` is the invocation URL." ), ) + additional_context: Optional[SimpleApplicationAdditionalContext] = Field( + default=None, + description="Read-only platform context derived for this response.", + ) class SimpleApplicationsResponse(BaseModel): diff --git a/api/oss/src/apis/fastapi/applications/overlay.py b/api/oss/src/apis/fastapi/applications/overlay.py new file mode 100644 index 0000000000..1689b6905d --- /dev/null +++ b/api/oss/src/apis/fastapi/applications/overlay.py @@ -0,0 +1,47 @@ +"""Read-only overlays attached to application inspect/fetch responses.""" + +from typing import Any, Dict, List + +from agenta.sdk.agents.adapters.agenta_builtins import GETTING_STARTED_WITH_AGENTA_SLUG +from agenta.sdk.agents.platform.op_catalog import PLATFORM_OPS + +from oss.src.core.workflows.static_catalog import ( + STATIC_SLUG_PREFIX, + StaticWorkflowCatalog, + _STATIC_WORKFLOWS, +) + + +def _workflow_embed(slug: str) -> Dict[str, Any]: + return {"@ag.embed": {"@ag.references": {"workflow": {"slug": slug}}}} + + +def _reserved_static_tool_slugs() -> List[str]: + catalog = StaticWorkflowCatalog() + slugs: List[str] = [] + for slug in _STATIC_WORKFLOWS: + if not slug.startswith(STATIC_SLUG_PREFIX): + continue + revision = catalog.retrieve_revision(slug=slug) + if revision and revision.flags and revision.flags.is_skill: + continue + slugs.append(slug) + return slugs + + +def build_agent_template_overlay() -> Dict[str, Any]: + """Build the playground-only agent-template overlay from platform-owned sources.""" + + return { + "tools": [ + *[{"type": "platform", "op": op_name} for op_name in PLATFORM_OPS], + *[_workflow_embed(slug) for slug in _reserved_static_tool_slugs()], + ], + "skills": [_workflow_embed(GETTING_STARTED_WITH_AGENTA_SLUG)], + "sandbox": { + "permissions": { + "write_files": "allow", + "execute_code": "allow", + } + }, + } diff --git a/api/oss/src/apis/fastapi/applications/router.py b/api/oss/src/apis/fastapi/applications/router.py index d0a812dc4c..3a9ca07097 100644 --- a/api/oss/src/apis/fastapi/applications/router.py +++ b/api/oss/src/apis/fastapi/applications/router.py @@ -66,9 +66,12 @@ SimpleApplicationCreateRequest, SimpleApplicationEditRequest, SimpleApplicationQueryRequest, + SimpleApplicationAdditionalContext, SimpleApplicationResponse, SimpleApplicationsResponse, + PlaygroundBuildKitContext, ) +from oss.src.apis.fastapi.applications.overlay import build_agent_template_overlay from oss.src.apis.fastapi.applications.utils import ( parse_application_variant_query_request_from_params, parse_application_variant_query_request_from_body, @@ -1905,6 +1908,13 @@ async def fetch_simple_application( simple_application_response = SimpleApplicationResponse( count=1 if simple_application else 0, application=simple_application, + additional_context=SimpleApplicationAdditionalContext( + playground_build_kit=PlaygroundBuildKitContext( + agent_template_overlay=build_agent_template_overlay(), + ), + ) + if simple_application + else None, ) return simple_application_response diff --git a/api/oss/tests/pytest/unit/applications/test_build_kit_overlay.py b/api/oss/tests/pytest/unit/applications/test_build_kit_overlay.py new file mode 100644 index 0000000000..192dfea852 --- /dev/null +++ b/api/oss/tests/pytest/unit/applications/test_build_kit_overlay.py @@ -0,0 +1,116 @@ +from types import SimpleNamespace +from unittest.mock import AsyncMock +from uuid import uuid4 + +import pytest + +from agenta.sdk.agents.adapters.agenta_builtins import GETTING_STARTED_WITH_AGENTA_SLUG +from agenta.sdk.agents.platform.op_catalog import PLATFORM_OPS + +from oss.src.apis.fastapi.applications import router as applications_router_module +from oss.src.apis.fastapi.applications.overlay import build_agent_template_overlay +from oss.src.apis.fastapi.applications.router import SimpleApplicationsRouter +from oss.src.core.applications.dtos import SimpleApplication +from oss.src.core.workflows.static_catalog import ( + STATIC_SLUG_PREFIX, + StaticWorkflowCatalog, + _STATIC_WORKFLOWS, +) + + +def _embed_slug(entry: dict) -> str | None: + refs = entry.get("@ag.embed", {}).get("@ag.references", {}) + workflow = refs.get("workflow") or refs.get("workflow_revision") or {} + return workflow.get("slug") + + +def test_agent_template_overlay_contains_platform_ops_authoring_skill_and_permissions(): + overlay = build_agent_template_overlay() + + platform_tools = [ + tool + for tool in overlay["tools"] + if isinstance(tool, dict) and tool.get("type") == "platform" + ] + assert platform_tools == [ + {"type": "platform", "op": op_name} for op_name in PLATFORM_OPS + ] + + assert overlay["skills"] == [ + { + "@ag.embed": { + "@ag.references": { + "workflow": {"slug": GETTING_STARTED_WITH_AGENTA_SLUG} + } + } + } + ] + assert overlay["sandbox"] == { + "permissions": {"write_files": "allow", "execute_code": "allow"} + } + + +def test_agent_template_overlay_includes_reserved_static_workflow_tool_embeds(): + overlay = build_agent_template_overlay() + tool_embed_slugs = { + _embed_slug(tool) + for tool in overlay["tools"] + if isinstance(tool, dict) and "@ag.embed" in tool + } + catalog = StaticWorkflowCatalog() + + expected_slugs = set() + for slug in _STATIC_WORKFLOWS: + revision = catalog.retrieve_revision(slug=slug) + if ( + slug.startswith(STATIC_SLUG_PREFIX) + and revision + and revision.flags + and not revision.flags.is_skill + ): + expected_slugs.add(slug) + + assert tool_embed_slugs == expected_slugs + + +@pytest.mark.asyncio +async def test_fetch_simple_application_includes_build_kit_context(monkeypatch): + project_id = uuid4() + user_id = uuid4() + application_id = uuid4() + + class DummySimpleApplicationsService: + applications_service = object() + + async def fetch(self, **kwargs): + assert kwargs["project_id"] == project_id + assert kwargs["application_id"] == application_id + return SimpleApplication(id=application_id, slug="agent") + + monkeypatch.setattr( + applications_router_module, + "check_action_access", + AsyncMock(return_value=True), + raising=False, + ) + + router = SimpleApplicationsRouter( + simple_applications_service=DummySimpleApplicationsService() + ) + request = SimpleNamespace( + state=SimpleNamespace(project_id=str(project_id), user_id=str(user_id)) + ) + + response = await router.fetch_simple_application( + request, + application_id=application_id, + ) + + overlay = ( + response.additional_context.playground_build_kit.agent_template_overlay + if response.additional_context + and response.additional_context.playground_build_kit + else None + ) + assert response.application is not None + assert overlay == build_agent_template_overlay() diff --git a/docs/design/agent-workflows/projects/default-agent-config/README.md b/docs/design/agent-workflows/projects/default-agent-config/README.md new file mode 100644 index 0000000000..274f8da0ef --- /dev/null +++ b/docs/design/agent-workflows/projects/default-agent-config/README.md @@ -0,0 +1,26 @@ +# Default agent config (playground build kit) + +The platform tools, the Agenta authoring skill, and the build permissions are a playground build +kit, modeled as an **agent-template overlay**: a partial agent template the platform serves +read-only on the inspect response. The frontend merges the overlay onto `parameters.agent` on a +playground run so the assistant can build and improve the agent, and leaves it out on commit. The +agent service stays dumb: it runs the template it receives. The published agent ships with only +the user's own config. + +## Files + +- `design.md`: the design. The overlay model (the frontend merges, the backend serves, the + service runs as-is), the overlay shape (a partial `parameters.agent`, skills as `@ag.embed`), + where it lives on the inspect response (`additional_context.playground_build_kit.agent_template_overlay`), the + merge semantics, the frontend run-and-commit behavior, the advanced-drawer UI, what the backend + does not do, the interface notes, and the change set. +- `research.md`: the code trace behind the design. Where the default comes from, how platform + tools and skills are shaped, the run path, and the commit path. +- `status.md`: where the design stands, the contract for the frontend, open questions, and + coordination. + +## In one line + +The frontend reads a read-only `agent_template_overlay` from the inspect response, merges its +tools, skill, and sandbox permissions onto `parameters.agent` on a kit-on run, and leaves them out +on commit. The backend only serves the overlay. The service never knows it exists. diff --git a/docs/design/agent-workflows/projects/default-agent-config/design.md b/docs/design/agent-workflows/projects/default-agent-config/design.md new file mode 100644 index 0000000000..e5a8292a82 --- /dev/null +++ b/docs/design/agent-workflows/projects/default-agent-config/design.md @@ -0,0 +1,432 @@ +# Design: the playground build kit (an agent-template overlay) + +Status: draft for Mahmoud's review. Grounded in code on `gitbutler/edit` over `big-agents`, +2026-06-28. Paths are absolute. See `research.md` for the code trace. + +## Problem and goal + +A new agent arrives near-bare. While the user builds it in the playground, the assistant needs +authoring scaffolding: the platform tools (find capabilities, query workflows, commit a +revision), the Agenta authoring skill, and elevated sandbox permissions (write files, execute +code). That scaffolding is a build aid, not the user's agent. The shipped agent must carry only +what the user authored. + +So the build kit is shown and used in the playground, never committed, and never reaches a +deployed run. This design decides who shows it, who applies it, and who keeps it out of the +commit. + +## The model: a build-kit overlay + +The build kit is an **agent-template overlay**: a partial agent template, the same shape as +`parameters.agent`. The platform owns it. The backend serves it read-only on the inspect +response. The frontend applies it to a playground run and leaves it out of a commit. + +Three actors, three jobs: + +- **The agent service runs the template it receives.** It reads `parameters.agent` and runs it. + It does not know the build kit exists. There is no run-prep merge and no run flag. The + platform tools, the authoring skill, and the build permissions reach a run only because they + are already inside the `parameters.agent` the service was handed, and the service resolves + them the way it resolves any tool, skill, or sandbox setting. +- **The backend serves the overlay.** It assembles the overlay once and attaches it to the + inspect response. It never applies the overlay and never acts on it. +- **The frontend applies the overlay.** It reads the overlay from the inspect response and + renders it read-only in the drawer. On a kit-on run, it merges the overlay onto the current + `parameters.agent` and sends the result. On commit, it does not merge, so the committed + config holds only the user's own template. + +"Shown but not committed" follows from where the overlay lives and when the frontend applies it. +The overlay never enters `parameters.agent`, the tree the playground edits and commits. It lives +in the read-only inspect response and, on a kit-on run, in a throwaway merged copy that feeds the +run payload. A deployed agent runs its stored config, which has no overlay, so authoring power +cannot reach a published run. + +## The overlay shape + +The overlay is a partial `parameters.agent`. It can carry any agent-template field: tools, +embedded skills, sandbox or permission overrides, a model choice, instructions, or anything the +template defines later. One mechanism covers every kind. There are no per-kind groups and no +display fields to invent, because the overlay reuses the agent-template shape the platform +already defines and the playground already renders. + +Skills in the overlay are ordinary `@ag.embed` references, full stop. An embed reference (the +slug, plus a version when pinned) is the platform's existing mechanism for embedding a variant, +and it already identifies the skill, which carries its own name and description. The overlay adds +no parallel `key`, `name`, or `description` for a skill. The authoring skill is the reserved +static skill `__ag__getting_started_with_agenta` +(`/home/mahmoud/code/agenta/sdks/python/agenta/sdk/agents/adapters/agenta_builtins.py`), served +read-only under the reserved `__ag__*` namespace. + +Today's overlay: + +```jsonc +{ + "agent_template_overlay": { + "tools": [ + { "type": "platform", "op": "find_capabilities" }, + { "type": "platform", "op": "query_workflows" }, + { "type": "platform", "op": "commit_revision" }, + // a client tool the embed resolver inlines: an @ag.embed of a reserved-slug workflow, the same shape as a skill embed (#4920) + { "@ag.embed": { "@ag.references": { "workflow": { "slug": "__ag__request_connection" } } } } + ], + "skills": [ + { "@ag.embed": { "@ag.references": { "workflow": { "slug": "__ag__getting_started_with_agenta" } } } } + ], + "sandbox": { + "permissions": { "write_files": "allow", "execute_code": "allow" } + } + } +} +``` + +The backend assembles `tools` from two parallel iterations, both over sources the platform +already owns. It iterates `PLATFORM_OPS` +(`/home/mahmoud/code/agenta/sdks/python/agenta/sdk/agents/platform/op_catalog.py`) for the +platform ops, so a new op added by the builder-tools project joins the kit with no edit here. It +also enumerates the reserved-slug platform workflows from the static workflow catalog +(`StaticWorkflowCatalog` over `_STATIC_WORKFLOWS` at +`/home/mahmoud/code/agenta/api/oss/src/core/workflows/static_catalog.py`) and adds each as an +`@ag.embed` reference, the same embed shape a skill uses, only the slug differs. These reserved +workflows are the same `__ag__*` family as the platform skills, so a client tool like +`__ag__request_connection` (a non-runnable tool the embed resolver inlines into a `client` tool the +frontend handles, owned by `#4920`) rides the kit beside the platform ops with no bespoke path. The +two iterations are symmetric: one walks the op catalog, the other walks the static +workflow catalog. `skills` is the `@ag.embed` reference to the authoring skill's reserved slug; +`sandbox` is the build-permission elevation. Each entry is an ordinary agent-template fragment, so +the dumb service resolves it the way it resolves any tool, skill, or sandbox setting, and a kit-on +run is indistinguishable on the wire from a config the user authored by hand with the same items. + +## Where the overlay lives in the inspect response + +The overlay is platform-owned, read-only, and derived per response. It is not user config and +not user metadata, so it does not belong in either of those contracts. It rides a dedicated +read-only container on the inspect response envelope. + +```jsonc +{ + "count": 1, + "application": { /* ... the agent, including data.parameters (the user's config) ... */ }, + "additional_context": { + "playground_build_kit": { + "agent_template_overlay": { /* the partial parameters.agent above */ } + } + } +} +``` + +- `additional_context` is a new optional field on `SimpleApplicationResponse` + (`/home/mahmoud/code/agenta/api/oss/src/apis/fastapi/applications/models.py`), a sibling of + `application`. It holds platform-supplied, read-only information derived for this response. The + build kit is its first member; future read-only hints or pointers add new members beside + `playground_build_kit` rather than overloading user-owned fields. +- `playground_build_kit` holds the kit's payload. Today that is one field, `agent_template_overlay`. +- `agent_template_overlay` is the partial agent template the frontend merges. The name says + exactly what it is and how it is used, and it cannot be mistaken for the saved template. + +This placement is the load-bearing decision, and it follows ownership and lifecycle: + +- User config that the revision persists and the deployment runs lives in + `application.data.parameters`. +- User metadata that round-trips through create, edit, and commit lives in `application.meta`. +- Platform information that the backend derives read-only for one inspect response lives in + `additional_context`. + +The overlay must not sit in `revision.data`. That object is the user's schema-constrained config, +it is `extra="forbid"` +(`/home/mahmoud/code/agenta/sdks/python/agenta/sdk/models/workflows.py`), and it flows into the +commit and edit paths. The overlay must not sit in `application.meta` either, because that field +is user-owned and persisted. Both would blur the ownership line the rest of this design depends +on. + +## Applying the overlay + +A kit-on run produces a derived run config by merging the overlay onto a copy of the current +`parameters.agent`. The merge is an override overlay: the overlay adds entries the config lacks +and overrides entries it already holds. When the overlay overrides a user setting, the drawer +flags it on that user's own control (see the override hint in the drawer UI), so the user can see +what the kit changed and how to match the published agent. + +- **Object fields** (`sandbox`, `runner`, `harness`, `llm`, `instructions`) deep-merge, and the + overlay wins on a conflict. So `sandbox.permissions.write_files: "allow"` elevates the run's + sandbox without discarding the user's other sandbox settings. +- **List fields** (`tools`, `skills`, `mcps`) merge by item identity. A tool's identity is its + `op` for a platform tool, the referenced workflow slug for an `@ag.embed` tool, + otherwise its `name`; a skill's identity is the slug its `@ag.embed` references; an MCP server's + identity is its `name`. On an identity match the overlay entry + overrides; otherwise it appends. So the kit's three platform ops and one authoring skill add to + whatever the user authored, and re-adding an op the user already has is a no-op rather than a + duplicate. + +The merge runs on a throwaway copy in the run-build path only. It never mutates the draft or the +committed tree. A separate, explicit applier owns this, because the frontend's general config +merge is shallow and array-replacing; a naive spread would clobber the user's `tools` or `skills` +arrays. The applier lives beside the run builder (next section), not in the shared config-merge +path. + +## Frontend behavior + +The frontend owns three behaviors. All read the same overlay; none change the run wire. + +1. **Read.** On loading the workflow, the frontend reads + `additional_context.playground_build_kit.agent_template_overlay` from the inspect response and keeps it in + session-scoped state, keyed like the existing inspect cache (per service), separate from the + persisted `data`. It renders the overlay in the drawer. It hardcodes no item list and no + labels. + +2. **Apply on a kit-on run.** The drawer holds one piece of session state, `buildKitEnabled`, + default on. When the frontend builds a run request, it applies the overlay if the toggle is + on. The build point is `buildAgentRequest` + (`/home/mahmoud/code/agenta/web/packages/agenta-playground/src/state/execution/agentRequest.ts`), + which composes the `/invoke` body's `data.parameters` from the draft-aware config. With the + kit on, the applier merges the overlay into the run copy's `parameters.agent` (object fields + deep-merged, list fields identity-merged) and sends that. With the kit off, it sends the + user's config unchanged, so the user previews the agent as users will see it. The builder + already transforms `parameters` on a throwaway copy before sending (it drops half-filled + entries and normalizes tool shapes), so the overlay merge is the same kind of pre-send step. + +3. **Exclude on commit.** The commit path reads the user's config from + `entity.data.parameters` through `prepareCommitParameters` + (`/home/mahmoud/code/agenta/web/packages/agenta-entities/src/workflow/state/commit.ts`). The + overlay was never written into `entity.data.parameters`, so the commit excludes it with no + strip step. The inspect response and the kit-on run payload are the only places the overlay + ever appears. + +The run wire stays byte-compatible. The backend cannot tell a kit-on run from a hand-authored +config with the same items, and it does not need to. + +## The drawer UI + +This design covers both the inspect contract and the advanced-drawer UI, in one document. The UI +recomposes existing drawer parts. The designer handoff +(`/home/mahmoud/code/agenta/design_handoff_advanced_build_kit/README.md`) is the high-fidelity +spec; the `.dc.html` is a visual reference, not code, and `support.js` is mock runtime to ignore. + +### The drawer today + +Verified on this branch, so the changes land on real structure. + +- The advanced drawer is `AgentTemplateControl.tsx` + (`/home/mahmoud/code/agenta/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx`), + rendered inside the `SectionDrawer` shell (icon, title, scroll body, Cancel and Save). +- The advanced body stacks three groups, separated by top borders, not yet collapsible: + Authentication (`:1517`), Execution environment (`:1530`), and Permissions (`:1565`). These + configure the committed agent. +- The collapsed-header summary the drawer computes (`advancedSummary`, `:1501`) reads values such + as `Agenta-managed` and `Sandbox: Local` from existing state. +- `SkillTemplateControl.tsx:205` already renders an Agenta-owned skill read-only ("Provided by + Agenta. This skill cannot be edited or removed"), with no editor and no delete. This is the + read-only pattern the kit reuses. + +### Change 1: make the advanced sections collapsible + +Each advanced group becomes a collapsible accordion item, the behavior the playground's left +panel already uses, so the groups stop rendering as one long scroll. + +- Default state is collapsed. Several sections can be open at once; it is not single-open. +- The header row shows the section icon, the title, a one-line summary on the right, then a + chevron that rotates when open. +- The body mounts only when expanded. Its content is the section's existing controls, unchanged. +- Summaries reuse state already present: Authentication shows `Agenta-managed`, Execution + environment shows `Sandbox: Local`, Permissions shows `Auto`. + +This change carries no contract and no commit logic. It is drawer polish on the existing groups, +so it ships as a separate, independent change, not part of the build-kit core. + +### Change 2: add the "Playground build kit" section + +A new collapsible section at the top of the drawer, above Authentication, with a subtly warmer +background (`#fcfcfa`) so it reads as a distinct playground-only layer. + +- **Header**: a wrench icon, the title `Playground build kit`, a restrained `Removed on commit` + tag (a small amber dot plus text, no pill, no banner), an enable or disable toggle, and the + chevron. The toggle's click stops propagation, so flipping the kit does not also expand the + section. +- **Body, when expanded**: an intro paragraph that ends with "None of this is part of the + published agent." When the kit is off, an info note that the assistant can no longer create + files, run code, or edit the agent here. Then the overlay, rendered read-only. + +The drawer renders the overlay by reusing the playground's existing config-item controls, the +same way it renders the committed config. An `@ag.embed` skill renders as an embedded skill +(read-only, the pattern from `SkillTemplateControl.tsx:205`); a platform tool renders as a +platform tool; the `sandbox.permissions` fragment renders in a permissions group. The drawer +derives the handoff's three labelled groups from the overlay's own `skills`, `tools`, and +`sandbox.permissions`. A permission's green `On` pill reflects its value in the overlay (`allow`), +so the overlay needs no separate status field. Every row is dimmed and locked. The drawer +hardcodes no item list; it renders whatever the overlay carries. The designer's sample rows are +illustrative. + +### The toggle wiring + +The toggle does not set a run flag. It is `buildKitEnabled`, ephemeral session state that resets +to on each session, default on. The frontend reads it when it builds the run request and applies +the overlay when it is on (frontend behavior, step 2). The drawer writes nothing into the config +and never echoes the overlay. The toggle is a playground preference, not a field on the revision +and not a stored preference in v1. + +### Keep two permission ideas distinct + +The drawer holds two things that both say "permissions," and they must not read as one setting. + +- The committed Permissions group and the Execution environment group configure the agent's own + permissions and sandbox. The user edits them, they commit, they ship. +- The build kit's permissions are a read-only reflection of the sandbox elevation the overlay + carries. They are dimmed, locked, tagged removed on commit, and never committed. + +The structure does the heavy lifting: the kit's permissions live inside the kit section, behind +the same dimming, lock, and removed-on-commit tag as the rest of the kit, while the agent's own +Permissions group stays an editable, committed control elsewhere. Review this with the designer, +because the two ideas sit close together and a user who conflates them could believe the agent +ships with write-files and execute-code permission. + +### Show an override hint on an overridden user control + +When the kit is on and the overlay overrides one of the user's own settings, the drawer control +for that user setting shows a hint or tooltip: the value is overridden in the playground by the +build kit, and to make the playground match the published agent the user toggles the build kit off. +The load-bearing case is a permission. If the user disables execute-code in their own config but +the kit re-enables it for the playground run, the Permissions control tells the user why the +playground behaves differently from the published agent and how to match it (toggle the kit off). +The hint appears only on a control the overlay actually overrides, and only while the kit is on. + +## Published default stays bare + +A new agent's stored config is bare: no platform tools, no authoring skill, no elevated sandbox. +Those three belong to the overlay now, not to the published default. + +Today `schemas.py` enriches the published default with the authoring skill and the sandbox +boundary (`build_agent_v0_default(skill_slug=..., include_sandbox_permission=True)` at +`/home/mahmoud/code/agenta/services/oss/src/agent/schemas.py`). Revert that enrichment so the +inspect schema default and the catalog template both carry the bare default, the same value the +SDK builtin carries. The skill and the sandbox elevation reach a run only through the overlay, by +the frontend's merge. The overlay delivers the authoring skill on its own, through its own +`@ag.embed` reference, so the published default carries no skill of its own. Reverting the +`schemas.py` enrichment is the only backend change this needs. + +## What the backend explicitly does NOT do + +State the negatives so no implementer re-adds them. + +- **No service-side merge.** The agent service does not merge the overlay into the config at + run-prep. There is no overlay merge in the agent app + (`/home/mahmoud/code/agenta/services/oss/src/agent/app.py`). The service runs `parameters.agent` + as received. +- **No run flag.** There is no inject flag on the run request and none on + `WorkflowInvokeRequestFlags`. The toggle is client session state. The run wire gains no field. +- **The service does not read the overlay.** The overlay is assembled by the inspect layer and + consumed by the frontend. The service's run path never references it. +- **No backend strip on commit.** The backend does not remove kit items from a committed config, + because the kit never enters the committed config. + +## Interface notes (design-interfaces) + +The model rests on role splits, not feature splits. + +- **Platform-derived information versus user-owned data.** `additional_context` is platform-supplied, + read-only, and derived per response. `application.data.parameters` is user config the revision + persists; `application.meta` is user metadata that round-trips. Three owners, three lifecycles, + three homes. This split is why the overlay needs no strip step and cannot leak into a commit. +- **One overlay, not three typed groups.** The kit is a partial agent template, so it reuses the + agent-template shape the platform already defines and the playground already renders. The kind + of each item (tool, skill, permission) is implicit in where it sits in the template, not in a + bespoke descriptor field. +- **A skill is its embed reference.** An `@ag.embed` reference already identifies a skill and + carries its name and description. The overlay adds no parallel display fields for it. +- **A permission's status is its value.** A sandbox permission set to `allow` in the overlay is + the status the drawer renders. No separate status field. +- **Session preference versus persisted config.** The toggle is a playground preference in client + session state, not a field on the revision and not a flag on the run. + +## Change set, by layer + +1. **Additional-context container (backend, new).** Add `additional_context` to + `SimpleApplicationResponse` with a typed `playground_build_kit.agent_template_overlay` + (`/home/mahmoud/code/agenta/api/oss/src/apis/fastapi/applications/models.py`). Populate it only + in the read path (`fetch_simple_application`), never in create, edit, or commit. + +2. **Overlay builder (backend, new).** One builder assembles the overlay: `tools` from both + `PLATFORM_OPS` and the reserved-slug platform workflows in the static workflow catalog, `skills` + as the `@ag.embed` reference to the authoring slug, `sandbox` as the build-permission + elevation. It reads platform-owned sources and is not referenced by the service run + path. + +3. **Published default goes bare (backend).** Revert the `schemas.py` enrichment so the schema + default is bare. Move the authoring skill and the sandbox elevation into the overlay. + +4. **Frontend read and render (web).** Read `additional_context.playground_build_kit.agent_template_overlay` + into session state. Render the "Playground build kit" section, reusing the existing config-item + controls. + +5. **Frontend overlay applier (web).** Add an explicit applier (deep-merge objects, identity-merge + lists) and call it in `buildAgentRequest` when the toggle is on, on the throwaway run copy + only. + +6. **Frontend exclude on commit (web).** No change beyond confirming the overlay is never written + into `entity.data.parameters`, so `prepareCommitParameters` excludes it for free. + +7. **Tests.** Backend: the builder lists the platform ops, the authoring skill, and the build + permissions as one overlay; the inspect response carries `additional_context.playground_build_kit`; the + published default is bare across the builtin, the inspect schema, and the catalog (update + `/home/mahmoud/code/agenta/services/oss/tests/pytest/unit/agent/test_default_agent_template.py`). + Frontend: a kit-on run merges the overlay into `parameters.agent` (deep-merge and identity-merge + verified); a kit-off run sends the bare config; a commit excludes the kit either way; the + applier never mutates the draft or committed tree. + +## Risks and edge cases + +- **Safety.** The kit grants self-commit, write files, and execute code. It cannot reach a + production run: nothing persists it and the run path never adds it. A deployed agent runs its + stored config, which has no overlay. Assert it (a published agent's resolved config never + contains the platform ops or the elevated sandbox). +- **No new attack surface.** The run API already accepts and resolves whatever tools and + permissions a config carries; a hand-authored config with the platform ops already runs them + today. The overlay grants the frontend no new capability; it only saves the user from authoring + those entries by hand. +- **Merge precedence.** The applier must deep-merge objects and identity-merge lists, not spread. + A shallow spread would replace the user's `tools` or `skills` arrays. This is the one place a + naive implementation breaks the feature. +- **Kit off.** A kit-off run uses the user's config only, so the user tests the agent as users + will see it. +- **Existing agents.** They gain the kit in the playground with no migration, because the + frontend merges it from the overlay at run time. Nothing is baked, so nothing goes stale. + +## Out of scope and coordination + +- The authoring skill's content and naming are owned by the skills project (`#4918`). This design + references the skill by its reserved slug. +- The builder-tools project (`#4919`) adds more platform ops. The builder reads `PLATFORM_OPS` at + assembly time, so new ops join the overlay automatically. +- A client tool such as `request_connection` is not a platform op. The kit carries it as an + `@ag.embed` reference to its reserved `__ag__*` slug, the same embed shape a skill uses, only the + slug differs; the embed resolver inlines it into a `client` tool the frontend handles (identity + by its referenced workflow slug), beside the platform ops. Its primary definition is owned by + `#4920`. +- Per-item edit or delete of kit items, and a picker to add platform tools to the published agent, + are out of scope. The kit is whole-toggle and read-only in v1. +- The overlay model carries through to `#4918`, `#4919`, and `#4920`. They are not edited here; the + orchestrator propagates it after Mahmoud approves this design. + +## Decisions + +- **Toggle persistence.** Ephemeral per playground session, resetting to on. Not a stored + preference in v1. +- **Merge precedence on a conflict.** The overlay wins on an identity match: it is an override + overlay. When it overrides a user setting, the drawer flags it on that user's own control with an + override hint (toggle the kit off to match the published agent). +- **Change 1 (collapsible sections).** Ships as a separate, independent change, not part of the + build-kit core. + +## Open questions for Mahmoud + +1. **Confirm the published default goes bare**, dropping the authoring skill and the sandbox + boundary from the schema default into the overlay. This touches the skills project's surface, so + it needs their nod. + +## Appendix: prior approaches + +Two earlier models were considered and dropped. The first had the agent service merge the kit at +run time behind a run flag; it put business logic in the service and a kit-injecting path on every +run. The second kept the merge on the frontend but modeled the kit as a bespoke descriptor with +three typed groups, each row carrying `key`, `name`, `description`, and a per-row `config`; it +reinvented display fields the agent-template shape and the embed reference already provide. The +current overlay model supersedes both: the service stays dumb, and the kit is one partial agent +template the frontend merges. diff --git a/docs/design/agent-workflows/projects/default-agent-config/research.md b/docs/design/agent-workflows/projects/default-agent-config/research.md new file mode 100644 index 0000000000..dbcf474978 --- /dev/null +++ b/docs/design/agent-workflows/projects/default-agent-config/research.md @@ -0,0 +1,159 @@ +# Research: default agent config (tools + skills loaded by default, opt-out) + +Grounded in code on branch `gitbutler/edit` (over `big-agents`), 2026-06-28. File paths are +absolute. + +## The goal + +When a user creates a new agent, the config should arrive pre-loaded with the platform +tools and the default Agenta skills. Workflow (reference) tools stay as they are. The user +can disable each default item, and ideally delete it. This is opt-out, not opt-in. UX and +debug-mode polish are out of scope for this design. + +## 1. Where a new agent's default config comes from + +There is one source of truth: `build_agent_v0_default(...)` in the SDK at +`/home/mahmoud/code/agenta/sdks/python/agenta/sdk/utils/types.py:1399`. Everything else +re-derives the seed from it. The default is concrete config baked into the stored revision +at create or commit time. It is not resolved by reference at run time. + +What the builder returns today: + +```python +{ + "instructions": {"agents_md": _DEFAULT_AGENTS_MD}, # friendly hello-world + "llm": {"model": "gpt-5.5"}, + "tools": [], + "mcps": [], + "harness": {"kind": "pi_core"}, + "runner": {"kind": "sidecar", "interactions": {"headless": "auto"}}, + "sandbox": {"kind": "local"}, +} +``` + +The builder takes an optional `skill_slug=` that adds one `@ag.embed` skill reference. No +caller passes it today, so a new agent gets `skills: []` and `tools: []`. + +How the seed flows to every surface: + +- SDK builder -> schema default at + `/home/mahmoud/code/agenta/services/oss/src/agent/schemas.py:52` + (`_DEFAULT_AGENT_TEMPLATE = build_agent_v0_default(...)`, attached as the + `agent-template` schema `default`). +- Schema default -> catalog template parameters at + `/home/mahmoud/code/agenta/api/oss/src/resources/workflows/catalog.py:107` + (`_extract_schema_parameter_defaults` reads + `parameters_schema.properties.agent.default` into `data.parameters`). +- Catalog template -> frontend pre-fill at + `/home/mahmoud/code/agenta/web/packages/agenta-entities/src/workflow/state/appUtils.ts:189` + (`createEphemeralAppFromTemplate` seeds `parameters` from `template.data.parameters`). + There is no hardcoded default config in the frontend. +- New variant fork copies the base entity's `parameters` at + `/home/mahmoud/code/agenta/web/packages/agenta-entities/src/workflow/state/commit.ts`. + +Implication: adding defaults to `build_agent_v0_default` puts them on every new-agent +surface at once. It does not touch existing agents (correct for opt-out defaults). + +The run-time fallback noted in the morning report is separate and thinner. When `/invoke` +runs by reference with no inline parameters, the API resolves the stored revision and uses +its baked `parameters` (`/home/mahmoud/code/agenta/api/oss/src/core/workflows/service.py`, +`_ensure_request_revision`). Only when `parameters.agent` is entirely absent does the +service fall back to file/class field defaults +(`/home/mahmoud/code/agenta/services/oss/src/agent/app.py:67`, +`AgentTemplate.from_params`). So by-reference invocation resolves a baked config; it does +not synthesize the platform tools or skills. + +## 2. Platform tools in config + +Catalog: `/home/mahmoud/code/agenta/sdks/python/agenta/sdk/agents/platform/op_catalog.py`, +`PLATFORM_OPS`. Exactly three reserved ops today: + +| op | reads/mutates | default permission / approval | +| --- | --- | --- | +| `find_capabilities` | read | allow, no approval | +| `query_workflows` | read | allow, no approval | +| `commit_revision` | mutates (self only) | ask, approval required | + +Tool list lives at `parameters.agent.tools`. The discriminator is `type`, with values +`builtin`, `gateway`, `code`, `client`, `reference`, `platform`. A platform entry is just: + +```json +{ "type": "platform", "op": "find_capabilities" } +``` + +The catalog owns the description, endpoint, input schema, context bindings, and the per-op +permission and approval default. `PlatformToolConfig.needs_approval` is `Optional[bool] = +None`, so an unset entry keeps the catalog default (commit_revision stays approval-gated). + +There is no wildcard or "all platform tools" today. To seed all of them you enumerate the +three entries, or have the builder iterate `PLATFORM_OPS.keys()`. Resolution turns a +platform entry into a direct `call` descriptor via +`/home/mahmoud/code/agenta/sdks/python/agenta/sdk/agents/platform/platform_tools.py`. + +## 3. Skills in config and the default skills that exist + +Skill model is `SkillTemplate` at +`/home/mahmoud/code/agenta/sdks/python/agenta/sdk/agents/skills/models.py`. One shape: an +inline SKILL.md package (`name`, `description`, `body`, `files`, `disable_model_invocation`, +`allow_executable_files`). A skill stored elsewhere is referenced by `@ag.embed` and +resolved server-side into the same shape. + +Skills list lives at `parameters.agent.skills`, sibling to `tools`. An item is either an +inline `SkillTemplate` or a bare `{"@ag.embed": {...}}` reference. + +Code-defined platform skills today: exactly one. Slug +`__ag__getting_started_with_agenta`, name `agenta-getting-started`. Source of truth is the +SDK constant `GETTING_STARTED_WITH_AGENTA_SKILL` in +`/home/mahmoud/code/agenta/sdks/python/agenta/sdk/agents/adapters/agenta_builtins.py`. It +is served read-only through `StaticWorkflowCatalog` +(`/home/mahmoud/code/agenta/api/oss/src/core/workflows/static_catalog.py`) under the +reserved `__ag__*` namespace (no DB, no per-project seed). The content is a placeholder. + +Important nuance about how this skill reaches a run today: + +- It is NOT seeded into stored config. `build_agent_v0_default` can add it via `skill_slug` + but no caller does. +- It reaches runs through the harness, not config. The `pi_agenta` harness + (`AgentaHarness`) unions `AGENTA_FORCED_SKILLS` into every run via `force_skills` + (`/home/mahmoud/code/agenta/sdks/python/agenta/sdk/agents/adapters/harnesses.py:140`). + This is always-on and cannot be disabled by the user. +- The default new-agent harness is `pi_core`, not `pi_agenta`. So a default new agent does + not even get the getting-started skill today. + +Forced skills (`force_skills`) and opt-out defaults are opposite intents. Forced means the +user can never remove it. The feature here wants present-by-default but removable. So this +feature cannot use `force_skills`; it must bake skills into the config. + +FE control: `SkillTemplateControl` at +`/home/mahmoud/code/agenta/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillTemplateControl.tsx`. +It renders `__ag__*` (static) skills read-only ("cannot be edited or removed"). + +## 4. Present-but-disabled vs removed: the model today + +No `enabled` or `disabled` flag exists on tools, skills, or MCP servers. The model is +strictly present or absent. Top-level config is `AgentTemplate` at +`/home/mahmoud/code/agenta/sdks/python/agenta/sdk/agents/dtos.py:525` with `tools`, +`mcp_servers`, `skills` lists. + +Nearby fields that are NOT a disable flag: + +- `permission: "allow"|"ask"|"deny"` on tools and MCP. `deny` means present but never runs. + It does not hide the tool from the model and does not exist on skills. +- `disable_model_invocation` on skills hides the skill from the model's auto-discovery but + still loads it (invokable via `/skill:name`). Not a present-but-off switch. + +Filtering before the runner is type-based only. `ToolResolver.resolve` +(`/home/mahmoud/code/agenta/sdks/python/agenta/sdk/agents/tools/resolver.py:115`) resolves +every tool present. `wire_skills` and `wire_mcp` emit every entry present. There is no +skip-disabled branch anywhere. So today the only way to turn an item off is to remove it. + +Precedent for present-but-disabled elsewhere in agenta: the convention is `is_active: bool = +True` at the entity/row level (webhooks, triggers, gateway connections). Not used inside a +config list yet. + +## Rename context (verified current) + +Config nests under `parameters.agent`. The catalog type-ref is `agent-template` in code +(the doc `agent-config-schema.md` is stale and still says `agent_config`). `ModelRef.params` +is now `extras`. `permission_policy` rides `runner.interactions.headless`. The `/run` wire +is byte-identical; only the authoring shape changed. diff --git a/docs/design/agent-workflows/projects/default-agent-config/status.md b/docs/design/agent-workflows/projects/default-agent-config/status.md new file mode 100644 index 0000000000..fb5bea2627 --- /dev/null +++ b/docs/design/agent-workflows/projects/default-agent-config/status.md @@ -0,0 +1,71 @@ +# Status: default agent config (playground build kit) + +## Where we are + +The design is in `design.md`. The build kit is an agent-template overlay: a partial agent template +the platform serves read-only on the inspect response, the frontend merges onto `parameters.agent` +on a playground run, and excludes on commit. The agent service stays dumb. The advanced-drawer UI +is folded into this design. One open question remains for Mahmoud. + +## The model + +- **The agent service runs the template it receives.** No run-prep merge, no run flag. It runs + `parameters.agent` as handed to it. +- **The backend serves the overlay.** It assembles the overlay once and attaches it to the inspect + response. It never applies it. +- **The frontend applies the overlay.** It reads the overlay, renders it read-only in the drawer, + merges it onto `parameters.agent` on a kit-on run, and leaves it out on commit. + +## The contract for the frontend + +- The overlay rides a new read-only container on the inspect response: + `additional_context.playground_build_kit.agent_template_overlay`, a sibling of `application` on + `SimpleApplicationResponse`. It is not in `revision.data` (user config, `extra="forbid"`, flows + into commit) and not in `application.meta` (user metadata, persisted). It is platform-owned, + read-only, derived per response. Future read-only hints add members beside `playground_build_kit`. +- The overlay is a partial `parameters.agent`. Today: the three platform ops plus an `@ag.embed` + reference to the client tool `__ag__request_connection` in `tools` (the same embed shape a skill + uses, only the slug differs), the authoring skill as an `@ag.embed` reference in `skills`, and the + build-permission elevation in `sandbox`. Skills and the client tool are ordinary embed references + with no parallel display fields; a permission's status is its value in the overlay. +- The frontend applies the overlay with a dedicated applier: deep-merge object fields, identity- + merge list fields, on a throwaway run copy only. It never mutates the draft or committed tree. +- No run flag. The toggle is client session state, default on. A kit-on run is just a run whose + `parameters.agent` already carries the merged items. The run wire is unchanged. + +(The naming and placement of `additional_context` follow a Codex review of the whole inspect response +schema, chosen over `meta` to avoid the user-owned artifact `meta` and to keep the platform-derived +container extensible.) + +## Decided + +- Toggle persistence: ephemeral per session, resets to on. Not a stored preference in v1. +- Merge precedence: the overlay wins on an identity match. When it overrides a user setting, the + drawer flags it on that user's own control with an override hint (toggle the kit off to match the + published agent). +- Change 1 (collapsible sections): ships as a separate, independent change, not part of the + build-kit core. + +## Open questions for Mahmoud + +1. Confirm the published default goes bare (drop the authoring skill and the sandbox boundary from + the schema default into the overlay). Touches the skills project. + +## Coordination + +- The advanced-drawer UI is folded into this design, not a separate PR. +- Authoring skill content and naming: skills project (`#4918`). We reference the reserved slug only. +- Builder tools (`#4919`) add more platform ops; the builder reads `PLATFORM_OPS` at assembly time, + so new ops join automatically. The builder also enumerates the reserved-slug platform workflows + in the static workflow catalog and adds each as an `@ag.embed` reference, parallel to iterating + `PLATFORM_OPS`. +- A client tool such as `request_connection` is not a platform op; its primary definition is owned + by `#4920`. The kit carries it as an `@ag.embed` reference to its reserved `__ag__*` slug, the + same embed shape a skill uses, only the slug differs. +- The overlay model carries through to `#4918`, `#4919`, and `#4920`. Do not edit them here; the + orchestrator propagates the design after Mahmoud approves it. + +## Out of scope + +- Per-item edit or delete of kit items (the kit is whole-toggle, read-only in v1). +- A picker to add platform tools to the published agent. diff --git a/services/oss/src/agent/schemas.py b/services/oss/src/agent/schemas.py index 2b6ea1ec46..cf9effb8b3 100644 --- a/services/oss/src/agent/schemas.py +++ b/services/oss/src/agent/schemas.py @@ -8,7 +8,6 @@ change and stays out of the handler logic. """ -from agenta.sdk.agents.adapters.agenta_builtins import GETTING_STARTED_WITH_AGENTA_SLUG from agenta.sdk.utils.types import build_agent_v0_default _SCHEMA = "https://json-schema.org/draft/2020-12/schema" @@ -37,22 +36,9 @@ # catalog type keeps the typed tools/mcps shape in one place; this schema only carries the default the # playground pre-fills. The agent handler passes `parameters` verbatim to `AgentTemplate.from_params`, # which reads the template at `parameters.agent` (so a tool is at `parameters.agent.tools`). -# Reserved slug of the static default skill, served from code by the StaticWorkflowCatalog -# (api/oss/src/core/workflows/static_catalog.py), never the database. The default config -# references it by stable slug through an @ag.embed; the embed resolver inlines the catalogue's -# SkillTemplate (at the canonical parameters.skill selector) before the runner sees it. The -# `__ag__` prefix is reserved: a user cannot author or shadow it. This replaces both -# AGENTA_FORCED_SKILLS and the old per-project skill seeder. Single source: the SDK constant. -_DEFAULT_SKILL_SLUG = GETTING_STARTED_WITH_AGENTA_SLUG - -# The service default = the shared builder (single source, in the SDK) plus the two service-only -# choices: the static default skill (inlined from the reserved slug) and the declared Layer-2 -# sandbox boundary the playground pre-fills. The SDK builtin interface uses the same builder -# without these, so a new default field changes one place. -_DEFAULT_AGENT_TEMPLATE = build_agent_v0_default( - skill_slug=_DEFAULT_SKILL_SLUG, - include_sandbox_permission=True, -) +# The published default stays bare. The playground build kit is served as read-only inspect/fetch +# context and applied only to playground runs; it is not part of the committed agent template. +_DEFAULT_AGENT_TEMPLATE = build_agent_v0_default() AGENT_TEMPLATE_SCHEMA = { "type": "object", diff --git a/services/oss/tests/pytest/unit/agent/test_default_agent_template.py b/services/oss/tests/pytest/unit/agent/test_default_agent_template.py index b3c7bab29a..c7674050cb 100644 --- a/services/oss/tests/pytest/unit/agent/test_default_agent_template.py +++ b/services/oss/tests/pytest/unit/agent/test_default_agent_template.py @@ -16,7 +16,7 @@ from agenta.sdk.engines.running.interfaces import agent_v0_interface from agenta.sdk.utils.types import build_agent_v0_default -from oss.src.agent.schemas import AGENT_SCHEMAS, _DEFAULT_SKILL_SLUG +from oss.src.agent.schemas import AGENT_SCHEMAS def _inspect_agent_default() -> dict: @@ -35,50 +35,40 @@ def test_builtin_default_is_the_bare_builder(): assert _builtin_agent_default() == build_agent_v0_default() -def test_service_default_is_the_builder_plus_service_only_choices(): - # The service default is the same builder plus the two service-only choices, passed as - # named args (not a second copy): the platform default skill and the declared sandbox boundary. - assert _inspect_agent_default() == build_agent_v0_default( - skill_slug=_DEFAULT_SKILL_SLUG, - include_sandbox_permission=True, - ) +def test_service_default_is_the_bare_builder(): + # The playground build kit carries authoring extras; the published default stays bare. + assert _inspect_agent_default() == build_agent_v0_default() def test_inspect_default_parses_into_the_runtime_selection(): # The default the playground pre-fills on `/inspect` must parse cleanly into the same runtime - # values `AgentTemplate.from_params` produces, so what the user sees is what the agent runs. The - # `@ag.embed` skill resolves server-side before this parse, so the config-level round-trip is - # asserted on the non-skill fields plus the execution selectors. + # values `AgentTemplate.from_params` produces, so what the user sees is what the agent runs. inspect_default = _inspect_agent_default() - no_skill = {k: v for k, v in inspect_default.items() if k != "skills"} - params = {"agent": no_skill} + params = {"agent": inspect_default} config = AgentTemplate.from_params(params) assert config.model == inspect_default["llm"]["model"] assert config.instructions == inspect_default["instructions"]["agents_md"] - assert ( - config.sandbox_permission is not None - ) # the service boundary survives the parse + assert config.sandbox_permission is None assert config.harness == "pi_core" assert config.sandbox == "local" assert config.permission_policy == "auto" -def test_service_only_extras_present_in_inspect_absent_from_builtin(): - # The platform default skill and the sandbox boundary ride the SERVICE default (the playground - # pre-fill + the runtime fallback) and are intentionally ABSENT from the SDK builtin, which is - # the minimal harness-agnostic shape with no platform opinion. They are in both inspect and - # runtime via the SAME service default object, so they cannot drift between the two. +def test_authoring_extras_absent_from_every_published_default(): + # Platform tools, the authoring skill, and elevated sandbox permissions belong to the + # playground build-kit overlay, not to the published default template. inspect_default = _inspect_agent_default() builtin_default = _builtin_agent_default() - assert "permissions" in inspect_default["sandbox"] - assert ( - inspect_default["skills"][0]["@ag.embed"]["@ag.references"]["workflow"]["slug"] - == _DEFAULT_SKILL_SLUG - ) + assert inspect_default["tools"] == [] + assert "skills" not in inspect_default + assert "permissions" not in inspect_default["sandbox"] + assert "execute_code" not in inspect_default["sandbox"] + assert "write_files" not in inspect_default["sandbox"] + assert builtin_default["tools"] == [] assert "permissions" not in builtin_default["sandbox"] assert "skills" not in builtin_default diff --git a/web/packages/agenta-entities/src/workflow/api/api.ts b/web/packages/agenta-entities/src/workflow/api/api.ts index 7f0f17321b..a99e64bb10 100644 --- a/web/packages/agenta-entities/src/workflow/api/api.ts +++ b/web/packages/agenta-entities/src/workflow/api/api.ts @@ -445,6 +445,11 @@ export interface InspectWorkflowResponse { } request?: Record meta?: Record + additional_context?: { + playground_build_kit?: { + agent_template_overlay?: Record | null + } | null + } | null } /** diff --git a/web/packages/agenta-entities/src/workflow/index.ts b/web/packages/agenta-entities/src/workflow/index.ts index 9c520ff89b..623dff1265 100644 --- a/web/packages/agenta-entities/src/workflow/index.ts +++ b/web/packages/agenta-entities/src/workflow/index.ts @@ -53,6 +53,12 @@ export { type HarnessCapabilitiesMap, } from "./state/inspectMeta" +export { + workflowAgentTemplateOverlayAtomFamily, + workflowBuildKitEnabledAtomFamily, + type AgentTemplate, +} from "./state" + // ============================================================================ // SCHEMAS & TYPES // ============================================================================ diff --git a/web/packages/agenta-entities/src/workflow/state/commit.ts b/web/packages/agenta-entities/src/workflow/state/commit.ts index 48ab23ef6c..2a33490be1 100644 --- a/web/packages/agenta-entities/src/workflow/state/commit.ts +++ b/web/packages/agenta-entities/src/workflow/state/commit.ts @@ -69,6 +69,8 @@ function prepareCommitParameters( entity: Workflow, flatParams: Record | null, ): Record | undefined { + // The playground build-kit overlay lives in read-only additional_context/session atoms. Commit + // reads only the user-owned revision config here, so platform ops and sandbox elevation stay out. const rawParams = stripAgentaMetadataDeep(entity.data?.parameters) as | Record | undefined diff --git a/web/packages/agenta-entities/src/workflow/state/index.ts b/web/packages/agenta-entities/src/workflow/state/index.ts index 9e031e2a4b..a2348db497 100644 --- a/web/packages/agenta-entities/src/workflow/state/index.ts +++ b/web/packages/agenta-entities/src/workflow/state/index.ts @@ -56,6 +56,9 @@ export { workflowEntityAtomFamily, workflowIsDirtyAtomFamily, workflowIsEphemeralAtomFamily, + workflowAgentTemplateOverlayAtomFamily, + workflowBuildKitEnabledAtomFamily, + type AgentTemplate, // Mutations updateWorkflowDraftAtom, discardWorkflowDraftAtom, diff --git a/web/packages/agenta-entities/src/workflow/state/store.ts b/web/packages/agenta-entities/src/workflow/state/store.ts index 55cc8abd76..c029349eca 100644 --- a/web/packages/agenta-entities/src/workflow/state/store.ts +++ b/web/packages/agenta-entities/src/workflow/state/store.ts @@ -1097,6 +1097,27 @@ export const workflowInspectAtomFamily = atomFamily((revisionId: string) => }), ) +// ============================================================================ +// PLAYGROUND BUILD KIT SESSION STATE +// ============================================================================ + +export type AgentTemplate = Record + +export const workflowAgentTemplateOverlayAtomFamily = atomFamily((revisionId: string) => + atom((get) => { + const inspectData = get(workflowInspectAtomFamily(revisionId)).data ?? null + const overlay = + inspectData?.additional_context?.playground_build_kit?.agent_template_overlay ?? null + return overlay && typeof overlay === "object" && !Array.isArray(overlay) + ? (overlay as AgentTemplate) + : null + }), +) + +export const workflowBuildKitEnabledAtomFamily = atomFamily((_revisionId: string) => + atom(true), +) + // ============================================================================ // AG-TYPE SCHEMA QUERY (resolves x-ag-type-ref targets into full schemas) // ============================================================================ 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 4fe789f956..44452f51f0 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx @@ -25,7 +25,11 @@ import {useCallback, useEffect, useMemo, useRef, useState} from "react" import {vaultSecretsQueryAtom} from "@agenta/entities/secret" import type {SchemaProperty} from "@agenta/entities/shared" -import {harnessCapabilitiesAtomFamily} from "@agenta/entities/workflow" +import { + harnessCapabilitiesAtomFamily, + workflowAgentTemplateOverlayAtomFamily, + workflowBuildKitEnabledAtomFamily, +} from "@agenta/entities/workflow" import {ConfigAccordionSection, LabeledField} from "@agenta/ui/components/presentational" import {useDrillInUI, type WorkflowReferencePayload} from "@agenta/ui/drill-in" import {SelectLLMProviderBase} from "@agenta/ui/select-llm-provider" @@ -51,7 +55,7 @@ import { Wrench, } from "@phosphor-icons/react" import {Button, Select, Switch, Tabs, Tag, Tooltip, Typography} from "antd" -import {useAtomValue} from "jotai" +import {useAtom, useAtomValue} from "jotai" import {useOptionalDrillIn} from "../components/MoleculeDrillInContext" @@ -485,6 +489,33 @@ function ItemRow({ ) } +function ReadOnlyItemRow({descriptor}: {descriptor: ItemDescriptor}) { + return ( +
+ +
+
{descriptor.name}
+ {descriptor.description ? ( + + {descriptor.description} + + ) : null} +
+
+ {descriptor.tags.map((tag) => ( + + {tag} + + ))} + Locked +
+
+ ) +} + /** * An instructions markdown file row. Avatar + filename + a 2-line preview of the (markdown-stripped) * content, clamped with an ellipsis. The whole row opens the editor drawer for the full content — @@ -544,6 +575,76 @@ function InstructionsFileRow({ ) } +function isEmbedRefEntry(entry: unknown): entry is Record { + return Boolean( + entry && typeof entry === "object" && "@ag.embed" in (entry as Record), + ) +} + +function describeBuildKitPlatformTool(tool: Record): ItemDescriptor { + const op = typeof tool.op === "string" ? tool.op : "platform tool" + return { + name: op, + description: "Platform-owned playground tool", + mono: "", + color: "#0d9488", + icon: , + tags: ["platform"], + typeLabel: "platform", + typeColor: "cyan", + subtitle: "Platform tool", + } +} + +function describeBuildKitEmbed( + entry: Record, + kind: "tool" | "skill", +): ItemDescriptor { + const slug = staticEmbedSlug(entry) + return { + name: slug ?? `${kind} reference`, + description: "Provided by Agenta. This item cannot be edited or removed.", + mono: kind === "tool" ? "wf" : "sk", + color: kind === "tool" ? "#0d9488" : "#6b7280", + tags: ["@ag.embed"], + typeLabel: "@ag.embed", + typeColor: "blue", + subtitle: "Agenta-owned reference", + } +} + +function formatPermissionValue(value: unknown): string { + if (typeof value === "string") return value + if (typeof value === "number" || typeof value === "boolean") return String(value) + if (value == null) return "null" + try { + return JSON.stringify(value) + } catch { + return String(value) + } +} + +function stableString(value: unknown): string { + try { + return JSON.stringify(value) + } catch { + return String(value) + } +} + +function overriddenPermissionKeys( + userPermissions: Record | null | undefined, + overlayPermissions: Record | null | undefined, +): string[] { + if (!userPermissions || !overlayPermissions) return [] + return Object.entries(overlayPermissions) + .filter(([key, overlayValue]) => { + if (!(key in userPermissions)) return false + return stableString(userPermissions[key]) !== stableString(overlayValue) + }) + .map(([key]) => key) +} + export function AgentTemplateControl({ schema, value, @@ -692,6 +793,13 @@ export function AgentTemplateControl({ const capabilities = useAtomValue( useMemo(() => harnessCapabilitiesAtomFamily(revisionId ?? ""), [revisionId]), ) + const agentTemplateOverlay = useAtomValue( + useMemo(() => workflowAgentTemplateOverlayAtomFamily(revisionId ?? ""), [revisionId]), + ) + const [buildKitEnabled, setBuildKitEnabled] = useAtom( + useMemo(() => workflowBuildKitEnabledAtomFamily(revisionId ?? ""), [revisionId]), + ) + const [buildKitExpanded, setBuildKitExpanded] = useState(true) // 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 @@ -1027,12 +1135,55 @@ export function AgentTemplateControl({ const hasMcp = Boolean(props.mcps) const hasSkills = Boolean(props.skills) const hasClaudePermissions = harnessValue === "claude" + const overlayTools = useMemo( + () => (Array.isArray(agentTemplateOverlay?.tools) ? agentTemplateOverlay.tools : []), + [agentTemplateOverlay], + ) + const overlaySkills = useMemo( + () => (Array.isArray(agentTemplateOverlay?.skills) ? agentTemplateOverlay.skills : []), + [agentTemplateOverlay], + ) + const overlaySandbox = useMemo( + () => asObj(agentTemplateOverlay?.sandbox), + [agentTemplateOverlay], + ) + const overlayPermissions = useMemo(() => asObj(overlaySandbox?.permissions), [overlaySandbox]) + const platformOverlayTools = useMemo( + () => + overlayTools.filter((tool): tool is Record => + Boolean(asObj(tool)?.type === "platform"), + ), + [overlayTools], + ) + const embeddedOverlayTools = useMemo(() => overlayTools.filter(isEmbedRefEntry), [overlayTools]) + const embeddedOverlaySkills = useMemo( + () => overlaySkills.filter(isEmbedRefEntry), + [overlaySkills], + ) + const hasBuildKitOverlay = Boolean( + agentTemplateOverlay && + (platformOverlayTools.length > 0 || + embeddedOverlayTools.length > 0 || + embeddedOverlaySkills.length > 0 || + Object.keys(overlayPermissions ?? {}).length > 0), + ) + const sandboxPermissionOverrideKeys = useMemo( + () => + buildKitEnabled + ? overriddenPermissionKeys( + (sandbox.permissions as Record | null) ?? null, + overlayPermissions, + ) + : [], + [buildKitEnabled, sandbox.permissions, overlayPermissions], + ) const hasAdvanced = Boolean( props.llm || // Authentication lives in Advanced now sandboxProps.kind || sandboxProps.permissions || runnerProps.interactions || - hasClaudePermissions, + hasClaudePermissions || + hasBuildKitOverlay, ) // Shared props for the tool picker, so the in-body popover and the header quick-add trigger @@ -1492,8 +1643,122 @@ export function AgentTemplateControl({ // both the wide drawer body (with the version-history side panel) and the tabs-inline body. const advancedControls = ( <> + {hasBuildKitOverlay ? ( +
+ + {buildKitExpanded ? ( +
+

+ These playground-only tools, skills, and permissions help the + assistant build and revise this agent. None of this is part of the + published agent. +

+ {!buildKitEnabled ? ( +
+ The assistant can no longer create files, run code, or edit the + agent here. +
+ ) : null} + {platformOverlayTools.length > 0 ? ( +
+ + Platform tools + + {platformOverlayTools.map((tool, index) => ( + + ))} +
+ ) : null} + {embeddedOverlayTools.length > 0 ? ( +
+ + Embedded tools + + {embeddedOverlayTools.map((tool, index) => ( + + ))} +
+ ) : null} + {embeddedOverlaySkills.length > 0 ? ( +
+ + Embedded skills + + {embeddedOverlaySkills.map((skill, index) => ( + + ))} +
+ ) : null} + {overlayPermissions && Object.keys(overlayPermissions).length > 0 ? ( +
+ + Sandbox permissions + +
+ {Object.entries(overlayPermissions).map(([key, value]) => ( +
+ {key} + + {formatPermissionValue(value)} + +
+ ))} +
+
+ ) : null} +
+ ) : null} +
+ ) : null} + {authControls ? ( -
+
Authentication @@ -1526,15 +1791,27 @@ export function AgentTemplateControl({ /> )} {sandboxProps.permissions ? ( - | null) ?? null - } - onChange={(v) => - setSection("sandbox", {...sandbox, permissions: v}) - } - disabled={disabled} - /> +
+ {sandboxPermissionOverrideKeys.length > 0 ? ( + +
+ + Build kit overrides{" "} + {sandboxPermissionOverrideKeys.join(", ")} +
+
+ ) : null} + | null) ?? + null + } + onChange={(v) => + setSection("sandbox", {...sandbox, permissions: v}) + } + disabled={disabled} + /> +
) : null}
diff --git a/web/packages/agenta-playground/src/state/execution/agentRequest.ts b/web/packages/agenta-playground/src/state/execution/agentRequest.ts index 3e60aaefb4..3f58290196 100644 --- a/web/packages/agenta-playground/src/state/execution/agentRequest.ts +++ b/web/packages/agenta-playground/src/state/execution/agentRequest.ts @@ -24,7 +24,12 @@ * - `project_id` / `application_id` ride the URL QUERY (never the body), and * `project_id` only travels alongside auth — mirroring `executionItems.ts`. */ -import {workflowMolecule} from "@agenta/entities/workflow" +import { + workflowAgentTemplateOverlayAtomFamily, + workflowBuildKitEnabledAtomFamily, + workflowMolecule, + type AgentTemplate, +} from "@agenta/entities/workflow" import {projectIdAtom} from "@agenta/shared/state" import {getDefaultStore} from "jotai" @@ -40,6 +45,133 @@ export interface AgentRequest { /** Minimal store surface — the default Jotai store, or a test store. */ type StoreLike = Pick, "get"> +type AgentTemplateListKey = "tools" | "skills" | "mcps" +type AgentTemplateObjectKey = "sandbox" | "runner" | "harness" | "llm" | "instructions" + +const isRecord = (value: unknown): value is Record => + Boolean(value && typeof value === "object" && !Array.isArray(value)) + +const embedWorkflowSlug = (entry: unknown): string | undefined => { + if (!isRecord(entry)) return undefined + const embed = entry["@ag.embed"] + if (!isRecord(embed)) return undefined + const refs = embed["@ag.references"] + if (!isRecord(refs)) return undefined + const workflow = refs.workflow + if (isRecord(workflow) && typeof workflow.slug === "string") return workflow.slug + const revision = refs.workflow_revision + if (isRecord(revision) && typeof revision.slug === "string") return revision.slug + return undefined +} + +const deepMerge = ( + base: Record, + overlay: Record, +): Record => { + const result: Record = {...base} + for (const [key, value] of Object.entries(overlay)) { + const existing = result[key] + result[key] = isRecord(existing) && isRecord(value) ? deepMerge(existing, value) : value + } + return result +} + +const getToolIdentity = (entry: unknown): string | undefined => { + if (!isRecord(entry)) return undefined + if (entry.type === "platform" && typeof entry.op === "string") return `platform:${entry.op}` + const slug = embedWorkflowSlug(entry) + if (slug) return `workflow:${slug}` + return typeof entry.name === "string" ? `name:${entry.name}` : undefined +} + +const getSkillIdentity = (entry: unknown): string | undefined => { + const slug = embedWorkflowSlug(entry) + return slug ? `workflow:${slug}` : undefined +} + +const getMcpIdentity = (entry: unknown): string | undefined => { + if (!isRecord(entry)) return undefined + return typeof entry.name === "string" ? entry.name : undefined +} + +const identityMerge = ( + base: unknown[], + overlay: unknown[], + getIdentity: (entry: unknown) => string | undefined, +): unknown[] => { + const result = [...base] + const indexByIdentity = new Map() + result.forEach((entry, index) => { + const identity = getIdentity(entry) + if (identity) indexByIdentity.set(identity, index) + }) + overlay.forEach((entry) => { + const identity = getIdentity(entry) + const index = identity ? indexByIdentity.get(identity) : undefined + if (index !== undefined) { + result[index] = entry + return + } + if (identity) indexByIdentity.set(identity, result.length) + result.push(entry) + }) + return result +} + +export function applyBuildKitOverlay( + base: AgentTemplate, + overlay: Partial, +): AgentTemplate { + const result: AgentTemplate = {...base} + + for (const key of [ + "sandbox", + "runner", + "harness", + "llm", + "instructions", + ] as const satisfies readonly AgentTemplateObjectKey[]) { + const overlayValue = overlay[key] + if (overlayValue !== undefined) { + result[key] = deepMerge( + isRecord(base[key]) ? (base[key] as Record) : {}, + isRecord(overlayValue) ? overlayValue : {}, + ) + } + } + + const listMergers: Record string | undefined> = { + tools: getToolIdentity, + skills: getSkillIdentity, + mcps: getMcpIdentity, + } + + for (const key of Object.keys(listMergers) as AgentTemplateListKey[]) { + const overlayValue = overlay[key] + if (Array.isArray(overlayValue)) { + result[key] = identityMerge( + Array.isArray(base[key]) ? (base[key] as unknown[]) : [], + overlayValue, + listMergers[key], + ) + } + } + + return result +} + +const withBuildKitOverlay = ( + parameters: Record, + overlay: AgentTemplate | null, + enabled: boolean, +): Record => { + if (!enabled || !overlay || !isRecord(parameters.agent)) return parameters + return { + ...parameters, + agent: applyBuildKitOverlay(parameters.agent as AgentTemplate, overlay), + } +} + // Backend rejects local-draft ids; only forward real UUIDs in `references`. const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i const realId = (value: unknown): string | undefined => { @@ -306,10 +438,17 @@ export async function buildAgentRequest( | undefined // The execution sections (`harness`/`runner`/`sandbox`) are nested in the template at // `parameters.agent`. Default them, never overriding values the resolved config carries. - const parameters = pruneBlankEntries(withAgentRunDefaults(config ?? {})) as Record< - string, - unknown - > + const buildKitEnabled = store.get(workflowBuildKitEnabledAtomFamily(entityId)) as boolean + const agentTemplateOverlay = store.get( + workflowAgentTemplateOverlayAtomFamily(entityId), + ) as AgentTemplate | null + const parameters = pruneBlankEntries( + withBuildKitOverlay( + withAgentRunDefaults(config ?? {}) as Record, + agentTemplateOverlay, + buildKitEnabled, + ), + ) as Record const entity = store.get(workflowMolecule.selectors.data(entityId)) as | RevisionLike diff --git a/web/packages/agenta-playground/src/state/execution/index.ts b/web/packages/agenta-playground/src/state/execution/index.ts index f1995badac..d8b24636c7 100644 --- a/web/packages/agenta-playground/src/state/execution/index.ts +++ b/web/packages/agenta-playground/src/state/execution/index.ts @@ -351,7 +351,12 @@ export { } from "./displayedEntities" // Agent-lane request builder (bypasses buffered-fetch execution; useChat streams). -export {buildAgentRequest, buildAgentReferences, type AgentRequest} from "./agentRequest" +export { + applyBuildKitOverlay, + buildAgentRequest, + buildAgentReferences, + type AgentRequest, +} from "./agentRequest" // Stream vs batch response channel for the agent lane (read by buildAgentRequest's Accept header). export {agentChannelModeAtom, type AgentChannelMode} from "./channelMode" // Agent-lane HITL resume predicate (approve AND deny both resume the conversation). diff --git a/web/packages/agenta-playground/src/state/index.ts b/web/packages/agenta-playground/src/state/index.ts index 58b0d495c2..914f0af673 100644 --- a/web/packages/agenta-playground/src/state/index.ts +++ b/web/packages/agenta-playground/src/state/index.ts @@ -175,7 +175,12 @@ export {inputVariableNamesAtom} from "./execution" // App-level mode selectors export {appTypeAtom, isChatModeAtom, type AppType} from "./execution" export {isAgentModeAtomFamily} from "./execution" -export {buildAgentRequest, buildAgentReferences, type AgentRequest} from "./execution" +export { + applyBuildKitOverlay, + buildAgentRequest, + buildAgentReferences, + type AgentRequest, +} from "./execution" export {agentChannelModeAtom, type AgentChannelMode} from "./execution" export {agentShouldResumeAfterApproval} from "./execution" export {canReleaseQueuedMessage, isHitlPending} from "./execution" diff --git a/web/packages/agenta-playground/tests/unit/agentRequest.test.ts b/web/packages/agenta-playground/tests/unit/agentRequest.test.ts index 70a91df6a0..c93a5f4e5c 100644 --- a/web/packages/agenta-playground/tests/unit/agentRequest.test.ts +++ b/web/packages/agenta-playground/tests/unit/agentRequest.test.ts @@ -37,13 +37,23 @@ vi.mock("@agenta/entities/workflow", async (importOriginal) => { isDirty: mk(false), }, }, + workflowAgentTemplateOverlayAtomFamily: mk | null>(null), + workflowBuildKitEnabledAtomFamily: mk(true), } }) -import {workflowMolecule} from "@agenta/entities/workflow" +import { + workflowAgentTemplateOverlayAtomFamily, + workflowBuildKitEnabledAtomFamily, + workflowMolecule, +} from "@agenta/entities/workflow" import {projectIdAtom} from "@agenta/shared/state" -import {buildAgentRequest, buildAgentReferences} from "../../src/state/execution/agentRequest" +import { + applyBuildKitOverlay, + buildAgentRequest, + buildAgentReferences, +} from "../../src/state/execution/agentRequest" import {agentChannelModeAtom} from "../../src/state/execution/channelMode" import {executionHeadersAtom} from "../../src/state/execution/webWorkerIntegration" @@ -62,6 +72,8 @@ function seed( config?: Record | null data?: any isDirty?: boolean + overlay?: Record | null + buildKitEnabled?: boolean }, ) { set( @@ -73,6 +85,22 @@ function seed( set(store, workflowMolecule.selectors.configuration, id, over.config ?? {temperature: 0.7}) set(store, workflowMolecule.selectors.data, id, over.data ?? null) set(store, workflowMolecule.selectors.isDirty, id, over.isDirty ?? false) + store.set( + workflowAgentTemplateOverlayAtomFamily(id) as PrimitiveAtom, + over.overlay ?? null, + ) + store.set( + workflowBuildKitEnabledAtomFamily(id) as PrimitiveAtom, + over.buildKitEnabled ?? true, + ) +} + +const authoringSkill = { + "@ag.embed": {"@ag.references": {workflow: {slug: "__ag__getting_started_with_agenta"}}}, +} + +const requestConnectionTool = { + "@ag.embed": {"@ag.references": {workflow: {slug: "__ag__request_connection"}}}, } describe("buildAgentReferences (draft-id stripping)", () => { @@ -188,6 +216,116 @@ describe("buildAgentRequest", () => { expect(template.runner.interactions.headless).toBe("deny") }) + it("applies the build-kit overlay to a kit-on run with deep and identity merges", async () => { + const config = { + agent: { + sandbox: {kind: "local", permissions: {execute_code: "deny", network: "on"}}, + tools: [ + {type: "platform", op: "find_capabilities", permission: "ask"}, + {type: "client", name: "weather"}, + requestConnectionTool, + ], + skills: [authoringSkill], + }, + } + const overlay = { + sandbox: {permissions: {execute_code: "allow", write_files: "allow"}}, + tools: [ + {type: "platform", op: "find_capabilities", permission: "allow"}, + {type: "platform", op: "commit_revision"}, + requestConnectionTool, + ], + skills: [authoringSkill], + } + const before = JSON.parse(JSON.stringify(config)) + seed(store, "e", {config, overlay, buildKitEnabled: true}) + + const req = await buildAgentRequest("e", [], {sessionId: "s1", store}) + const template = (req!.requestBody.data as any).parameters.agent + + expect(template.sandbox).toEqual({ + kind: "local", + permissions: {execute_code: "allow", network: "on", write_files: "allow"}, + }) + expect(template.tools).toEqual([ + {type: "platform", op: "find_capabilities", permission: "allow"}, + {type: "client", name: "weather"}, + requestConnectionTool, + {type: "platform", op: "commit_revision"}, + ]) + expect(template.skills).toEqual([authoringSkill]) + expect(config).toEqual(before) + }) + + it("sends the bare agent config unchanged when the build kit is off", async () => { + const config = { + agent: { + sandbox: {kind: "local", permissions: {execute_code: "deny"}}, + tools: [{type: "client", name: "weather"}], + }, + } + seed(store, "e", { + config, + overlay: { + sandbox: {permissions: {execute_code: "allow", write_files: "allow"}}, + tools: [{type: "platform", op: "commit_revision"}], + skills: [authoringSkill], + }, + buildKitEnabled: false, + }) + + const req = await buildAgentRequest("e", [], {sessionId: "s1", store}) + const template = (req!.requestBody.data as any).parameters.agent + + expect(template.sandbox.permissions).toEqual({execute_code: "deny"}) + expect(template.tools).toEqual([{type: "client", name: "weather"}]) + expect(template.skills).toBeUndefined() + }) + + it("leaves entity parameters bare so commit reads never include the kit", async () => { + const config = { + agent: { + sandbox: {kind: "local"}, + tools: [{type: "client", name: "weather"}], + }, + } + seed(store, "e", { + config, + overlay: { + sandbox: {permissions: {execute_code: "allow", write_files: "allow"}}, + tools: [{type: "platform", op: "commit_revision"}], + }, + }) + + await buildAgentRequest("e", [], {sessionId: "s1", store}) + + expect(store.get(workflowMolecule.selectors.configuration("e"))).toEqual(config) + expect(JSON.stringify(config)).not.toContain("commit_revision") + expect(JSON.stringify(config)).not.toContain("write_files") + }) + + it("applyBuildKitOverlay never mutates its input template", () => { + const base = { + sandbox: {kind: "local", permissions: {execute_code: "deny"}}, + tools: [{type: "platform", op: "find_capabilities", permission: "ask"}], + skills: [], + } + const snapshot = JSON.parse(JSON.stringify(base)) + + const result = applyBuildKitOverlay(base, { + sandbox: {permissions: {execute_code: "allow"}}, + tools: [{type: "platform", op: "find_capabilities", permission: "allow"}], + skills: [authoringSkill], + }) + + expect(base).toEqual(snapshot) + expect(result).not.toBe(base) + expect(result.sandbox).toEqual({ + kind: "local", + permissions: {execute_code: "allow"}, + }) + }) + it("INCLUDES references for a CLEAN committed revision run (marks it non-draft)", async () => { // A run of an unmodified committed revision claims its identity: the service marks it // non-draft from the resolved revision reference and a self-targeting tool binds it.