21 Commits

Author SHA1 Message Date
Oliver Kieselbach 587ed74d96 deploy-web: Entra redirect URIs from the endpoint registry
Both were repository variables holding the www origin; lib/msalConfig.ts uses them
only as the static-render fallback and window.location.origin in the browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:39:49 +02:00
Oliver Kieselbach 8d24c5680b CI: one endpoint registry for every host the workflows use
.github/scripts/Get-Endpoints.ps1 defines the storage account (blob and table
derived from its name), the containers, the AdminConfiguration row, the legacy
mirror, the download alias, API, MCP, portal, www, docs and the timestamp
server. Workflows load it once per job; the script publisher and the local
release build call it; the dev-render anchors are built from the alias entry.

deploy-web takes NEXT_PUBLIC_API_BASE_URL and NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL
from the registry instead of repository variables and refuses an empty value.
A new Workflow gates job fails on any host spelled out in a workflow or CI
helper; the enforced list is derived from the registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:32:36 +02:00
Oliver Kieselbach c26542776e CI: credential-free build jobs, trust jobs that never build
Every job that runs third-party code (restore, build, tests, npm ci, next build,
wix) now holds only a read-only token and hands its output on as one archive.
Signing, publishing and deploying happen in separate jobs on a fresh runner that
run only this repository's scripts and pinned actions, unpack artifacts outside
the checkout and take no build-job output into a script.

- build-agent: reserve -> build -> release
- build-msi: sign-loader -> build -> release
- deploy-backend: reserve -> build -> deploy (publish output as one tar.gz)
- deploy-web: build -> deploy (SWA token only, no id-token) -> record
- combine-rules: validate (read-only, PRs too) -> publish (push to main only)
- deploy-mcp: same shape; documents docker build as its boundary

