From dcf97961145f5ad110bd482492135b8b6ff5e655 Mon Sep 17 00:00:00 2001 From: Andy Boutte Date: Mon, 12 Oct 2026 09:50:23 -0500 Subject: [PATCH 1/3] feat(duplo-extension-dev): file extensions under a root-level agent The extension skills now ask which agent an extension is filed under and write it as an optional `agent` field at the manifest root, beside id, name and version: a plain string, or { name, order } where order suggests the agent heading's position in the left nav. - The portal groups every extension sharing an agent under one heading; "DevOps" merges into the built-in DevOps group - Every top-level frontend.menus[] node renders as a child of the heading; a menu node's order positions it under the heading only - The agent order suggests the heading's own position (lowest wins), overridable via config.json ai-menu.order keyed by the heading id - One agent per extension; omitted rather than written as agent: "" - The helloworld template carries { "name": "DevOps", "order": 30 } at the root and drops its DevOps wrapper node Co-Authored-By: Claude Opus 4.5 --- .claude/commands/duplo-extension.md | 3 + .claude/skills/duplo-extension-dev/SKILL.md | 7 +++ .../reference/02-authoring-guide.md | 1 + .../reference/08-parent-child-and-menus.md | 59 +++++++++++++++++++ .../templates/helloworld/manifest.json | 20 +++---- 5 files changed, 77 insertions(+), 13 deletions(-) diff --git a/.claude/commands/duplo-extension.md b/.claude/commands/duplo-extension.md index 0abe226..3b775a8 100644 --- a/.claude/commands/duplo-extension.md +++ b/.claude/commands/duplo-extension.md @@ -157,6 +157,9 @@ backend + Angular remote + provisioning skill) — never ask the user to choose sit under a parent / an extension category? Should any endpoint require an **explicitly granted named action** rather than plain CRUD (restart, console/exec, credential-minting shapes — see reference/18-access-control.md)? - **menu** placement (a top-level `collapsible-section` with `item` children renders nested correctly); +- **agent** — which agent (DevOps / Compliance / Observability / Release Management / custom) this + extension files under, written as the manifest-root `agent` field (never on a menu node); optionally a heading + order (`{ "name": …, "order": N }`) only if the author wants one; omit the field if the author doesn't pick one; - **Ask AI** is opt-in: scaffold it ONLY if the user's requirement explicitly asks for AI chat sessions on the resource — never offer it or add it unprompted (see skill reference 16-ask-ai); - for an external system: **auth** style (basic/bearer) and which API calls provisioning makes (a custom diff --git a/.claude/skills/duplo-extension-dev/SKILL.md b/.claude/skills/duplo-extension-dev/SKILL.md index 549533d..39917db 100644 --- a/.claude/skills/duplo-extension-dev/SKILL.md +++ b/.claude/skills/duplo-extension-dev/SKILL.md @@ -153,6 +153,13 @@ Settle before building (derive, else ask — **after** listening): **no menu**. The user gives titles; **you** generate ids, `relativeUrl`s and the matching `frontend.routes[]` — [08-parent-child-and-menus](reference/08-parent-child-and-menus.md). Also settle **title** + **`matIcon`** per item — derive the icon from the extension's purpose, per the Left-nav icon bullet above. +- **Agent** — ask which agent this extension is filed under: DevOps, Compliance, Observability, Release + Management, or a typed custom name. Write it verbatim as the **manifest-root** `agent` field (sibling of `id` / + `name` / `version`; never on a menu node; one agent per extension). Only if the author wants to position the + agent's left-nav heading, also ask for an order and write `"agent": { "name": "…", "order": N }`; otherwise the + plain string. Every top-level `menus[]` node then renders as a child of the agent's heading, so don't wrap menus + in a node titled after the agent ([08 → Filing under an agent](reference/08-parent-child-and-menus.md#filing-under-an-agent--the-root-agent-field)). + If the author declines to pick one, omit the field entirely — never write `agent: ""`. - **One or many resources?** — **flat** (independent — e.g. `GCP` → network/cluster/database) or **parent → child** (`samples/parent-child`); several top-level menu groups are fine — [08](reference/08-parent-child-and-menus.md). diff --git a/.claude/skills/duplo-extension-dev/reference/02-authoring-guide.md b/.claude/skills/duplo-extension-dev/reference/02-authoring-guide.md index c0b9bb3..216f3e6 100644 --- a/.claude/skills/duplo-extension-dev/reference/02-authoring-guide.md +++ b/.claude/skills/duplo-extension-dev/reference/02-authoring-guide.md @@ -61,6 +61,7 @@ Ties the bundle together. For a typed extension (see the hello-world manifest): { "manifestVersion": "1.0", "id": "duplo.examples.helloworld", "name": "Hello World", "version": "0.1.0", + "agent": "DevOps", // optional, root-level — see 08-parent-child-and-menus → Filing under an agent "sdkVersion": "", "backend": { "assemblyDir": "duplo.examples.helloworld/0.1.0/backend", "entryAssembly": "Duplo.Extension.HelloWorld.dll" }, diff --git a/.claude/skills/duplo-extension-dev/reference/08-parent-child-and-menus.md b/.claude/skills/duplo-extension-dev/reference/08-parent-child-and-menus.md index 688d35c..2495be9 100644 --- a/.claude/skills/duplo-extension-dev/reference/08-parent-child-and-menus.md +++ b/.claude/skills/duplo-extension-dev/reference/08-parent-child-and-menus.md @@ -166,6 +166,65 @@ see [00-naming](00-naming.md)): `extension-route-registrar.ts` injects these under `suite/:tenantId` at runtime (no host rebuild). A child resolver recovers `:parentId` by walking `ActivatedRoute.params`. +### Filing under an agent — the root `agent` field +`agent` is an optional field at the **manifest root** — a sibling of `id`, `name`, `version` and `frontend` — naming +the agent this extension is filed under. It describes the **extension**, so it never goes on a `menus[]` node. +**One agent per extension.** + +It is either a plain string, or an object when the author also wants to suggest where the agent's heading sits: + +```jsonc +// manifest.json (root) — the usual form +"agent": "Release Management", + +// …or, only when there is an order to carry +"agent": { "name": "Release Management", "order": 3 }, +``` + +Use the plain string unless the author gives an order. + +Ask the author to pick one (the starter list), or to type their own: + +1. DevOps +2. Compliance +3. Observability +4. Release Management +5. Something else (type a name) + +Write the chosen name **verbatim** — no slugging. Then, only if the author wants to position the heading, ask for a +heading order and use the object form; otherwise don't ask further. If the author declines to pick an agent, +**omit the field entirely**; never write `agent: ""`. + +How the portal places it: + +- The portal groups every extension sharing an agent name under **one** left-nav heading, titled with the agent name + and marked with a shared agent icon. `"DevOps"` merges into the platform's built-in **DevOps** heading rather than + creating a second one. +- **Every top-level `frontend.menus[]` node becomes a child of that heading.** So don't wrap your menus in a node + titled after the agent — that would render as a same-named sub-group under the heading. A top-level `item` sits + directly under the heading; a top-level `collapsible-section` becomes a sub-group under it. + With `agent` set, the heading decides top-level placement; the title-based placement in Pattern A above + applies to extensions without one. +- **Menu `order` vs agent `order`.** A menu node's `order` only positions it among the heading's children. The + heading's own position comes from the agent `order` — the **lowest** order any extension of that agent suggests. +- The deployment's `config.json` → `ai-menu.order`, keyed by the heading id (`ai-suite-agent-`, e.g. + `ai-suite-agent-release-management`), **overrides** the suggested agent order. + +```jsonc +// manifest.json : filed under the DevOps agent — the item lands directly under the DevOps heading +{ + "id": "duplo.examples.helloworld", "name": "Hello World", "version": "0.1.0", + "agent": "DevOps", + … + "frontend": { + "menus": [ + { "id": "duplo.examples.helloworld-hello-world", "title": "Hello World", "type": "item", + "matIcon": "star-outline", "relativeUrl": "extensions/helloworlds", "order": 40 } + ] + } +} +``` + ### Choosing `matIcon` — give the nav entry a real icon, not the template's placeholder The portal renders every menu node as ``, resolved against two SVG sprites it registers at startup (`app.component.ts` → `addSvgIconSet`): diff --git a/.claude/skills/duplo-extension-dev/templates/helloworld/manifest.json b/.claude/skills/duplo-extension-dev/templates/helloworld/manifest.json index 0f5b2fd..b672084 100644 --- a/.claude/skills/duplo-extension-dev/templates/helloworld/manifest.json +++ b/.claude/skills/duplo-extension-dev/templates/helloworld/manifest.json @@ -3,6 +3,7 @@ "id": "duplo.examples.helloworld", "name": "Hello World", "version": "0.1.0", + "agent": { "name": "DevOps", "order": 30 }, "sdkVersion": "REPLACED_BY_SKILL_FROM_sdk-version_ENDPOINT", "backend": { @@ -41,19 +42,12 @@ }, "menus": [ { - "id": "duplo.examples.helloworld-devops", - "title": "DevOps", - "type": "collapsible-section", - "children": [ - { - "id": "duplo.examples.helloworld-hello-world", - "title": "Hello World", - "type": "item", - "matIcon": "star-outline", - "relativeUrl": "extensions/helloworlds", - "order": 40 - } - ] + "id": "duplo.examples.helloworld-hello-world", + "title": "Hello World", + "type": "item", + "matIcon": "star-outline", + "relativeUrl": "extensions/helloworlds", + "order": 40 } ], "routes": [ From 7e1f03b09ae174416351ea4829cd3733be0587c5 Mon Sep 17 00:00:00 2001 From: Andy Boutte Date: Mon, 12 Oct 2026 23:02:01 -0500 Subject: [PATCH 2/3] docs(duplo-extension-dev): make manifest agent the console fallback The console now decides agent, heading order and position; the manifest agent is the backup and the way customer extensions get their own heading. Document validation and config.json override keys, and update the template and samples to a root agent with no DevOps wrapper. Co-Authored-By: Claude Opus 4.5 --- .claude/commands/duplo-extension.md | 7 ++-- .claude/skills/duplo-extension-dev/SKILL.md | 10 ++++-- .../reference/02-authoring-guide.md | 2 +- .../reference/08-parent-child-and-menus.md | 34 +++++++++++++++---- .../templates/helloworld/manifest.json | 2 +- samples/helloworld/manifest.json | 20 ++++------- samples/network-stack/manifest.json | 20 ++++------- samples/parent-child/manifest.json | 20 ++++------- samples/passthrough-configmap/manifest.json | 10 ++---- samples/worker-appstack/manifest.json | 10 ++---- samples/worker-compute/manifest.json | 10 ++---- 11 files changed, 68 insertions(+), 77 deletions(-) diff --git a/.claude/commands/duplo-extension.md b/.claude/commands/duplo-extension.md index 3b775a8..be6165c 100644 --- a/.claude/commands/duplo-extension.md +++ b/.claude/commands/duplo-extension.md @@ -158,8 +158,11 @@ backend + Angular remote + provisioning skill) — never ask the user to choose rather than plain CRUD (restart, console/exec, credential-minting shapes — see reference/18-access-control.md)? - **menu** placement (a top-level `collapsible-section` with `item` children renders nested correctly); - **agent** — which agent (DevOps / Compliance / Observability / Release Management / custom) this - extension files under, written as the manifest-root `agent` field (never on a menu node); optionally a heading - order (`{ "name": …, "order": N }`) only if the author wants one; omit the field if the author doesn't pick one; + extension files under, written as the manifest-root `agent` field (never on a menu node). It is the fallback: the + console's pack wins for Duplo-managed extensions (which still set their pack's name), and a customer name matching a + console agent joins it, otherwise it gets its own heading. Optionally an integer heading order + (`{ "name": …, "order": N }`) only if the author wants one; the name must be non-empty, at most 64 characters, no + control characters, or the upload is rejected (400); omit the field if the author doesn't pick one; - **Ask AI** is opt-in: scaffold it ONLY if the user's requirement explicitly asks for AI chat sessions on the resource — never offer it or add it unprompted (see skill reference 16-ask-ai); - for an external system: **auth** style (basic/bearer) and which API calls provisioning makes (a custom diff --git a/.claude/skills/duplo-extension-dev/SKILL.md b/.claude/skills/duplo-extension-dev/SKILL.md index 39917db..bc1a245 100644 --- a/.claude/skills/duplo-extension-dev/SKILL.md +++ b/.claude/skills/duplo-extension-dev/SKILL.md @@ -155,9 +155,13 @@ Settle before building (derive, else ask — **after** listening): item — derive the icon from the extension's purpose, per the Left-nav icon bullet above. - **Agent** — ask which agent this extension is filed under: DevOps, Compliance, Observability, Release Management, or a typed custom name. Write it verbatim as the **manifest-root** `agent` field (sibling of `id` / - `name` / `version`; never on a menu node; one agent per extension). Only if the author wants to position the - agent's left-nav heading, also ask for an order and write `"agent": { "name": "…", "order": N }`; otherwise the - plain string. Every top-level `menus[]` node then renders as a child of the agent's heading, so don't wrap menus + `name` / `version`; never on a menu node; one agent per extension). The manifest `agent` is the **fallback**: for + Duplo-managed extensions the console's pack decides the agent, heading position and order (and the extension still + sets `agent` to its pack's name as a backup); a customer extension's name that matches a console agent joins it, + otherwise it gets its own heading. Only if the author wants to position the heading, also ask for an integer order + and write `"agent": { "name": "…", "order": N }`; otherwise the plain string. The name must be non-empty, at most 64 + characters, with no control characters, and `order` an integer — invalid values are rejected at upload (HTTP 400). + Every top-level `menus[]` node then renders as a child of the agent's heading, so don't wrap menus in a node titled after the agent ([08 → Filing under an agent](reference/08-parent-child-and-menus.md#filing-under-an-agent--the-root-agent-field)). If the author declines to pick one, omit the field entirely — never write `agent: ""`. - **One or many resources?** — **flat** (independent — e.g. `GCP` → network/cluster/database) or **parent → child** diff --git a/.claude/skills/duplo-extension-dev/reference/02-authoring-guide.md b/.claude/skills/duplo-extension-dev/reference/02-authoring-guide.md index 216f3e6..bd20d9a 100644 --- a/.claude/skills/duplo-extension-dev/reference/02-authoring-guide.md +++ b/.claude/skills/duplo-extension-dev/reference/02-authoring-guide.md @@ -61,7 +61,7 @@ Ties the bundle together. For a typed extension (see the hello-world manifest): { "manifestVersion": "1.0", "id": "duplo.examples.helloworld", "name": "Hello World", "version": "0.1.0", - "agent": "DevOps", // optional, root-level — see 08-parent-child-and-menus → Filing under an agent + "agent": "DevOps", // optional, root-level fallback for console grouping (≤64 chars; order, if an object, an integer) — see 08-parent-child-and-menus → Filing under an agent "sdkVersion": "", "backend": { "assemblyDir": "duplo.examples.helloworld/0.1.0/backend", "entryAssembly": "Duplo.Extension.HelloWorld.dll" }, diff --git a/.claude/skills/duplo-extension-dev/reference/08-parent-child-and-menus.md b/.claude/skills/duplo-extension-dev/reference/08-parent-child-and-menus.md index 2495be9..b9a0378 100644 --- a/.claude/skills/duplo-extension-dev/reference/08-parent-child-and-menus.md +++ b/.claude/skills/duplo-extension-dev/reference/08-parent-child-and-menus.md @@ -171,7 +171,7 @@ resolver recovers `:parentId` by walking `ActivatedRoute.params`. the agent this extension is filed under. It describes the **extension**, so it never goes on a `menus[]` node. **One agent per extension.** -It is either a plain string, or an object when the author also wants to suggest where the agent's heading sits: +It is either a plain string, or an object when the author also wants to carry a heading order: ```jsonc // manifest.json (root) — the usual form @@ -183,6 +183,15 @@ It is either a plain string, or an object when the author also wants to suggest Use the plain string unless the author gives an order. +**The manifest `agent` is the fallback; the console decides when it can.** + +- **Duplo-managed extensions** — the console's pack membership decides the agent, the heading's position (the pack's + sort order, used as given) and the extension's position under the heading (its place in the pack). The console wins + over the manifest. Duplo-managed extensions **still set `"agent": ""`** as a backup, for + deployments without console data (air-gapped, or before the first sync). +- **Customer extensions** — set `agent` as a string or `{ "name": …, "order": }`. A name matching a console + agent (trimmed, case-insensitive) joins that agent's heading. Otherwise the extension gets its own heading. + Ask the author to pick one (the starter list), or to type their own: 1. DevOps @@ -195,20 +204,31 @@ Write the chosen name **verbatim** — no slugging. Then, only if the author wan heading order and use the object form; otherwise don't ask further. If the author declines to pick an agent, **omit the field entirely**; never write `agent: ""`. +**Validation.** An invalid `agent` is rejected at upload/deploy with HTTP 400: + +- the name is non-empty after trimming, at most 64 characters, with no control characters; +- `order`, when present, is an integer; +- the object has no keys other than `name` and `order`. + How the portal places it: -- The portal groups every extension sharing an agent name under **one** left-nav heading, titled with the agent name +- The portal groups every extension sharing an agent under **one** left-nav heading, titled with the agent name and marked with a shared agent icon. `"DevOps"` merges into the platform's built-in **DevOps** heading rather than - creating a second one. + creating a second one; under a built-in heading the built-in pages come first, then the agent's extensions. - **Every top-level `frontend.menus[]` node becomes a child of that heading.** So don't wrap your menus in a node titled after the agent — that would render as a same-named sub-group under the heading. A top-level `item` sits directly under the heading; a top-level `collapsible-section` becomes a sub-group under it. With `agent` set, the heading decides top-level placement; the title-based placement in Pattern A above applies to extensions without one. -- **Menu `order` vs agent `order`.** A menu node's `order` only positions it among the heading's children. The - heading's own position comes from the agent `order` — the **lowest** order any extension of that agent suggests. -- The deployment's `config.json` → `ai-menu.order`, keyed by the heading id (`ai-suite-agent-`, e.g. - `ai-suite-agent-release-management`), **overrides** the suggested agent order. +- **Menu `order` vs agent `order`.** A menu node's `order` only positions it among the heading's children. A manifest + agent's `order` is the heading's nav position, used **exactly as given**, in the same number space as the built-in + headings (HelpDesk 10, Projects 20, DevOps 30, … Settings 90). A missing order is `0`; ties go by title. If several + extensions of one manifest agent disagree on `order`, the lowest wins. Console agents take their position from the + console, not from the manifest. +- The deployment's `config.json` → `ai-menu.order`, keyed by the heading id, **overrides** the heading's position: + - `ai-suite-agent-` for console agents; + - `ai-suite-agent-` for manifest agents, where the slug keeps runs of `[a-z0-9]` joined by `-`, e.g. + `ai-suite-agent-release-management`. A name with no ASCII letters or digits gets a hashed id instead. ```jsonc // manifest.json : filed under the DevOps agent — the item lands directly under the DevOps heading diff --git a/.claude/skills/duplo-extension-dev/templates/helloworld/manifest.json b/.claude/skills/duplo-extension-dev/templates/helloworld/manifest.json index b672084..d7d9153 100644 --- a/.claude/skills/duplo-extension-dev/templates/helloworld/manifest.json +++ b/.claude/skills/duplo-extension-dev/templates/helloworld/manifest.json @@ -3,7 +3,7 @@ "id": "duplo.examples.helloworld", "name": "Hello World", "version": "0.1.0", - "agent": { "name": "DevOps", "order": 30 }, + "agent": "DevOps", "sdkVersion": "REPLACED_BY_SKILL_FROM_sdk-version_ENDPOINT", "backend": { diff --git a/samples/helloworld/manifest.json b/samples/helloworld/manifest.json index a84b2d8..7311dcc 100644 --- a/samples/helloworld/manifest.json +++ b/samples/helloworld/manifest.json @@ -3,6 +3,7 @@ "id": "duplo.examples.helloworld", "name": "Hello World", "version": "0.1.0", + "agent": "DevOps", "sdkVersion": "REPLACED_BY_SKILL_FROM_sdk-version_ENDPOINT", "backend": { @@ -41,19 +42,12 @@ }, "menus": [ { - "id": "duplo.examples.helloworld-devops", - "title": "DevOps", - "type": "collapsible-section", - "children": [ - { - "id": "duplo.examples.helloworld-hello-world", - "title": "Hello World", - "type": "item", - "matIcon": "waving_hand", - "relativeUrl": "extensions/helloworlds", - "order": 40 - } - ] + "id": "duplo.examples.helloworld-hello-world", + "title": "Hello World", + "type": "item", + "matIcon": "waving_hand", + "relativeUrl": "extensions/helloworlds", + "order": 40 } ], "routes": [ diff --git a/samples/network-stack/manifest.json b/samples/network-stack/manifest.json index f71ae84..29c2a63 100644 --- a/samples/network-stack/manifest.json +++ b/samples/network-stack/manifest.json @@ -3,6 +3,7 @@ "id": "duplo.examples.networkstack", "name": "Network Stack", "version": "0.1.0", + "agent": "DevOps", "sdkVersion": "REPLACED_BY_SKILL_FROM_sdk-version_ENDPOINT", "backend": { @@ -41,19 +42,12 @@ }, "menus": [ { - "id": "duplo.examples.networkstack-devops", - "title": "DevOps", - "type": "collapsible-section", - "children": [ - { - "id": "duplo.examples.networkstack-network-stack", - "title": "Network Stacks", - "type": "item", - "matIcon": "lan", - "relativeUrl": "extensions/network-stacks", - "order": 41 - } - ] + "id": "duplo.examples.networkstack-network-stack", + "title": "Network Stacks", + "type": "item", + "matIcon": "lan", + "relativeUrl": "extensions/network-stacks", + "order": 41 } ], "routes": [ diff --git a/samples/parent-child/manifest.json b/samples/parent-child/manifest.json index 313ccb7..019c4cd 100644 --- a/samples/parent-child/manifest.json +++ b/samples/parent-child/manifest.json @@ -3,6 +3,7 @@ "id": "duplo.examples.parentchild", "name": "Parent / Child", "version": "0.1.0", + "agent": "DevOps", "sdkVersion": "REPLACED_BY_SKILL_FROM_sdk-version_ENDPOINT", "backend": { @@ -60,19 +61,12 @@ }, "menus": [ { - "id": "duplo.examples.parentchild-devops", - "title": "DevOps", - "type": "collapsible-section", - "children": [ - { - "id": "duplo.examples.parentchild-parent-child", - "title": "Parent / Child", - "type": "item", - "matIcon": "account-tree", - "relativeUrl": "extensions/hello-parents", - "order": 44 - } - ] + "id": "duplo.examples.parentchild-parent-child", + "title": "Parent / Child", + "type": "item", + "matIcon": "account-tree", + "relativeUrl": "extensions/hello-parents", + "order": 44 } ], "routes": [ diff --git a/samples/passthrough-configmap/manifest.json b/samples/passthrough-configmap/manifest.json index 8d2c1a6..33ff02d 100644 --- a/samples/passthrough-configmap/manifest.json +++ b/samples/passthrough-configmap/manifest.json @@ -3,6 +3,7 @@ "id": "duplo.examples.passthrough-configmap", "name": "Passthrough ConfigMap", "version": "0.1.0", + "agent": "DevOps", "sdkVersion": "REPLACED_BY_SKILL_FROM_sdk-version_ENDPOINT", "backend": { @@ -33,14 +34,7 @@ "type": "module" }, "menus": [ - { - "id": "duplo.examples.passthrough-configmap-devops", - "title": "DevOps", - "type": "collapsible-section", - "children": [ - { "id": "duplo.examples.passthrough-configmap-cm", "title": "ConfigMaps", "type": "item", "matIcon": "settings", "relativeUrl": "extensions/configmapdemos", "order": 42 } - ] - } + { "id": "duplo.examples.passthrough-configmap-cm", "title": "ConfigMaps", "type": "item", "matIcon": "settings", "relativeUrl": "extensions/configmapdemos", "order": 42 } ], "routes": [ { "path": "extensions/configmapdemos", "resourceType": "ConfigMapDemo", "subType": "configmap-demo" } diff --git a/samples/worker-appstack/manifest.json b/samples/worker-appstack/manifest.json index be1977d..4c5ce9d 100644 --- a/samples/worker-appstack/manifest.json +++ b/samples/worker-appstack/manifest.json @@ -3,6 +3,7 @@ "id": "duplo.examples.worker-appstack", "name": "Worker AppStack", "version": "0.1.0", + "agent": "DevOps", "sdkVersion": "REPLACED_BY_SKILL_FROM_sdk-version_ENDPOINT", "backend": { @@ -33,14 +34,7 @@ "type": "module" }, "menus": [ - { - "id": "duplo.examples.worker-appstack-devops", - "title": "DevOps", - "type": "collapsible-section", - "children": [ - { "id": "duplo.examples.worker-appstack-as", "title": "App Stacks", "type": "item", "matIcon": "layers", "relativeUrl": "extensions/appstacks", "order": 43 } - ] - } + { "id": "duplo.examples.worker-appstack-as", "title": "App Stacks", "type": "item", "matIcon": "layers", "relativeUrl": "extensions/appstacks", "order": 43 } ], "routes": [ { "path": "extensions/appstacks", "resourceType": "AppStack", "subType": "app-stack" } diff --git a/samples/worker-compute/manifest.json b/samples/worker-compute/manifest.json index daf7c8e..440112d 100644 --- a/samples/worker-compute/manifest.json +++ b/samples/worker-compute/manifest.json @@ -3,6 +3,7 @@ "id": "duplo.examples.worker-compute", "name": "Worker Compute (Calculator)", "version": "0.1.0", + "agent": "DevOps", "sdkVersion": "REPLACED_BY_SKILL_FROM_sdk-version_ENDPOINT", "backend": { @@ -33,14 +34,7 @@ "type": "module" }, "menus": [ - { - "id": "duplo.examples.worker-compute-devops", - "title": "DevOps", - "type": "collapsible-section", - "children": [ - { "id": "duplo.examples.worker-compute-cw", "title": "Calc Worker", "type": "item", "matIcon": "calculate", "relativeUrl": "extensions/calcworkers", "order": 45 } - ] - } + { "id": "duplo.examples.worker-compute-cw", "title": "Calc Worker", "type": "item", "matIcon": "calculate", "relativeUrl": "extensions/calcworkers", "order": 45 } ], "routes": [ { "path": "extensions/calcworkers", "resourceType": "CalcWorker", "subType": "calc-worker" } From 26ec025241fec82925a57964251ea1df64ee2474 Mon Sep 17 00:00:00 2001 From: Andy Boutte Date: Mon, 12 Oct 2026 23:18:39 -0500 Subject: [PATCH 3/3] docs(duplo-extension-dev): correct agent rejection wording The backend validates the manifest agent only on new input: an upload is rejected with HTTP 400, a deploy through an extension resource fails its provisioning, and an extension already installed with a bad agent still loads, re-enables and rolls back. Say so, and limit the object form to customer extensions that set a heading order, matching the customer bullet. Note that an agent merged into a built-in heading is moved by the built-in's config key. Co-Authored-By: Claude Opus 4.5 --- .claude/commands/duplo-extension.md | 3 ++- .claude/skills/duplo-extension-dev/SKILL.md | 3 ++- .../reference/08-parent-child-and-menus.md | 12 +++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.claude/commands/duplo-extension.md b/.claude/commands/duplo-extension.md index be6165c..583e6a7 100644 --- a/.claude/commands/duplo-extension.md +++ b/.claude/commands/duplo-extension.md @@ -162,7 +162,8 @@ backend + Angular remote + provisioning skill) — never ask the user to choose console's pack wins for Duplo-managed extensions (which still set their pack's name), and a customer name matching a console agent joins it, otherwise it gets its own heading. Optionally an integer heading order (`{ "name": …, "order": N }`) only if the author wants one; the name must be non-empty, at most 64 characters, no - control characters, or the upload is rejected (400); omit the field if the author doesn't pick one; + control characters, or the upload is rejected (HTTP 400) and a deploy through a resource fails its provisioning; + omit the field if the author doesn't pick one; - **Ask AI** is opt-in: scaffold it ONLY if the user's requirement explicitly asks for AI chat sessions on the resource — never offer it or add it unprompted (see skill reference 16-ask-ai); - for an external system: **auth** style (basic/bearer) and which API calls provisioning makes (a custom diff --git a/.claude/skills/duplo-extension-dev/SKILL.md b/.claude/skills/duplo-extension-dev/SKILL.md index bc1a245..73b10ff 100644 --- a/.claude/skills/duplo-extension-dev/SKILL.md +++ b/.claude/skills/duplo-extension-dev/SKILL.md @@ -160,7 +160,8 @@ Settle before building (derive, else ask — **after** listening): sets `agent` to its pack's name as a backup); a customer extension's name that matches a console agent joins it, otherwise it gets its own heading. Only if the author wants to position the heading, also ask for an integer order and write `"agent": { "name": "…", "order": N }`; otherwise the plain string. The name must be non-empty, at most 64 - characters, with no control characters, and `order` an integer — invalid values are rejected at upload (HTTP 400). + characters, with no control characters, and `order` an integer — an invalid value is rejected at upload (HTTP 400) + and fails the provisioning when deployed through a resource. Every top-level `menus[]` node then renders as a child of the agent's heading, so don't wrap menus in a node titled after the agent ([08 → Filing under an agent](reference/08-parent-child-and-menus.md#filing-under-an-agent--the-root-agent-field)). If the author declines to pick one, omit the field entirely — never write `agent: ""`. diff --git a/.claude/skills/duplo-extension-dev/reference/08-parent-child-and-menus.md b/.claude/skills/duplo-extension-dev/reference/08-parent-child-and-menus.md index b9a0378..ba35642 100644 --- a/.claude/skills/duplo-extension-dev/reference/08-parent-child-and-menus.md +++ b/.claude/skills/duplo-extension-dev/reference/08-parent-child-and-menus.md @@ -171,13 +171,15 @@ resolver recovers `:parentId` by walking `ActivatedRoute.params`. the agent this extension is filed under. It describes the **extension**, so it never goes on a `menus[]` node. **One agent per extension.** -It is either a plain string, or an object when the author also wants to carry a heading order: +It is either a plain string or `{ "name": …, "order": }`. The object form is only for a customer +extension whose author wants to set its heading order; a Duplo-managed extension uses the plain string (its pack's +name), since the console decides its heading order: ```jsonc // manifest.json (root) — the usual form "agent": "Release Management", -// …or, only when there is an order to carry +// …or, for a customer extension whose author gives a heading order "agent": { "name": "Release Management", "order": 3 }, ``` @@ -204,7 +206,9 @@ Write the chosen name **verbatim** — no slugging. Then, only if the author wan heading order and use the object form; otherwise don't ask further. If the author declines to pick an agent, **omit the field entirely**; never write `agent: ""`. -**Validation.** An invalid `agent` is rejected at upload/deploy with HTTP 400: +**Validation.** An invalid `agent` is rejected when the extension is uploaded (HTTP 400), and fails the provisioning +when it is deployed through an extension resource. An extension already installed with an invalid `agent` still +loads, re-enables and rolls back; it is simply filed under no agent. The rules: - the name is non-empty after trimming, at most 64 characters, with no control characters; - `order`, when present, is an integer; @@ -229,6 +233,8 @@ How the portal places it: - `ai-suite-agent-` for console agents; - `ai-suite-agent-` for manifest agents, where the slug keeps runs of `[a-z0-9]` joined by `-`, e.g. `ai-suite-agent-release-management`. A name with no ASCII letters or digits gets a hashed id instead. + - An agent named like a built-in heading (`DevOps`) sits under that heading, so it moves by the built-in's id + (`ai-suite-infrastructure`), not by an `ai-suite-agent-…` key. ```jsonc // manifest.json : filed under the DevOps agent — the item lands directly under the DevOps heading