pilotswarm-portal-app-reg
Skill14 starsChanged 8 months agoMITView on GitHub
Use when bringing up a PilotSwarm portal stamp with `PORTAL_AUTH_PROVIDER=entra` and no existing `PORTAL_AUTH_ENTRA_CLIENT_ID`. Drives the Entra app-registration step (create app, configure redirect URIs, optionally define app roles + require assignment, capture tenant/client IDs). Skip entirely for `PORTAL_AUTH_PROVIDER=none` or when a client ID is already supplied.
- Reads credentials
We split this skill into the rules it gives an agent
15 rules. About 0 are common, 2 are distinctive, and 13 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
13
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 8
Read .github/DEPLOYMENT.md first.
- Very specific: line 55
Before creating a new app, ask the user for their Service Tree ID.
- Very specific: line 120
If the user provided an EnvName (stamp name) and a bicep-output cache exists, also surface the resolved redirect URI:
- Very specific: line 202
Always invoke pwsh directly.
- Very specific: line 218
Always invoke this wrapper with -File, never -Command.
- Very specific: line 220
If you must run an ad-hoc snippet (e.g.
- Very specific: line 226
Do not use pwsh -Command "$PSVersionTable…" from a PowerShell parent — the parent evaluates $PSVersionTable first and passes garbage to the …
- Very specific: line 273
After this completes the operator MUST assign at least one admin via Set-PortalAuthAssignments.ps1 -EnvName <stamp-name> -AdminAssignments …
- Distinctive: line 296
Use only when you have tenant-admin support to pre-grant the OIDC scopes for the app, OR you accept that each assigned principal will need …
- Very specific: line 352
If a user is a member of 200+ groups, Entra emits the _claim_names / hasGroups overage indicator instead of an inline groups claim.
- Very specific: line 360
After the script creates the admin and user app roles, principals must still be assigned.
- Very specific: line 370
If -AssignmentRequired is set and no one is assigned, sign-in fails with AADSTS50105: The signed in user is not assigned to a role for the …
- Very specific: line 374
If -AssignmentRequired is NOT set (the recommended default), the
- Distinctive: line 396
Never run the wrapper against a non-target tenant — the script will succeed but the app will be useless to your portal
- Very specific: line 398
Never propose granting Application.ReadWrite.All, GroupMember.Read.All, User.Read, or any other delegated/app scope on the portal app — the …
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Select the execution path
Read .github/DEPLOYMENT.md first. The commands below are operator-managed
environment recipes. For this repository’s managed environment, local Azure or
Kubernetes mutations are not permitted. Identity setup is a one-time admin
prerequisite; ongoing changes/reset/teardown need an authorized maintenance
Action, which is not currently supplied. Do not run these recipes locally as
a fallback. Preserve the selected auth posture; VPN steps apply only to a
VPN-enabled target.
pilotswarm-portal-app-reg
Drives the Entra app-registration step for a PilotSwarm portal stamp.
This skill is optional — only invoke it when the user wants
PORTAL_AUTH_PROVIDER=entra AND does not already have a
PORTAL_AUTH_ENTRA_CLIENT_ID to plug in. For PORTAL_AUTH_PROVIDER=none
(open sandbox), skip the whole skill.
When to use this skill
| User signal | Use this skill? |
| "set up entra auth for a new stamp" / "I need a client id" / new stamp with no opinion on auth wiring | YES — create new (default recommendation) |
| "add a new stamp to the existing app" / mentions sharing a client id / wants single SSO consent across stamps | YES — -ExistingAppId mode |
| "production stamp, lock down access" / "only certain people should sign in" / smoke-testing the role-driven authz | YES — create new with -CreateAppRoles and assign users to the admin / user roles |
"skip auth" / "open sandbox" / PORTAL_AUTH_PROVIDER=none | NO — skip entirely |
User already pasted a PORTAL_AUTH_ENTRA_CLIENT_ID | NO — that value flows straight through to deploy |
Default posture: one dedicated app per stamp. Recommend the
create-new path unless the user explicitly asks to share an existing
app. Per-stamp apps keep redirect URI lists short, make environment
teardown a single az ad app delete, and isolate consent / revocation
blast radius. The script auto-derives a friendly, env-specific display
name ("PilotSwarm Portal - <EnvName>") from -EnvName — do not
override unless the user asks.
The deployer agent must decide the auth posture before opening the
new-env defaults table, because the value of
PORTAL_AUTH_ENTRA_CLIENT_ID depends on the outcome.
Service Tree ID for new app registrations (no default)
Setup-PortalAuth.ps1 requires -ServiceTreeId when creating a new
app. Microsoft tenant policy rejects app registrations without a valid
serviceManagementReference. Adding a redirect URI to an existing app
does not create an app and does not require this parameter.
Before creating a new app, ask the user for their Service Tree ID. If they don't
have one registered for their PilotSwarm deployment, stop and direct
them to register one — the tenant will reject az ad app create
otherwise. Do not invent a placeholder GUID.
Underlying tooling
| Script | Path | Purpose |
Setup-PortalAuth.ps1 | deploy/scripts/auth/ | Opinionated wrapper that produces the exact SPA-shaped app reg the portal expects |
Create3PApplication.ps1 | deploy/scripts/auth/ | Generic Azure AD app primitive (reference; not used by the wrapper) |
README.md | deploy/scripts/auth/ | Operator docs |
The wrapper bakes in:
signInAudience: AzureADMyOrgserviceManagementReference: <-ServiceTreeId>(operator-supplied)- SPA platform (no Web reply URLs)
implicitGrantSettings: id-token + access-token issuance ON- No MS Graph / API permissions declared. The portal does NOT call
any downstream API at runtime — group/role claims ride on the token
itself. SPA requests only OIDC standard scopes (
openid,profile) at sign-in, which require no consent. Future downstream API access (e.g. ADO via OBO) belongs on per-purpose worker apps with their own admin consent — seedocs/proposals/portal-auth-provider-and-authz.md. groupsoptional claim onidToken,accessToken,saml2Tokenwith Default formatting- Owner = signed-in user
- Service principal created
Optional (off by default, opt-in with switches):
-CreateAppRolesdefines two app roles (admin,user) that the portal's role-driven authorization engine reads from the access token (seepackages/app/web/auth/authz/engine.js)-AssignmentRequiredsetsappRoleAssignmentRequired=trueon the service principal so only users/groups explicitly assigned to the app can obtain a token.⚠️ Caveat — leave OFF by default. In tenants where user-consent is restricted to verified-publisher apps (e.g. the Microsoft corporate tenant), turning this on causes the first sign-in by every assigned principal to trip an AADSTS90094 admin-consent prompt for the OIDC scopes (
openid profile offline_access) against Microsoft Graph, even though this app declares no API permissions. The workaround is a per-user "dance" (flip off, sign in once to accept user-consent for Graph, flip back on) or a tenant admin pre-granting the OIDC scopes. The recommended lockdown posture uses-CreateAppRolesplus role assignments in Entra, while leavingappRoleAssignmentRequired=false. The role assignment list is the allowlist — the engine's deny-by-default behavior rejects any signed-in principal without an admin/user role claim. See the posture matrix below.
These bakes are NOT user-configurable — they are the contract the PilotSwarm portal depends on. The script's parameters only cover the things that vary per invocation.
Discovery (run before showing the table)
az account show --query "{tenant:tenantId, user:user.name, userObjectId:id}" -o jsontenant→ user's current tenant (used asPORTAL_AUTH_ENTRA_TENANT_ID)user→ suggested display-name suffix and known to the operator- The wrapper itself derives the owner objectId via
az ad signed-in-user show, but surface the UPN so the user knows whose name will be on the app
If the user provided an EnvName (stamp name) and a bicep-output cache
exists, also surface the resolved redirect URI:
# Path: deploy/envs/<env>/bicep-outputs.cache.json
# Look for: portalFqdn | afdEndpointHostname | portalUrl | PORTAL_FQDNPresent the full input surface upfront
Show every parameter in a single table — same UX contract as
pilotswarm-new-env-deploy. Mark each value (default), (discovered),
(required), or (suggested):
Mode
mode create-new (default) # create-new | add-redirect-to-existing
Identity
ServiceTreeId <required for create-new; omit for existing app>
DisplayName <suggested: "PilotSwarm Portal - ${EnvName}">
Owner <discovered: ${userObjectId} (${user})>
SkipGroupsClaim false (default) # keep groups claim ON for PORTAL_AUTH_ENTRA_*_GROUPS
Role-driven authz (recommended for prod stamps)
CreateAppRoles false (default) # add 'admin' + 'user' app roles
AssignmentRequired false (default, recommended OFF — see caveat in "Underlying tooling" above)
Redirect URI (mode=create-new only)
EnvName <required-or-pick-one>
RedirectUri <auto-discover from EnvName, OR ask user explicitly>
# leave both empty to create app shell first, add URI later
Existing app (mode=add-redirect-to-existing)
ExistingAppId <required for this mode>
RedirectUri <required for this mode>
Output
OutputFile deploy/envs/${EnvName}/entra-app.json (default when EnvName given)State the chosen mode explicitly to the user before invoking. Confirm before running — this WRITES to Entra and creates a permanent app reg.
Decide app-role posture before invoking
There are three meaningful postures for a portal stamp; pick one with the user:
| Posture | Switches | Who can sign in | Who gets the admin role | When to use |
| Open | neither (and PORTAL_AUTHZ_DEFAULT_ROLE=user in .env) | Any tenant user | Nobody — every signed-in user gets user | Short-lived test stamps; pure sandbox where the portal is intentionally open to the whole tenant |
| Roles (recommended for prod) | -CreateAppRoles, plus role assignments in Entra | Any tenant user can hit sign-in, but the engine denies anyone without an admin or user role claim | Users assigned to the admin app role via Set-PortalAuthAssignments.ps1 or Enterprise apps > Users and groups | Production stamps. The role assignment in Entra is the allowlist; the engine is deny-by-default for principals with no admin/user role. No env-var allowlist needed. |
Roles + appRoleAssignmentRequired=true (advanced opt-in) | -CreateAppRoles -AssignmentRequired | Only users/groups explicitly assigned to a role | Same as above | Same use cases as the previous row, but adds a second gate at the Entra level. Requires per-user admin or self consent for the OIDC scopes against Microsoft Graph the first time anyone signs in — in restricted tenants this trips AADSTS90094. Prefer the previous row unless you have tenant-admin support to pre-grant. |
| Legacy email allowlist (no roles) | neither, plus populate PORTAL_AUTHZ_ADMIN_GROUPS / PORTAL_AUTHZ_USER_GROUPS in the stamp's .env | Any tenant user can hit sign-in; the engine denies anyone whose email is not on the list | Users whose email matches PORTAL_AUTHZ_ADMIN_GROUPS | Stamps that don't want to do per-stamp Entra role assignments. The engine consults these allowlists only when the JWT carries no roles[] claim — combining them with -CreateAppRoles is redundant (the role-authoritative branch wins). |
Deny-by-default is now in the engine (since v0.1.33). When
PORTAL_AUTHZ_DEFAULT_ROLE is unset or set to none, a principal that
carries no admin/user role claim and matches no email allowlist is
denied at the portal layer — even when appRoleAssignmentRequired is
off. Set PORTAL_AUTHZ_DEFAULT_ROLE=user to restore the legacy "any
tenant user gets user" open posture (only do this for sandbox stamps).
Pick one mechanism per stamp. The engine's role-authoritative
branch (see packages/app/web/auth/authz/engine.js) ignores
PORTAL_AUTHZ_ADMIN_GROUPS / PORTAL_AUTHZ_USER_GROUPS whenever the
JWT carries any roles[] claim. So if you chose Roles, do NOT
also populate the email allowlists — they are bypassed and become a
second source of truth the next operator has to reconcile. The
allowlists are only meaningful in the Legacy email allowlist
posture (no -CreateAppRoles).
Assignment is a separate step. This skill creates the app + role
definitions, but does not assign any principals to them. Immediately
after invoking Setup-PortalAuth.ps1 -CreateAppRoles, use the
pilotswarm-portal-auth-assignments
skill to assign at least one admin (default: the deploying user).
Without that, every sign-in is denied at the portal layer because no
one has the admin or user role claim yet.
Invocation
Always invoke pwsh directly. Do not wrap through npm — there is no
npm wrapper for this script intentionally (keeps the deploy npm pipeline
focused on what's controlled by .env).
Shell compatibility: the script runs identically on Windows, Linux,
and macOS as long as PowerShell 7+ (pwsh) is installed. From the
calling shell's perspective the invocation is a single pwsh -File ...
token followed by named parameters — bash and pwsh handle it the same
way. Use \ for line continuation in bash, ` (backtick) in pwsh.
The script uses forward-slash path separators and cross-platform pwsh
APIs internally. See deploy/scripts/auth/README.md for pwsh install
commands per OS.
Shell quoting pitfalls (read before running ad-hoc pwsh from another shell):
- Always invoke this wrapper with
-File, never-Command.-Filetakes a path + named params and is immune to outer-shell expansion. - If you must run an ad-hoc snippet (e.g. a quick version probe), the
outer shell — whether PowerShell, bash, or zsh — will try to expand
$VARand backticks before pwsh ever sees them. Two safe forms:pwsh -Version(no script needed — preferred for version checks)pwsh -NoProfile -Command '<single-quoted script>'(single quotes suppress outer-shell expansion in both bash and PowerShell)
- Do not use
pwsh -Command "$PSVersionTable…"from a PowerShell parent — the parent evaluates$PSVersionTablefirst and passes garbage to the child. The same trap applies to bash with"$VAR". - For multi-line scripts, prefer a real
.ps1file invoked with-File. Inline-Commandis for one-liners only.
Create new for a stamp (default — recommended)
pwsh -NoProfile -ExecutionPolicy Bypass -File deploy/scripts/auth/Setup-PortalAuth.ps1 \
-ServiceTreeId <your-service-tree-id> \
-EnvName <stamp-name>-EnvName does double duty: it sets the display name to
"PilotSwarm Portal - <stamp-name>" automatically and tells the script
where to read/write per-stamp artifacts. Do not pass -DisplayName
unless the user wants a non-standard name.
This auto-discovers the redirect URI from
deploy/envs/<stamp>/bicep-outputs.cache.json and writes a JSON summary
to deploy/envs/<stamp>/entra-app.json. AFD + VPN stamps
(VPN_GATEWAY_ENABLED=true with EDGE_MODE=afd) get two redirect
URIs registered automatically: the AFD endpoint (public path) and the
portal PORTAL_HOSTNAME (VPN-private path, served by the AppGw
listener behind the Private DNS A record). If the bicep cache doesn't
exist yet (you're running BEFORE first deploy), keep -EnvName for the
display name and per-stamp output path, and add -RedirectUri once you
know the AFD endpoint (or omit it entirely to create the app shell now
and append the URI after bicep runs):
pwsh -NoProfile -ExecutionPolicy Bypass -File deploy/scripts/auth/Setup-PortalAuth.ps1 \
-ServiceTreeId <your-service-tree-id> \
-EnvName <stamp-name> \
-RedirectUri "https://<predicted-afd-fqdn>"Create new for a production stamp (roles — recommended)
pwsh -NoProfile -ExecutionPolicy Bypass -File deploy/scripts/auth/Setup-PortalAuth.ps1 \
-ServiceTreeId <your-service-tree-id> \
-EnvName <stamp-name> \
-CreateAppRolesAfter this completes the operator MUST assign at least one admin via
Set-PortalAuthAssignments.ps1 -EnvName <stamp-name> -AdminAssignments <upn>
(or Entra portal > Enterprise applications > "PilotSwarm Portal - <stamp-name>" > Users and groups).
The portal engine is deny-by-default (since v0.1.33). With role
assignments in place, assigned users get admin / user from the JWT
roles claim. Any tenant user can still hit the sign-in page (since
appRoleAssignmentRequired=false), but the engine rejects them when
they arrive without a role claim. Do not also populate
PORTAL_AUTHZ_ADMIN_GROUPS here — when the JWT carries roles[], the
engine's role-authoritative branch ignores the email allowlists
entirely.
Advanced: roles + appRoleAssignmentRequired=true (Entra-level lockdown)
pwsh -NoProfile -ExecutionPolicy Bypass -File deploy/scripts/auth/Setup-PortalAuth.ps1 \
-ServiceTreeId <your-service-tree-id> \
-EnvName <stamp-name> \
-CreateAppRoles \
-AssignmentRequiredUse only when you have tenant-admin support to pre-grant the OIDC scopes for the app, OR you accept that each assigned principal will need to do the one-time consent dance described in the caveat above.
Add a stamp to a shared existing app
pwsh -NoProfile -ExecutionPolicy Bypass -File deploy/scripts/auth/Setup-PortalAuth.ps1 \
-ExistingAppId <appId> \
-EnvName <stamp-name>After the script runs
The script prints two lines the user must paste somewhere:
PORTAL_AUTH_ENTRA_TENANT_ID = <tenantId>
PORTAL_AUTH_ENTRA_CLIENT_ID = <new-app-id>For a stamp using the npm new-env orchestrator, the operator paths are:
- If you have NOT scaffolded the stamp yet: hand the values to the
user before
new-envruns so they go into the scaffolded.envin the right place. - If you ALREADY scaffolded with
PORTAL_AUTH_PROVIDER=none: the user editsdeploy/envs/<stamp>/.envto flip provider toentraand paste the two values, then re-runs deploy from the manifests step (node deploy/scripts/deploy.mjs portal <stamp> --steps manifests,rollout).
For the legacy bash AKS flow (scripts/deploy-aks.sh /
scripts/deploy-portal.sh), paste the values into .env.remote
(or wherever your live cluster reads portal config from) and re-run
./scripts/deploy-portal.sh.
The wrapper itself NEVER edits .env files — the operator pastes the
values explicitly so they surface visibly.
Admin consent
Not required. The portal app registration declares no API
permissions. The SPA requests only OIDC standard scopes (openid,
profile) at sign-in, which require no user or admin consent. The
portal's own-API token (<clientId>/.default acquired silently after
login) is a token to the portal's own app, not to any external resource,
so it also requires no consent.
This is deliberate: the portal does not call MS Graph or any other downstream API at runtime. Group and role claims that drive authorization are sourced from the ID token directly:
- Group membership via the
groupsoptional claim configured on the app registration (the wrapper applies this). - Role membership via Entra app-role assignments (created with
-CreateAppRoles, assigned viaSet-PortalAuthAssignments.ps1).
If a user is a member of 200+ groups, Entra emits the _claim_names /
hasGroups overage indicator instead of an inline groups claim. The
portal does not resolve overage (would require Graph and admin
consent). Stamps with overage-prone users should use the roles posture
(-CreateAppRoles) instead of group-based authz.
Role assignments (when -CreateAppRoles is used)
After the script creates the admin and user app roles, principals
must still be assigned. The script does NOT do this — it only defines
the roles. Use the dedicated assignments script + skill:
deploy/scripts/auth/Set-PortalAuthAssignments.ps1— idempotent add/remove/list. See.github/skills/pilotswarm-portal-auth-assignments/SKILL.md.- Entra portal ("Enterprise applications >
<app>> Users and groups") still works if you'd rather click.
If -AssignmentRequired is set and no one is assigned, sign-in fails
with AADSTS50105: The signed in user is not assigned to a role for the
application. Assign the operator FIRST so they can finish setting up.
If -AssignmentRequired is NOT set (the recommended default), the
portal engine still enforces deny-by-default: a signed-in
principal with no admin/user role claim sees a "You are not authorized
to access this portal" page rather than the app. Assign at least one
app-role admin via Set-PortalAuthAssignments.ps1 before the first
sign-in so someone can actually reach the portal.
What this skill will not do
- Will not write to
.envfiles (operator surfaces values explicitly) - Will not assign users to app roles — use
Set-PortalAuthAssignments.ps1 - Will not modify the npm
new-env/deploypipelines — they continue to consumePORTAL_AUTH_ENTRA_CLIENT_IDas plain input - Will not provision app registrations of non-portal shapes — for
worker daemons or APIs, use
Create3PApplication.ps1directly or write a new wrapper - Will not invent a Service Tree ID when creating an app — operator must supply
Constraints
-ServiceTreeIdis mandatory for app creation by tenant policy — refuse to invent a placeholder. It is not required when appending a redirect URI.- Never run the wrapper against a non-target tenant — the script will succeed but the app will be useless to your portal
- Never propose granting
Application.ReadWrite.All,GroupMember.Read.All,User.Read, or any other delegated/app scope on the portal app — the portal declares no API permissions at runtime. Downstream API access (e.g. ADO via OBO) belongs on per-purpose worker apps, not the portal app - Re-running the wrapper against a legacy app reg (created before this
shape was canonized) will normalize
requiredResourceAccessto[]by removing dead-weightUser.Read. This is intentional —User.Readwas declared but never actually used at runtime. Legacy users with existing consent records are unaffected; the portal's.defaultscope on its own clientId continues to work - Single-tenant only by design. Multi-tenant or personal-MSA sign-in is
out of scope for the wrapper; use
Create3PApplication.ps1if you need a different shape
In this repository50 other files it gives its agents.
- packages/sdk/examples/repo-workspaces/seed/home/AGENTS.mdAGENTS.md
- .github/copilot-instructions.mdCopilot instructions
- agent-repair-loopSkill
- duroxide-orchestration-versioningSkill
- editorial-standardsSkill
- finance-research-standardsSkill
- valuation-methodsSkill
- pilotswarm-architectureSkill
- deployment-safetySkill
- incident-responseSkill
- add-activitySkill
- add-eventSkill
- add-testSkill
- add-toolSkill
- agent-versioningSkill
- azure-ps-test-runner-vmSkill
- debug-orchestrationSkill
- debug-testSkill
- horizondb-idiosyncrasiesSkill
- horizon-facts-eval-sweepSkill
- investigate-duroxide-runtimeSkill
- pilotswarm-aks-deploySkill
- pilotswarm-aks-resetSkill
- pilotswarm-new-env-deploySkill
- pilotswarm-perfSkill
- pilotswarm-portal-auth-assignmentsSkill
- pilotswarm-releaseSkill
- pilotswarm-tuiSkill
- pilotswarm-vpn-client-profileSkill
- schema-migrationSkill
- notesSkill
- share-a-fileSkill
- cost-latency-analysisSkill
- graph-debugSkill
- orchestration-session-lifecycleSkill
- resourcemgrSkill
- sweeperSkill
- canvas-appsSkill
- html-visualsSkill
- pilotswarm-agent-versioningSkill
- pilotswarm-aks-identitySkill
- pilotswarm-azure-deployerSkill
- pilotswarm-azure-lessonsSkill
- pilotswarm-cli-builderSkill
- pilotswarm-duroxide-versioningSkill
- pilotswarm-hybrid-datastoreSkill
- pilotswarm-knowledge-harvesterSkill
- pilotswarm-portal-builderSkill
- pilotswarm-sdk-builderSkill
- pilotswarm-three-tierSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

