Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0e779b0
fix(scripts): derive backend.assemblyDir from id and version
bennett-elder Sep 25, 2026
e12ce21
fix(ui): listen on 8080 to match the UI image's own port
aboutte Sep 28, 2026
73d6dc3
fix(ui): discover the nginx resolver at run time instead of hard-codi…
duplo-darren Sep 29, 2026
33b5057
fix(scripts): derive skills[].folder from id and version
bennett-elder Sep 29, 2026
5f4ef8f
Merge pull request #11 from bennett-elder/fix/stale-assembly-dir
aboutte Sep 29, 2026
330e7c3
Merge pull request #17 from duplocloud/fix/ui-nginx-port-8080
aboutte Sep 29, 2026
2d68753
docs: soften email requirement messaging in run.sh and docs
aboutte Sep 30, 2026
5ce600c
Merge pull request #20 from duplocloud/docs/relax-email-messaging
aboutte Sep 30, 2026
bc5ef99
Merge pull request #19 from duplo-darren/fix/ui-nginx-resolver
aboutte Sep 30, 2026
0efc7d1
Feat/runtime detection to add podman support (and base for others) (#18)
duplo-darren Oct 1, 2026
d3e8dc6
ng22 migration skill: expose the kept NgModule, and drop a remoteEntr…
amaechiabuah Oct 2, 2026
0a888ee
feat(ci): fail when a duplocloud image tag is not published multi-arc…
duplo-darren Oct 5, 2026
4489764
chore: vendor ng-common-lib 0.4.0 (AI disclosure, ui-config)
shreyas-duplo Oct 6, 2026
7c2176b
feat(build): fail Agent-mode extensions that omit the AI-use disclosure
shreyas-duplo Oct 6, 2026
8fff308
feat(scaffold): Agent-mode forms ship the AI-use disclosure
shreyas-duplo Oct 6, 2026
42e23ba
docs(skill): Agent-mode forms ship the AI-use disclosure (DUPLOAI-3817)
shreyas-duplo Oct 6, 2026
14e9b7d
DUPLOAI-3817 fix: version-aware AI disclosure gate, wizard-stepper ex…
shreyas-duplo Oct 6, 2026
e021621
release 0.2.23: pin service images to 0.2.23
Oct 9, 2026
356b520
Merge main into release/0.2.23 (conflicts resolved, keeping both sides)
Oct 9, 2026
d42f708
release 0.2.23: pin 0.2.23-rc.13
Oct 12, 2026
80fb06c
release 0.2.23: pin 0.2.23-rc.15
Oct 13, 2026
e2fdac7
release 0.2.23: pin 0.2.23-rc.16
Oct 14, 2026
ef8bb52
Hotfix 0.2.23.1-test.1: pin DevKit images to 0.2.23.1-test.1
Oct 14, 2026
a91b1a4
release 0.2.23: pin 0.2.23-rc.17
Oct 14, 2026
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
10 changes: 5 additions & 5 deletions .claude/skills/duplo-extension-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ required/optional), a **Result** field table (name · type), the **provisioning
**detail view's shape** — the **lifecycle phases** this resource goes through (label + the status/result field that
drives each; [19-detail-page](reference/19-detail-page.md) → "Deriving phases") and the **Result tabs** (Overview at
minimum; name each further tab; [17-custom-result-views](reference/17-custom-result-views.md)) — and a short
**user-experience** walkthrough: left-nav placement, the add form, the list, the detail page, and what provisioning
**user-experience** walkthrough: left-nav placement, the add form (with the AI-use disclosure in Agent mode), the list, the detail page, and what provisioning
does end-to-end. Do this per resource for multi-resource extensions.

