From f59ed89023705ac8cd00907550b38e9055ce9654 Mon Sep 17 00:00:00 2001
From: Artem Korotkikh
<105195369+artkorotkikh-dfinity@users.noreply.github.com>
Date: Wed, 24 Jun 2026 10:25:32 +0200
Subject: [PATCH 1/2] feat: document app icon (__META_BASE_URL +
__META_ICON_PATH) for the console
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The skill covered name/label/Open metadata but not the app icon. Add the
icon pair, with behavior verified against the console source (the
control-panel frontend that renders these), not guessed:
- `__META_BASE_URL` — app 'Open' link and icon base; must be an absolute
https URL (the console rejects non-https / data: / javascript:).
- `__META_ICON_PATH` — icon path resolved against the base
(new URL(iconPath, baseUrl)); rendered as
.
- Both are read ONLY from the main canister; the icon has no fallback, so
a missing/invalid base or a path on a side canister means no icon.
- The variable is `__META_ICON_PATH`, not `__META_ICON_LINK`/`_ICON`/`_LOGO`
(a common wrong guess) — Pitfall 8 calls this out.
- Env values are length-capped (~128 chars), so a data: URI does not fit;
it is a served path by design. Pitfall 9 + the adversarial eval cover it.
- Note the deploy-twice ordering: the frontend canister id is only known
after the first deploy, so set the base URL then re-deploy.
Evals: 'Give the app an icon' output eval, 'icon not showing (data URI /
wrong canister)' adversarial, and a should_trigger icon query.
Co-Authored-By: Claude Fable 5
---
evaluations/deploy-to-cloud-engine.json | 26 +++++++++++++++++++++++--
skills/deploy-to-cloud-engine/SKILL.md | 24 +++++++++++++++++++----
2 files changed, 44 insertions(+), 6 deletions(-)
diff --git a/evaluations/deploy-to-cloud-engine.json b/evaluations/deploy-to-cloud-engine.json
index 530cad4..95c379c 100644
--- a/evaluations/deploy-to-cloud-engine.json
+++ b/evaluations/deploy-to-cloud-engine.json
@@ -1,6 +1,6 @@
{
"skill": "deploy-to-cloud-engine",
- "description": "Evaluation cases for the deploy-to-cloud-engine skill. Tests whether agents drive the correct cloud-engine deploy flow (link the CLI identity against the console origin, target the engine's subnet with icp deploy), use the default console origin https://opencloud.org unless the user names another, ask for the subnet id instead of guessing, set the `__META_*` environment variables so the console shows a named app with labelled canisters, and avoid the documented pitfalls.",
+ "description": "Evaluation cases for the deploy-to-cloud-engine skill. Tests whether agents drive the correct cloud-engine deploy flow (link the CLI identity against the console origin, target the engine's subnet with icp deploy), use the default console origin https://opencloud.org unless the user names another, ask for the subnet id instead of guessing, set the `__META_*` environment variables so the console shows a named app with labelled canisters and an app icon (`__META_BASE_URL` + `__META_ICON_PATH` on the main canister), and avoid the documented pitfalls.",
"output_evals": [
{
@@ -105,6 +105,27 @@
"Tells the user to set it and re-deploy by explicitly naming the `icp deploy` command (not just saying 'redeploy')",
"Does NOT attribute the missing button to the subnet, the identity, or the --auth origin"
]
+ },
+ {
+ "name": "Give the app an icon in the console",
+ "prompt": "My app is already deployed to my cloud engine and shows up named in the console. How do I give it an icon? My frontend already serves favicon.svg.",
+ "expected_behaviors": [
+ "Uses `__META_ICON_PATH` for the icon path (e.g. `/favicon.svg`), resolved against `__META_BASE_URL`",
+ "Sets `__META_BASE_URL` to an absolute https URL (the frontend canister URL like https://.icp.net or a custom domain)",
+ "Sets both on the main canister (the one marked `__META_MAIN_CANISTER: \"true\"`)",
+ "Re-runs `icp deploy` to apply the settings",
+ "Does NOT invent `__META_ICON`, `__META_LOGO`, or `__META_ICON_LINK`, and does NOT put an inline data: URI in the icon variable"
+ ]
+ },
+ {
+ "name": "Adversarial: icon not showing (data URI / wrong canister)",
+ "prompt": "I set `__META_ICON_PATH` to a base64 data: URI of my logo on my backend canister and redeployed, but the console still shows no icon. Why?",
+ "expected_behaviors": [
+ "Identifies that the icon must be set on the MAIN canister (marked `__META_MAIN_CANISTER: \"true\"`), not the backend",
+ "Identifies that `__META_ICON_PATH` is a path resolved against `__META_BASE_URL`, not an inline image — a data: URI does not belong there (and would exceed the engine env value length cap)",
+ "Tells the user to set `__META_BASE_URL` to an absolute https URL and `__META_ICON_PATH` to a served path (e.g. /favicon.svg), both on the main canister, then re-deploy",
+ "Does NOT attribute the missing icon to the subnet, the identity, or the --auth origin"
+ ]
}
],
@@ -119,7 +140,8 @@
"Ship this project to my cloud engine",
"My agent should deploy straight to my engine",
"Make my deployed app show up with a name in the cloud engine console",
- "Label my backend and frontend canisters in the engine console instead of bare principal ids"
+ "Label my backend and frontend canisters in the engine console instead of bare principal ids",
+ "Give my deployed app an icon in the OpenCloud engine console"
],
"should_not_trigger": [
"Package my app as a .icp archive so others can install it on their engines from the marketplace",
diff --git a/skills/deploy-to-cloud-engine/SKILL.md b/skills/deploy-to-cloud-engine/SKILL.md
index 892336d..5bcc18c 100644
--- a/skills/deploy-to-cloud-engine/SKILL.md
+++ b/skills/deploy-to-cloud-engine/SKILL.md
@@ -1,6 +1,6 @@
---
name: deploy-to-cloud-engine
-description: "Deploys an already-built Internet Computer project to a user's own cloud engine (an OpenCloud / control-panel engine, administered from a web console). Covers verifying the icp CLI, linking the user's console identity to the CLI with `icp identity link web`, defaulting the console origin to https://opencloud.org (overridable when the user signs in to a different console), obtaining the engine's subnet id (asking the user when it is unknown), running `icp deploy` against that subnet, and tagging the canisters with `__META_*` environment variables so the engine console shows a named app with labelled backend/frontend canisters. Use when a developer wants to ship an app to their cloud engine, mentions a cloud engine, OpenCloud, an engine subnet id, linking the icp CLI to an engine console, or giving a deployed app a name in the console. Do NOT use for a general mainnet deploy with no specific engine or subnet (use the icp-cli skill) or for writing canister code."
+description: "Deploys an already-built Internet Computer project to a user's own cloud engine (an OpenCloud / control-panel engine, administered from a web console). Covers verifying the icp CLI, linking the user's console identity to the CLI with `icp identity link web`, defaulting the console origin to https://opencloud.org (overridable when the user signs in to a different console), obtaining the engine's subnet id (asking the user when it is unknown), running `icp deploy` against that subnet, and tagging the canisters with `__META_*` environment variables so the engine console shows a named app with labelled backend/frontend canisters and an app icon. Use when a developer wants to ship an app to their cloud engine, mentions a cloud engine, OpenCloud, an engine subnet id, linking the icp CLI to an engine console, or giving a deployed app a name or icon in the console. Do NOT use for a general mainnet deploy with no specific engine or subnet (use the icp-cli skill) or for writing canister code."
license: Apache-2.0
compatibility: "icp-cli >= 0.3.0 (commands verified against 0.3.0), a cloud engine console account, a browser for the Internet Identity sign-in"
metadata:
@@ -77,13 +77,17 @@ icp identity default # prints the active identity name
icp identity principal # prints the principal the deploy will sign as
```
-## Step 2 — Name the app in the console (recommended)
+## Step 2 — Name the app (and give it an icon) in the console (recommended)
-By default, CLI-deployed canisters appear on the engine console's Applications page as bare rows labelled only by their principal id. Three **canister environment variables** make the console group them into a single named application with readable per-canister labels and an "Open" button. Set them once in your project config:
+By default, CLI-deployed canisters appear on the engine console's Applications page as bare rows labelled only by their principal id. A set of **canister environment variables** makes the console group them into a single named application with readable per-canister labels, an "Open" button, and an icon. Set them once in your project config:
- `__META_PROJECT` — the application name. Canisters that share the **same** value are grouped into one named app, so set an identical value on every canister of the app.
- `__META_NAME` — the per-canister display label (e.g. `Backend`, `Frontend`).
- `__META_MAIN_CANISTER` — the literal string `"true"` on the single entry-point canister (usually the frontend/asset canister). That canister gets the "Open" button and the app's public URL.
+- `__META_BASE_URL` — the app's public base, as an **absolute `https://` URL** (e.g. the frontend canister's URL `https://.icp.net`, or a custom domain). It is the app's "Open" link **and** the base that the icon path resolves against. Set it on the **main** canister.
+- `__META_ICON_PATH` — the path to the app icon, resolved against `__META_BASE_URL` to form the icon the console renders (e.g. `/favicon.svg` → `https:///favicon.svg`). Set it on the **main** canister, alongside `__META_BASE_URL`.
+
+The icon and "Open" link are read **only from the main canister** (the one marked `__META_MAIN_CANISTER: "true"`) — `__META_BASE_URL` / `__META_ICON_PATH` on any other canister are ignored.
Set them under each canister's `settings.environment_variables` — this is valid alongside a recipe. With per-canister `canister.yaml` files:
@@ -102,6 +106,8 @@ settings:
__META_PROJECT: "My App"
__META_NAME: "Frontend"
__META_MAIN_CANISTER: "true"
+ __META_BASE_URL: "https://.icp.net"
+ __META_ICON_PATH: "/favicon.svg"
```
```yaml
@@ -129,6 +135,8 @@ canisters:
__META_PROJECT: "My App"
__META_NAME: "Frontend"
__META_MAIN_CANISTER: "true"
+ __META_BASE_URL: "https://.icp.net"
+ __META_ICON_PATH: "/favicon.svg"
- name: backend
recipe: # … as in the canister.yaml example above
settings:
@@ -142,6 +150,12 @@ Notes:
- All values are strings; `__META_MAIN_CANISTER` must be the exact string `"true"`.
- They are applied during `icp deploy` (the "Setting environment variables" step). After deploy, confirm with `icp canister settings show -e ic`.
+Icon specifics (the console builds the icon as `__META_BASE_URL` + `__META_ICON_PATH`):
+- **Both** must be present and **on the main canister** for an icon to appear — there is no fallback. `__META_ICON_PATH` alone does nothing.
+- `__META_BASE_URL` must parse as an absolute **`https://`** URL. A bare host, an `http://` URL, or a `data:` / `javascript:` value is rejected, and then *neither* the icon *nor* the "Open" link renders. (The console validates the scheme before using it.)
+- `__META_ICON_PATH` is a **path to an asset your frontend actually serves** (e.g. `/favicon.svg`), not an inline image. The resolved URL is rendered as an `
` `src`, so it must return an image. Do **not** put a `data:` URI here: engine env values are length-capped (≤128 chars observed), so it would not fit, and the field is a path by design.
+- The frontend canister's id is only known **after** the first deploy. The usual flow is: deploy once, read the frontend canister id from the output, set `__META_BASE_URL` to `https://.icp.net` (and `__META_ICON_PATH`), then re-deploy to apply. If you control a custom domain for the app, you can set it up front instead.
+
## Step 3 — Deploy to the engine's subnet
From the project root:
@@ -159,7 +173,7 @@ icp deploy -e ic --subnet
- The `icp deploy` output reports the deployed canister ids.
- The canisters appear on the engine's **Applications** page in the console; each canister's detail view offers an "Open in browser" link.
-- If you set the metadata in Step 2, the canisters are grouped under your `__META_PROJECT` name with their `__META_NAME` labels, and the main canister shows an "Open" button — instead of bare principal rows.
+- If you set the metadata in Step 2, the canisters are grouped under your `__META_PROJECT` name with their `__META_NAME` labels, and the main canister shows an "Open" button — instead of bare principal rows. With `__META_BASE_URL` + `__META_ICON_PATH` set, the app also shows its icon (allow for a short console cache delay).
- A frontend (asset) canister is served at `https://.icp.net`.
Report the deployed canister ids (and the frontend URL, if any) back to the user.
@@ -173,6 +187,8 @@ Report the deployed canister ids (and the frontend URL, if any) back to the user
5. **Using `dfx`.** This ecosystem uses `icp`, never `dfx`. The correct sequence is `icp identity link web --auth ` (Step 1), then `icp deploy -e ic --subnet ` (Step 3). See the `icp-cli` skill.
6. **Skipping the app metadata.** Without `__META_PROJECT` (Step 2), the canisters still deploy and work but render as bare, unnamed principal rows in the console. Setting `__META_*` is what produces a named app with labelled canisters and an "Open" button.
7. **Wrong `__META_MAIN_CANISTER` value.** It is matched as the exact string `"true"`. A boolean, `"True"`, or marking more than one canister means no (or the wrong) "Open" button. Mark exactly one entry-point canister.
+8. **Inventing an icon variable.** The icon variable is `__META_ICON_PATH` (a path resolved against `__META_BASE_URL`). Do not guess `__META_ICON`, `__META_LOGO`, or `__META_ICON_LINK` — they are ignored, so the icon silently never appears.
+9. **Icon set on the wrong canister, or without a base URL.** The icon is read only from the **main** canister and needs **both** `__META_BASE_URL` (a valid absolute `https://` URL) and `__META_ICON_PATH`. Setting the icon path on a side canister, omitting the base URL, or giving a non-https / `data:` base means no icon renders (and a bad base URL also drops the "Open" link).
## Related Skills
From 56cea2d0cd9b2681a981072a66fedbb8ded29464 Mon Sep 17 00:00:00 2001
From: Artem Korotkikh
<105195369+artkorotkikh-dfinity@users.noreply.github.com>
Date: Wed, 24 Jun 2026 11:48:23 +0200
Subject: [PATCH 2/2] docs(deploy-to-cloud-engine): clarify main-canister vs
base-url and Open-link fallback
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Address Copilot review on Step 2: separate which canister is the metadata
source (__META_MAIN_CANISTER) from the Open-button URL value (__META_BASE_URL).
Also correct the claim that an invalid/missing base URL drops the "Open" link —
per control-panel's grouping logic it falls back to the main canister's gateway
URL, so only the icon is lost.
Co-Authored-By: Claude Opus 4.8
---
skills/deploy-to-cloud-engine/SKILL.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/skills/deploy-to-cloud-engine/SKILL.md b/skills/deploy-to-cloud-engine/SKILL.md
index 5bcc18c..55de3d1 100644
--- a/skills/deploy-to-cloud-engine/SKILL.md
+++ b/skills/deploy-to-cloud-engine/SKILL.md
@@ -83,8 +83,8 @@ By default, CLI-deployed canisters appear on the engine console's Applications p
- `__META_PROJECT` — the application name. Canisters that share the **same** value are grouped into one named app, so set an identical value on every canister of the app.
- `__META_NAME` — the per-canister display label (e.g. `Backend`, `Frontend`).
-- `__META_MAIN_CANISTER` — the literal string `"true"` on the single entry-point canister (usually the frontend/asset canister). That canister gets the "Open" button and the app's public URL.
-- `__META_BASE_URL` — the app's public base, as an **absolute `https://` URL** (e.g. the frontend canister's URL `https://.icp.net`, or a custom domain). It is the app's "Open" link **and** the base that the icon path resolves against. Set it on the **main** canister.
+- `__META_MAIN_CANISTER` — the literal string `"true"` on exactly one canister (the entry point, usually the frontend/asset canister). This marks the app's **main canister**: the console reads `__META_BASE_URL` and `__META_ICON_PATH` only from it, and the "Open" button targets it.
+- `__META_BASE_URL` — an **absolute `https://` URL**, set on the main canister (e.g. the frontend canister's URL `https://.icp.net`, or a custom domain). When present and valid, it is the URL the "Open" button opens; when absent or not `https`, the "Open" button falls back to the main canister's gateway URL. It is also the base that `__META_ICON_PATH` resolves against.
- `__META_ICON_PATH` — the path to the app icon, resolved against `__META_BASE_URL` to form the icon the console renders (e.g. `/favicon.svg` → `https:///favicon.svg`). Set it on the **main** canister, alongside `__META_BASE_URL`.
The icon and "Open" link are read **only from the main canister** (the one marked `__META_MAIN_CANISTER: "true"`) — `__META_BASE_URL` / `__META_ICON_PATH` on any other canister are ignored.
@@ -152,7 +152,7 @@ Notes:
Icon specifics (the console builds the icon as `__META_BASE_URL` + `__META_ICON_PATH`):
- **Both** must be present and **on the main canister** for an icon to appear — there is no fallback. `__META_ICON_PATH` alone does nothing.
-- `__META_BASE_URL` must parse as an absolute **`https://`** URL. A bare host, an `http://` URL, or a `data:` / `javascript:` value is rejected, and then *neither* the icon *nor* the "Open" link renders. (The console validates the scheme before using it.)
+- `__META_BASE_URL` must parse as an absolute **`https://`** URL. A bare host, an `http://` URL, or a `data:` / `javascript:` value is rejected: the icon then does not render, and the "Open" button falls back to the main canister's gateway URL (it does not disappear). (The console validates the scheme before using it.)
- `__META_ICON_PATH` is a **path to an asset your frontend actually serves** (e.g. `/favicon.svg`), not an inline image. The resolved URL is rendered as an `
` `src`, so it must return an image. Do **not** put a `data:` URI here: engine env values are length-capped (≤128 chars observed), so it would not fit, and the field is a path by design.
- The frontend canister's id is only known **after** the first deploy. The usual flow is: deploy once, read the frontend canister id from the output, set `__META_BASE_URL` to `https://.icp.net` (and `__META_ICON_PATH`), then re-deploy to apply. If you control a custom domain for the app, you can set it up front instead.
@@ -188,7 +188,7 @@ Report the deployed canister ids (and the frontend URL, if any) back to the user
6. **Skipping the app metadata.** Without `__META_PROJECT` (Step 2), the canisters still deploy and work but render as bare, unnamed principal rows in the console. Setting `__META_*` is what produces a named app with labelled canisters and an "Open" button.
7. **Wrong `__META_MAIN_CANISTER` value.** It is matched as the exact string `"true"`. A boolean, `"True"`, or marking more than one canister means no (or the wrong) "Open" button. Mark exactly one entry-point canister.
8. **Inventing an icon variable.** The icon variable is `__META_ICON_PATH` (a path resolved against `__META_BASE_URL`). Do not guess `__META_ICON`, `__META_LOGO`, or `__META_ICON_LINK` — they are ignored, so the icon silently never appears.
-9. **Icon set on the wrong canister, or without a base URL.** The icon is read only from the **main** canister and needs **both** `__META_BASE_URL` (a valid absolute `https://` URL) and `__META_ICON_PATH`. Setting the icon path on a side canister, omitting the base URL, or giving a non-https / `data:` base means no icon renders (and a bad base URL also drops the "Open" link).
+9. **Icon set on the wrong canister, or without a base URL.** The icon is read only from the **main** canister and needs **both** `__META_BASE_URL` (a valid absolute `https://` URL) and `__META_ICON_PATH`. Setting the icon path on a side canister, omitting the base URL, or giving a non-https / `data:` base means no icon renders. (The "Open" button still works — it falls back to the main canister's gateway URL — so a bad base URL costs the icon and the custom Open URL, not the button.)
## Related Skills