Deploy-only jobs run in the production environment. The jobs that sign stay
without an environment for now: a job carries one OIDC subject, and the signing
identity still trusts the ref. oidc-login-test.yml is removed; combine-rules no
longer triggers on the dev branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:21:04 +02:00
Oliver Kieselbach be0e37008a Action pins: exact release tag in every version comment (D-331) (#178)
* Action pins: exact release tag in every version comment (D-331)

setup-node v7.1.0 and upload-artifact v7.0.2 moved the floating v7 tag, so the
"# v7" comments on our still-correct pins (v7.0.0, v7.0.1) stopped naming the
pinned commit and zizmor's ref-version-mismatch raised 11 code-scanning
alerts on main. The other five major-only comments (checkout, setup-dotnet,
cache, attest-build-provenance, functions-action) were one upstream release
away from the same alert.

All 38 major-only comments now carry the tag their unchanged SHA resolves to
(checkout v7.0.1, setup-dotnet v6.0.0, cache v6.1.0, setup-node v7.0.0,
upload-artifact v7.0.1, attest-build-provenance v4.2.2, functions-action
v1.5.7); no SHA moved, Dependabot keeps advancing SHA and comment together.
verify-pins now accepts only a vX.Y.Z comment and names the floating-major
case in its own error.

Verified: zizmor online (GH_TOKEN) reproduced the 11 findings before and
reports 0 ref-version-mismatch after, --min-severity low exits 0; the
verify-pins script passes all 51 uses lines and rejects a major-only comment,
a missing comment and a tag reference in a scratch workflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* bump pointer: internal (D-331 action pin comments)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:45:34 +02:00
Claude 762fef4e24 Landing page bakes the platform stats of the deploy into its static HTML; browsers then swap in the live numbers
Crawlers and LLM fetchers run no JS and saw only the skeleton of the stats
band. app/page.tsx now awaits one read of the public stats blob during
next build (lib/platformStats.ts: manifest, then the versioned file it
names) and renders the numbers into the export. StatsBand starts from that
snapshot and replaces it with the live numbers; a failed live read keeps
the baked numbers instead of hiding the band.

The read is awaited at page level, not under Suspense: React moves a
resolved boundary of a page this size into a hidden div at the end of the
body. It uses no-store (route force-static), so Next's fetch cache cannot
hand a later build the previous numbers. Fail-soft: without a snapshot the
page ships the skeleton and loads live as before, and deploy-web.yml warns.

The browser now revalidates both files (no-cache instead of force-cache):
the backend rewrites the per-day file every 2 hours under immutable, so a
copy cached earlier that day would have made the numbers drop after load.

The page body moved to components/landing/LandingPage.tsx, which the
marketing guards render synchronously; StatsBand left the .json() ratchet
baseline; the sitemap lastmod of / includes the body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4ddfosgdeCwXo7NvB9xd2
2026-10-03 03:27:20 +00:00
Oliver Kieselbach 5b0dfaad99 Landing shows the GitHub star count and a customer quote
The navbar gets a "Star" pill with the repository's star count from sm up;
phones keep the bare GitHub mark. A new section before the closing CTA
shows one customer quote with a star ask underneath. The footer's
"Star us on GitHub" line is gone, so the page asks twice, not three times.

The count is read once per web deploy (deploy-web.yml, own step, job token
only there) and inlined by next build as NEXT_PUBLIC_GITHUB_STARS, so
visitors' browsers never contact GitHub and the CSP stays as it is.
Without a value the pill renders without a counter (D-303).

The desktop links, bell and sign-in now start at lg instead of md: the bar
already overflowed between 768 and ~1000 px and the pill widened it further.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 13:13:33 +02:00
dependabot[bot] fb8703a4c6 chore(deps): bump the actions group with 3 updates (#160)
Bumps the actions group with 3 updates: [azure/login](https://github.com/azure/login), [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


Updates `azure/login` from 3.0.2 to 3.1.0
- [Release notes](https://github.com/azure/login/releases)
- [Commits](https://github.com/azure/login/compare/7ddb5af1ef8758cf1353cf3b42f940aee27ba21c...a641126d1b8aa4d1fa005f4f92df94a3a4c4c906)

Updates `github/codeql-action/init` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63)

Updates `github/codeql-action/analyze` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63)

---
updated-dependencies:
- dependency-name: azure/login
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-23 08:18:43 +02:00
Oliver Kieselbach 1b27babd17 ci: zizmor gate for the workflow files plus a clean baseline (D-268)
zizmor (static analysis for GitHub Actions) now runs twice: locally before a
workflow commit (hard rule in .claude/CLAUDE.md) and as the `zizmor` job next to
the pin gate in action-pins.yml (zizmorcore/zizmor-action pinned by SHA, zizmor
1.30.1 pinned, SARIF into Code scanning, severity low and above). Like the pin
gate it reports and does not block. zizmor.yml holds the single reasoned ignore.

Baseline fixes so the gate starts green:
- persist-credentials: false on every checkout except combine-rules.yml, the one
  job that pushes with the checkout credentials (ignored in zizmor.yml, with reason)
- publish-scripts.yml: id-token: write moves from the workflow to the publish job;
  indexnow-notify.yml: permissions: {}
- deploy-mcp.yml: env values referenced as shell variables inside bash steps
  instead of template expansion
- pester.yml / publish-scripts.yml: reusable workflow via the self-repository form
  $/ (GitHub changelog 2026-07-30); the pin gate treats $/ like ./
- combine-rules.yml: ajv-cli installed with npm ci from rules/scripts/package-lock.json
  (no ts-node peer, no lifecycle scripts) instead of an ad-hoc global install;
  Dependabot watches /rules/scripts
- azure/login pin comment corrected to the tag the SHA actually is (v3.0.2; v3
  moved on to v3.1.0), no runtime change
- Dependabot: 7-day cooldown on every ecosystem (security updates are exempt)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 12:56:03 +02:00
Oliver Kieselbach f5efadfd07 ci: pin every third-party action to a commit SHA, gated and Dependabot-maintained (D-226)
A tag is a mutable pointer owned by the action's publisher. These jobs are not
ordinary CI: they hold the code-signing Key Vault token (D-223), the federated
storage credentials (D-222), and combine-rules.yml holds a contents:write token
that pushes to main. Whoever could repoint v7 would run code with all of that.

azure/login and Azure/functions-action were already pinned; the actions/* and
github/codeql-action references were not, on the usual argument that GitHub owns
them and shares the runner's trust boundary. That argument does not survive the
fact that they run in the same jobs as the signing identity. All seven are now
pinned by SHA with the version in a trailing comment -- the comment is what keeps
the line reviewable, and Dependabot rewrites both together.

action-pins.yml is the guard: pins nobody enforces rot, and no review reliably
catches one @v4 added to a 200-line workflow. It lives in its own workflow because
ci.yml deliberately ignores changes to the other workflow files, so a job placed
there would stay silent for exactly the commits it must inspect. Like CI it reports
rather than blocks -- main is unprotected; making it binding would mean adding it
as a required status check, which is a branch-protection decision.

Verified both directions: 49 references pass against the repo, and a fixture with
@v7, @main and a bare SHA fails with three annotations while the local reusable
workflow refs are skipped.

Two floating tool versions closed while here. wix went from the range 5.* to 5.0.2
-- it writes the bytes of the MSI customers install, where AzureSignTool only signs
them. ajv was the worse one: unpinned, and npm runs lifecycle scripts at install,
so a hijacked release would have executed with the push token before a single rule
was validated; now exact versions with --ignore-scripts.

Line endings: sed -i rewrote the workflow files as LF. Git normalises on commit, so
the content diff is unaffected and indexnow-notify.yml carries no change at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 22:14:15 +02:00
Oliver Kieselbach e4a5a6654d ci: workflows authorize Azure Storage with the federated identity, not SAS secrets (D-222)
Every storage write against autopilotmonitoreu -- build-counter CAS, agent ZIP and MSI,
version manifests, customer bootstrap scripts, the AdminConfiguration hash oracle -- now
rides on the existing federated credential (subject repo:.../ref:refs/heads/main) instead
of a long-lived SAS in repo secrets.

The REST calls themselves are unchanged: '?<sas>' became an Authorization header, so the
ETag-CAS of the counters, Content-MD5 and the no-cache contract survive verbatim. That is
also why this is not a rewrite onto 'az storage blob upload' -- it cannot express If-Match,
and two concurrent builds could then mint the same build number.

.github/scripts/Get-StorageAuthHeaders.ps1 is the single place that knows the resource id,
the az invocation and the service version. x-ms-version is mandatory for Entra-authorized
requests (>= 2017-11-09) and for any Table request without a SAS; 2021-08-06 was verified
live against both the blob and the table endpoint of this account.

azure/login added to build-agent, build-msi, publish-scripts (plus id-token: write) and
deploy-web; deploy-backend and deploy-mcp already had it. Publish-BootstrapScripts.ps1
-DryRun acquires NO token: the PR gate runs it without any Azure login, and that must
stay true.

The AdminConfiguration merge lost its "secret not configured" skip branch -- a failure
there now surfaces instead of silently leaving the hash oracle pointing at an older build.

AZURE_BLOB_AGENT_SAS_TOKEN, AZURE_BLOB_VERSIONS_SAS_TOKEN and AZURE_TABLE_ADMIN_SAS_TOKEN
are retired once the first runs are green. AZURE_BLOB_AGENT_LEGACY_SAS_TOKEN stays: that
account lives in the pre-migration tenant, where this principal has no identity -- it dies
with the mirror, not before.

Requires on the SP: Storage Blob Data Contributor on the containers 'agent' and 'versions',
Storage Table Data Contributor on the table 'AdminConfiguration'. Granted 2026-09-08.
2026-09-08 17:38:14 +02:00
Oliver Kieselbach 4e73ba8900 Agent Host changes for agents/improve-changelog-promotion-ui (#152) 2026-09-08 14:57:11 +02:00
Oliver Kieselbach 01311898a8 fix(ci): no third-party action holds release/deploy credentials by mutable tag — build-agent creates releases with first-party gh release create (job holds contents/id-token/attestations:write), combine-rules pushes generated outputs with plain git (contents:write on main), deploy-web builds the static export in CI (setup-node 22, npm ci, next build — same path ci.yml tests) and uploads it with the exactly pinned SWA CLI 2.0.10 instead of Azure/static-web-apps-deploy (Oryx in-container build, deploy token as action input), attests portal-<sha>.tar.gz and drops the unusable per-PR preview environments; azure/login and Azure/functions-action are SHA-pinned with version comments for Dependabot
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 18:21:19 +02:00
Oliver Kieselbach c4280e823c fragility audit P5.x + P7: drift guards across SignalR names, RowKeys, config sections, scoped routing
P5.x-A: Constants.SignalRMessages catalog (13 names) + SignalRGroupHelper.HubName;
all backend send sites and web subscriptions typed against the shared-manifest
union (SignalRContext.on/off) — unknown names are tsc errors. Dead eventStream
session-payload handling removed from diagnosis page.

P5.x-B: RowKeyCodec as the single inverted-tick encoder (persisted widths pinned,
UserActivity D20 legacy); DeletionRowDump.Table on tombstone rows with
DeletionTombstoneTables.Resolve replacing the duplicated Contains('_') heuristic
(legacy fallback retained for pre-field manifests).

P7.1: PATCH config/{tenantId}/fields flipped to TenantAdminOrGA (phase 2);
Settings sections save via per-section diff PATCH from sectionFieldMap.ts —
the ~39-field stale round-trip vector is gone; legacy webhook clearing now only
on the notifications save; agent-settings reset no longer discards contact edits.

P7.2/7.3: useGlobalAdminScope is a projection over useAggregatedAdminScope
(resolveConcreteScopeView, test-pinned); scopedApi builders own the tenant/global
endpoint-pair routing for all 11 former hand-rolled call sites; scopedFetch adds
the shared JSON fetch layer.

P7.4: navVisibility extraction + roles x nav-config matrix and nav<->route-guard
consistency test with a ratchet for the two known pre-existing mismatches
(Operator and GlobalReader vs /settings — user decision pending).

P7.5: web build stamp public/version.json (prebuild, GITHUB_SHA), SWA no-store
route, deploy-web verify poll + versions/web.json manifest, Portal Build card.

P7.6: OkAsyncBaselineGuardTests freezes the 44 anonymous-response sites
(shrink-only ratchet).

Verified: dotnet build 0W/0E, Functions.Tests 4123, DecisionCore 519, V2.Core
2206, SummaryDialog 44; tsc 0, ESLint 0, vitest 762/762; mutation proofs for all
new drift nets. Docs: docs/drift-guards.md + versioning.md web row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 23:17:48 +02:00
Oliver Kieselbach f795fbcb4e feat(auth): dual app-reg parallel operation - legacy gktatooine app alongside new C4A8 app
True parallel operation, no forced re-consent: existing tenants keep signing
in via the legacy app (1a400946), new tenants land on the C4A8 app (886ab5e2).

Model "default legacy, funnel primary, learn in the background":
- Web: am_auth_app flag -> NEXT_PUBLIC_ENTRA_DEFAULT_APP -> primary; get-started
  CTA hands over with ?authapp=primary; auth/me homedApp updates the flag so an
  operator tenant flip takes effect on each user's NEXT login; AADSTS90094/65001
  one-shot cross-app retry, AADSTS65004 -> failed screen with previous-app link;
  am_login_attempt pins redirect returns to the app that started them.
- Backend: TenantConfiguration.HomedAppClientId (null = legacy, GA-only flip via
  PUT config) + LastAuthClientId/Since observability; EntraAppRegistry resolves
  the acting app per tenant (no-op until the legacy config pair is set); Graph
  client-credentials, admin-consent URLs and the surfaced client id follow the
  homed app (app-scoped cache keys); JwtBearer [Authorize] pipeline now shares
  the middleware audience builders (was single-audience - would 401 legacy
  tokens after the swap); middleware trusts EntraId:LegacyClientId implicitly.
- well-known association file now lists both app ids (publisher domain);
  deploy-web passes NEXT_PUBLIC_ENTRA_LEGACY_CLIENT_ID / _DEFAULT_APP;
  mcp-server.bicep default client id -> C4A8 app.

Backend xUnit 3730/3730, vitest 625/625 (13 new), tsc/ESLint clean, next build ok.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:12:25 +02:00
Oliver Kieselbach cc195990ff fix: SWA deploy — drop invalid is_static_export input, dedupe trailing-slash routes
The pinned Azure/static-web-apps-deploy@v1 has no is_static_export input
(warning only — Oryx detected the export and validated out/ regardless).
The real failure was config validation: with "trailingSlash": "auto" SWA
normalizes /x and /x/ to the same route key, so the defensive
trailing-slash duplicates were rejected as duplicate rules. One entry per
route covers both forms under auto.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 01:52:28 +02:00
Oliver Kieselbach 18b28260a7 feat: portal static export — eliminate the SWA SSR runtime (cold-start root fix)
output:'export' + trailingSlash: the SWA now serves plain static files; the
managed SSR runtime — the portal's last cold-start source ("Backend call
failure" when a cold start exceeded SWA's 45s limit) — no longer exists.

- Detail routes are query-string based (/sessions?id=, /sessions/inspector?id=,
  /diagnosis?id=, /apps/detail?name=, /admin/backups/detail?id=,
  /admin/customs-archive/detail?tenantId=&rowKey=). Single-definition builders
  in lib/routes.ts; all 21 link sites migrated, duplicate sessionHref helpers
  consolidated. Suspense boundaries added everywhere useSearchParams drives
  page identity (hard export-build errors otherwise).
- 9 [section] families prerender via server shells (generateStaticParams from
  the *NavSections registries, dynamicParams=false, client bodies extracted to
  SectionClient) — 37 pages; server redirect() index pages became
  ClientRedirect shells. robots/sitemap/og-image routes force-static.
- Legacy PATH-shaped deep links are preserved FOREVER (sent Teams/Slack/
  webhook notifications cannot be edited): SWA wildcard rewrites serve the
  family page and the globally mounted LegacyPathRedirect rewrites to the
  canonical query URL, preserving query + #event hashes (mapping pinned by
  lib/__tests__/legacyPathRedirect.test.ts).
- middleware.ts deleted → client-side HostRoutingGuard (prefix list moved to
  lib/hostRouting.ts). Legacy /go/[code] route + bootstrapValidation deleted —
  BootstrapSessions table verified free of active pre-cutover sessions.
- staticwebapp.config.json now carries production routing: 19 docs/marketing
  redirects (order-sensitive), legacy rewrites (exact before wildcard), 404
  override, all security headers incl. literalized prod CSP (no unsafe-eval),
  Content-Type routes for the extension-less og images. Guarded by the new
  utils/__tests__/swaConfig.guard.test.ts. Maintenance mode is now a
  first-routes-entry insert, never a whole-file swap (README rewritten).
- Backend notification deep links go through Constants.PortalSessionUrl
  (query shape, escaped, pinned); 3 producers migrated.
- deploy-web.yml uploads out/ with is_static_export.

Export build: 79 static pages, no go/ output. Web: tsc clean, vitest 53
files green (618 incl. new guards; 2 deleted legacy suites). Backend:
solution build clean, 3622/3622 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 01:44:32 +02:00
Oliver Kieselbach fd2b5c2da2 fix(ci): drop invalid SWA input, gate deploy on token via env guard
Azure/static-web-apps-deploy@v1 has no `skip_deploy_on_missing_secrets`
input (confirmed against its action.yml), so passing it did nothing but
emit an "Unexpected input(s)" warning — the intended "skip green on
Dependabot PRs" behaviour was never actually in effect.

Replace it with the documented mechanism: surface the deploy token as a
job-level env var and gate the deploy/close step with `if: env.DEPLOY_TOKEN
!= ''` (secrets cannot be referenced directly in an `if:`). On Dependabot
PRs the token is absent, so the step skips and the job stays green; real
deploys on main and internal PRs still run. Applied to both jobs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 16:38:58 +02:00
Oliver Kieselbach cbda62d131 Merge branch 'main' of https://github.com/okieselbach/Autopilot-Monitor 2026-07-22 01:19:36 +02:00
Oliver Kieselbach 3951000b40 ci(deploy-web): skip SWA deploy when token absent instead of failing
Dependabot PRs run without repository secrets, so AZURE_SWA_DEPLOY_TOKEN is
empty and Azure/static-web-apps-deploy exits with an error, painting a red
"Build and Deploy Job" check on every web-touching Dependabot PR. Setting
skip_deploy_on_missing_secrets makes the action skip the deploy (green) when
the token is missing; real pushes to main still carry the token and deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 01:19:26 +02:00
dependabot[bot] 4a1194569e chore(deps): bump actions/checkout from 5 to 7 (#91)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 01:07:14 +02:00
Oliver Kieselbach e2d78c6b06 chore(ci): rename SWA workflow to deploy-web.yml with neutral deploy-token secret
The brave-grass filename and secret name referred to the pre-migration
Static Web App hostname. Workflow now matches the deploy-backend/-mcp
naming scheme and uses AZURE_SWA_DEPLOY_TOKEN (already set to the new
SWA's token). The old secret is removed after the first green run.
2026-07-18 16:51:39 +02:00