diff --git a/.claude/commands/duplo-extension.md b/.claude/commands/duplo-extension.md index 0abe226..583e6a7 100644 --- a/.claude/commands/duplo-extension.md +++ b/.claude/commands/duplo-extension.md @@ -157,6 +157,13 @@ 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). 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 (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 549533d..73b10ff 100644 --- a/.claude/skills/duplo-extension-dev/SKILL.md +++ b/.claude/skills/duplo-extension-dev/SKILL.md @@ -153,6 +153,18 @@ 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). 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 — 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: ""`. - **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..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,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 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 688d35c..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 @@ -166,6 +166,91 @@ 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 `{ "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, for a customer extension whose author gives a heading order +"agent": { "name": "Release Management", "order": 3 }, +``` + +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 +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: ""`. + +**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; +- the object has no keys other than `name` and `order`. + +How the portal places it: + +- 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; 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. 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. + - 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 +{ + "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..d7d9153 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": "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": "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": [ 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" }