Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/commands/duplo-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions .claude/skills/duplo-extension-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<from GET …/extensions/sdk-version>",
"backend": { "assemblyDir": "duplo.examples.helloworld/0.1.0/backend",
"entryAssembly": "Duplo.Extension.HelloWorld.dll" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": <integer> }`. 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": "<their pack's name>"`** 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": <integer> }`. 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-<pack uuid>` for console agents;
- `ai-suite-agent-<slug(name)>` 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 `<mat-icon svgIcon="{{ item.matIcon }}">`, resolved against two SVG sprites
it registers at startup (`app.component.ts` → `addSvgIconSet`):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": [
Expand Down
20 changes: 7 additions & 13 deletions samples/helloworld/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": [
Expand Down
20 changes: 7 additions & 13 deletions samples/network-stack/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": [
Expand Down
20 changes: 7 additions & 13 deletions samples/parent-child/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": [
Expand Down
10 changes: 2 additions & 8 deletions samples/passthrough-configmap/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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" }
Expand Down
10 changes: 2 additions & 8 deletions samples/worker-appstack/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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" }
Expand Down
10 changes: 2 additions & 8 deletions samples/worker-compute/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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" }
Expand Down
Loading