**Where the answers come from / how to ask:**
Expand Down Expand Up @@ -209,7 +209,7 @@ Adapt inside `extensions/<name>/` — rename `HelloWorld`→`<Name>` consistentl
`extensions/<feature>-<resource>` (== manifest `restSegment`; a bare leaf 404s).** Rename every FE `Hello`/`hw`
identifier (files, classes, selectors, routes, and a **unique** `federation.config.js` `name`) per the rename table
in [02](reference/02-authoring-guide.md#frontend). List = `searchable-datatable` with the filter + workspace-refresh
wiring; Add/Edit = `panel-form-accordion` + `form-field`; Detail = the **Template-G shell the scaffold already
wiring; Add/Edit = `panel-form-accordion` + `form-field`; **Agent mode: every Add/Edit form ends with `<app-ai-disclosure />`** (ISO 42001; the scaffold carries it, [14](reference/14-forms-and-wizards.md#ai-use-disclosure-agent-mode)); Detail = the **Template-G shell the scaffold already
carries** — keep `shared/` as copied and **rewrite the two things that are the resource's own**: the `phases()`
computed (your lifecycle ladder, per mode) and the Result strip's Overview tiles + any further tabs
([19](reference/19-detail-page.md), [17](reference/17-custom-result-views.md)). The federation `shared` block stays a
Expand All @@ -233,11 +233,11 @@ status", the "Needs your input" phase) exists **only in Agent mode** ([19](refer
3. add an `IDuploExtension` whose `Configure` calls `builder.Services.AddHostedService<<Name>Worker>();` — hot-load
starts the worker directly; `Configure` is what the boot-time replay uses ([10](reference/10-sdk-api.md) Gotchas).
4. **delete** the `skills/` dir and ship **no** `skills`/`skillMappings` in the manifest.
5. **FE:** remove the ticket UI, drop the "Needs your input" phase, compute the Worker ladder from `workerState`
5. **FE:** remove the ticket UI and the `<app-ai-disclosure />` (Agent-only), drop the "Needs your input" phase, compute the Worker ladder from `workerState`
(copy `samples/worker-compute`'s `phases()`), remove `track()`/`ticketName()` and the list-row "Track Provisioning"
item, label the add button **"Create"** (not "Provision").
- **Agent**: keep `skills/provision-*/SKILL.md` (+ `provision.sh`, write to the OWN route) and the manifest
`skills` + `skillMappings`; keep the ticket UI. **Deprovision seam (if it creates real infra):** the platform reuses the
`skills` + `skillMappings`; keep the ticket UI and the `<app-ai-disclosure />` on every Add/Edit form (`scripts/build-extension.sh` fails without it on ng-common-lib >= 0.4.0). **Deprovision seam (if it creates real infra):** the platform reuses the
SAME ticket and sends the generic teardown message — there is **no separate deprovision skill mapping**, so the
provision `SKILL.md` needs a **Deprovision** section + an idempotent `deprovision.sh`, or a second skill in the same
`skillNames` ([11](reference/11-deprovisioning.md)). Once your skill POSTs `DeProvisioned`, the platform hard-deletes
Expand All @@ -249,7 +249,7 @@ status", the "Needs your input" phase) exists **only in Agent mode** ([19](refer
## Phases 3–5 shortcut (clone-and-own repo)

If you're in a cloned dev-kit repo, **`./scripts/build-extension.sh extensions/<name>`** (or `./scripts/build-all.sh`
for every extension) does Phases 3–5 in one step (naming gate, fetch + pin the SDK, `dotnet publish`,
for every extension) does Phases 3–5 in one step (naming gate, AI-use disclosure check, fetch + pin the SDK, `dotnet publish`,
`npm ci && npm run build`, trim the bundle to your extension's own assemblies, assemble
`extensions/<name>/dist/extension.zip`). The manual steps below are the equivalent — use them in-platform or when
there's no script. After it succeeds, skip to Phase 6.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,11 @@ Ties the bundle together. For a typed extension (see the hello-world manifest):
}
```
Critical: `archetype` must be `"typed"` so the loader takes the DLL path; the five `*Type` names must be the
fully-qualified type names in your DLL; `backend.assemblyDir` is `<id>/<version>/backend`. **Bump `version` on
every backend code change** ([06](06-registration.md#reloading-changed-code--bump-manifestversion)).
fully-qualified type names in your DLL; `backend.assemblyDir` is `<id>/<version>/backend` and each
`skills[].folder` is `<id>/<version>/skills/<name>`. **Bump `version` on every backend code change**
([06](06-registration.md#reloading-changed-code--bump-manifestversion)) — you don't have to bump either path
with it, since `build-extension.sh` derives both from `id` and `version` when it writes the bundle manifest.
It prints a `NOTE:` if a checked-in value disagreed.

### Naming

Expand Down Expand Up @@ -256,12 +259,12 @@ a plain `npm install` just works from a fresh clone. Two schemes coexist:

- **The samples** share one copy at repo-root `packages/`, since they never leave this repo:
```json
"@duplocloud-internal/ng-common-lib": "file:../../../packages/duplocloud-internal-ng-common-lib-0.2.0.tgz",
"@duplocloud-internal/ng-common-lib": "file:../../../packages/duplocloud-internal-ng-common-lib-0.4.0.tgz",
```
- **The skill template** (`templates/helloworld/frontend/`) keeps its **own** copy under `vendor/`, because it
gets copied out to `extensions/<name>/` or a provisioning workdir and must stay self-contained:
```json
"@duplocloud-internal/ng-common-lib": "file:vendor/duplocloud-internal-ng-common-lib-0.2.0.tgz",
"@duplocloud-internal/ng-common-lib": "file:vendor/duplocloud-internal-ng-common-lib-0.4.0.tgz",
```

Scaffolding from the template carries the tarball and the specifier with it — keep both when you copy.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ Import `SharedFormsModule` and use:
The single-page create form is the `panel-form-accordion` 3-column layout — see
[02-authoring-guide](02-authoring-guide.md) and `samples/helloworld`.

## AI-use disclosure (Agent mode)

DuploCloud is ISO 42001 certified: a form whose submit hands work to an AI agent (Agent mode — the resource maps a
skill in `manifest.skillMappings`) must say so at the point of use. Every Agent-mode Add/Edit form renders the
library's `<app-ai-disclosure />` (import `AiDisclosureComponent` from `@duplocloud-internal/ng-common-lib`):

- **Single-page form** — last element inside `.form-container`, under the Cancel/Provision row (see `samples/helloworld`).
- **Wizard** — pass `[aiDisclosure]="true"` to the copied `wizard-stepper`; it renders under the footer on every step.
- **Modal form** — inside `.modal-footer`, above the buttons.

The text and link come from the deployment's branding config (brand name included) — never hardcode them. Worker,
Passthrough and No-provision forms do **not** carry it. `scripts/build-extension.sh` fails an Agent-mode extension
with no `app-ai-disclosure` under `frontend/src`.

## Multi-step wizards — there is NO library stepper

The library ships **no stepper/wizard component**, and the host's own multi-step forms (AppService, etc.) hand-roll
Expand Down Expand Up @@ -121,6 +135,8 @@ prototype:
omits a step defeats confirm-before-provision.
- **Contextual error fallback**: `extractErrorMessage(err) || 'Could not create the <resource>.'` — name
the action that failed.
- **AI-use disclosure** — Agent-mode wizards pass `[aiDisclosure]="true"` to the stepper
([above](#ai-use-disclosure-agent-mode)).

## Dynamic fields from a remote schema

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Deployed phase does exactly this.
### Ladders by provisioning mode — and the ticket UI that goes with them

Only **Agent** mode has a provisioning ticket. So only Agent-mode views render the header **Ask agent**
button, the rail's **Track status** (`[trackable]="true"`) and a **"Needs your input"** phase; they also
button, the rail's **Track status** (`[trackable]="true"`) and a **"Needs your input"** phase; their Add/Edit forms carry the `<app-ai-disclosure />` ([14](14-forms-and-wizards.md#ai-use-disclosure-agent-mode)); they also
keep `track()`/`ticketName()` and the list-row "Track Provisioning" item. Worker, Passthrough and
No-provision views omit all of those, use the ladders below, and label the add button **"Create"**
(not "Provision"). Opt-in Ask AI sessions ([16-ask-ai](16-ask-ai.md)) are user-created and work in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Everything the platform UI library exports, what each piece is for, how to impor
inside an extension remote. Check here before hand-rolling a table, form control, viewer or modal — the
platform almost certainly ships it already, and using it is what makes your pages match the suite.

Written against **0.3.0**. The source of truth is `portal/common-lib/public-api.ts` in duplo-ui; exact
Written against **0.4.0**. The source of truth is `portal/common-lib/public-api.ts` in duplo-ui; exact
signatures are in the installed package — see [Checking an exact API](#checking-an-exact-api).

## Rules
Expand Down Expand Up @@ -32,6 +32,7 @@ signatures are in the installed package — see [Checking an exact API](#checkin
| Row actions beyond Add / Edit | `searchable-datatable` `[extraActions]` | [table slots](#table-slots-filters-actions-alert) |
| A short child list inside a detail tab | `app-expandable-list` | [expandable list](#expandable-list) |
| A form field, validator or error summary | `SharedFormsModule` | [14-forms-and-wizards](14-forms-and-wizards.md) |
| The AI-use disclosure on an Agent-mode form | `app-ai-disclosure` | [14-forms-and-wizards](14-forms-and-wizards.md#ai-use-disclosure-agent-mode) |
| The real reason an API call failed | `extractErrorMessage`, `[form-group-errors]` | [15-error-handling](15-error-handling.md) |
| A delete / deprovision confirmation | `DeleteConfirmationModalService.openGeneric` | [confirm dialogs](#confirm-dialogs) |
| A yes/no confirmation | `ConfirmationModalService.openWithMessage` | [confirm dialogs](#confirm-dialogs) |
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@angular/material": "^22.1.0",
"@angular/platform-browser": "^22.1.0",
"@angular/router": "^22.1.0",
"@duplocloud-internal/ng-common-lib": "file:vendor/duplocloud-internal-ng-common-lib-0.3.0.tgz",
"@duplocloud-internal/ng-common-lib": "file:vendor/duplocloud-internal-ng-common-lib-0.4.0.tgz",
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
"@ng-select/ng-select": "^23.5.1",
"@ngbracket/ngx-layout": "^22.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, OnInit, inject, signal, viewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormGroupErrorsComponent, SharedFormsModule } from '@duplocloud-internal/ng-common-lib';
import { AiDisclosureComponent, FormGroupErrorsComponent, SharedFormsModule } from '@duplocloud-internal/ng-common-lib';
import { HelloService } from '../hello.service';

// Add/Edit form in the platform's 3-column `panel-form-accordion` layout: title+description (left), inputs
Expand All @@ -16,7 +16,7 @@ import { HelloService } from '../hello.service';
// SharedFormsModule re-exports FormsModule plus form-field and every validation directive used here.
@Component({
selector: 'hw-add',
imports: [SharedFormsModule],
imports: [SharedFormsModule, AiDisclosureComponent],
styles: [`
:host { display: block; }
.panel-form-accordion { background: #fff; padding: 1.25rem 0 1rem 1.5rem; }
Expand Down Expand Up @@ -73,6 +73,8 @@ import { HelloService } from '../hello.service';
{{ isEdit ? 'Save' : 'Provision' }}
</button>
</div>
<!-- Agent mode only (ISO 42001): remove with the ticket UI when converting to Worker/Passthrough. -->
<app-ai-disclosure class="mt-1" />
</div>
</form>
}
Expand Down
Binary file not shown.
Binary file not shown.
23 changes: 20 additions & 3 deletions .claude/skills/duplo-extension-ng22-migration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,23 @@ both of which are barred here.

## 2. Add `federation.config.js`

Copy the reference verbatim, then change exactly one line:
Copy the reference verbatim, then change exactly two things — the container name, and the exposed
module:

```bash
cp <devkit>/samples/helloworld/frontend/federation.config.js "$FE/federation.config.js"
perl -pi -e "s/duploExtensionHelloworld/$REMOTE_NAME/g" "$FE/federation.config.js"
perl -pi -e "s{'\./Extension': '\./src/app/extension\.routes\.ts'}{'./Extension': './src/app/extension.module.ts'}" \
"$FE/federation.config.js"
```

The second edit matters because helloworld has since been modernized to standalone components, so its
config exposes `./src/app/extension.routes.ts`. This migration keeps the extension's
`src/app/extension.module.ts` ([step 5](#5-rewrite-the-tsconfig-pair-and-srcmaints)), and an extension
coming off Angular 15 has no `extension.routes.ts` — the build dies at config load with
`FsPath: …/src/app/extension.routes.ts does not exist`. Point `exposes` at the NgModule; the modernization
pass that later adds `extension.routes.ts` moves it back.

Read the file's comment block — it documents the aliasing hazard, the subset rule and the skip list,
all of which are covered in [Traps](#traps) below.

Expand Down Expand Up @@ -404,12 +414,19 @@ its request resolves.

## 7. Flip the manifest to `remoteEntry.json`

Native Federation publishes a JSON descriptor, not a JS container. One character changes:
Native Federation publishes a JSON descriptor, not a JS container. One line changes:

```bash
perl -pi -e 's{/remoteEntry\.js"}{/remoteEntry.json"}' "$EXT/manifest.json"
perl -pi -e 's{/remoteEntry\.js(\?[^"]*)?"}{/remoteEntry.json"}' "$EXT/manifest.json"
grep -n '"remoteEntry"' "$EXT/manifest.json" # must now end in /remoteEntry.json"
```

The optional group drops a cache-busting query such as `remoteEntry.js?v=0.6.118`, which some Webpack-era
manifests carry. Drop it rather than carrying it over: the [gate](reference/verification.md) requires
`remoteEntry` to end in `/remoteEntry.json`, and the migrated samples carry no query. A substitution
written for the bare `remoteEntry.js"` form matches nothing on such a manifest and leaves it on the Webpack
entry, so check the `grep` output rather than assuming the edit landed.

Keep `remoteName`, `exposedModule` and `type: "module"` exactly as they were.

Use a **targeted substitution, not a `jq` round-trip**. `jq`'s pretty-printer reflows the whole
Expand Down
Loading
Loading