Skip to content

feat(analyzer): Daml DAR analyzer — core, CLI, REST, and Web UI - #295

Draft
zheli wants to merge 108 commits into
mainfrom
daml-analyzer
Draft

feat(analyzer): Daml DAR analyzer — core, CLI, REST, and Web UI#295
zheli wants to merge 108 commits into
mainfrom
daml-analyzer

Conversation

@zheli

@zheli zheli commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a Daml DAR analyzer spanning the core runtime, CLI, REST API, and Web UI.

  • Core (internal/analyzer/): analyzer types + JAR wrapper package; runs the analyzer as a pinned Docker image (not a vendored JAR); prefers the DPM component runtime.
  • CLI: new dar analyze command (internal/cli/localnet/dar/analyze.go).
  • REST: analyzer handlers + routes (internal/ui/handlers/analyzer.go, internal/api/types/analyzer.go).
  • Web UI: Analyzer tab with full upstream view parity (frontend/src/screens/AnalyzerScreen.tsx).

Test plan

  • Go tests: internal/analyzer, internal/ui/handlers, internal/cli/localnet/dar, internal/api/types.
  • Frontend tests: frontend/src/screens/AnalyzerScreen.test.tsx.

zheli and others added 30 commits June 25, 2026 22:50
The help text no longer references cache/instance paths, but the
format call still passed those values and failed govet printf.

Co-authored-by: Cursor <cursoragent@cursor.com>
Modified the labels for wallet endpoints in the orderedEndpointKeys function to include the username for each wallet type, enhancing clarity for users. This change improves the user interface by providing more descriptive labels for the Wallet options.
The down test computed the non-devkit container count with
`grep -cv ... || echo "0"`. On empty/no-match input, `grep -c`
already prints "0" but also exits non-zero, so the `|| echo "0"`
fallback fired and produced a two-line value ("0\n0"). That broke the
subsequent `[ "$BEFORE" -eq "$AFTER" ]` integer comparison
("[: 0\n0: integer expected"), making the test fail with
"down failed or containers remain" even though localnet down
succeeded.

Drop the redundant fallback so the count is a clean integer.
The E2E job runs on a persistent self-hosted runner. `docker compose
down --volumes` only removes volumes Compose itself created; volumes
left by an earlier run are re-adopted as external on the next `up`
(Compose warns "already exists but was not created by Docker Compose")
and are NOT removed by `down --volumes`.

This stranded canton-e2e-test-default_postgres and
_domain-upgrade-dump across runs, tripping M1-CLN-001's "volumes remain
after clean" check even though `canton-devkit clean` ran correctly.
The failure was an environment/harness issue, not a product bug.

Explicitly remove the per-project volumes by name prefix in both the
pre-run "Clean stale state" and the "Force cleanup on failure" steps so
each run starts from a clean slate regardless of how prior volumes were
created.
Updated AGENTS.md to include a new section on best practices for creating temporary files and directories. Emphasized the importance of using the current working directory or repository root, suggested relative paths, and outlined cleanup procedures. Noted exceptions for using system-level directories like /tmp when necessary.
Add docs/changes-from-proposal.md, a maintained changelog of every
deliberate deviation between the original Development Fund proposal
(docs/original-devkit-proposal.md) and the shipped implementation.

Seeded with 24 entries covering all deviations audited from the current
CLI surface: instance name addressing, --format vs --json, command
aliases, new lifecycle/inspection/token subcommands, connection flag
conventions, telemetry, and up-time flag additions.

Also adds a load-bearing 'Proposal deviation tracking' rule to AGENTS.md
(plus PR checklist item #7) requiring contributors to update the
changes file in the same PR whenever command syntax, flags, aliases,
defaults, or user-facing behaviour diverges from the proposal. The rule
lives in AGENTS.md rather than a .claude/skills/ skill so it fires on
every agent session, not only when an agent judges the task matches.
Group all deviations by the subcommand they affect instead of a flat
list. Add a top-level framing paragraph stating that every deviation is
intentional (UX, performance/resource efficiency, security, correctness,
or CLI \u2194 Web UI parity) rather than a mistake.

Structural changes:
- Add 'Cross-cutting conventions' section for name addressing, --format,
  and aliases (span multiple commands so not tied to one subcommand)
- Group all three 'localnet up' flag additions under a single section
  with subsections
- Merge contracts/tx entries into one section with subsections
- Merge dar entries into one section with subsections
- Merge all five token entries into one section with subsections
- Rebuild table of contents to match new structure

No content removed; all 'Proposal said / Shipped / Why' text preserved
and lightly improved to reinforce intentionality.
Comment out the DPM component OCI publish steps (Install DPM CLI, Lay
out component dirs, Log in to GHCR, Validate manifest, Publish) and
their associated env vars (GHCR_NAMESPACE, DPM_VERSION, DPM_LINUX_SHA256).

The OCI publish now lives in homebrew-canton-devkit's publish-oci.yml
workflow, which triggers on GitHub Release creation and publishes to
the public namespace ghcr.io/bitdynamics-ab/homebrew-canton-devkit:<ver>.

The commented-out steps are preserved with a TODO for re-enabling once
we have a public OCI registry we can push to directly from this repo.

Also updates docs (getting-started.md, packaging.md, README.md) to
reference the new public GHCR namespace.
The release workflow has no docker build/push. Everything published to ghcr.io/bitdynamics-ab/canton-devkit:<tag> goes through 'dpm publish component'. This was leftover wording from before the DPM-component publish existed.
…rkflow

Describes the GitHub Actions workflow to add at .github/workflows/verify-public-oci.yml. Verifies that the public canton-devkit DPM component OCI artifact is:
- Anonymously pullable from GHCR (package is Public)
- Multi-arch in OCI index metadata (linux/amd64, darwin/arm64, windows/amd64)
- Installable + runnable on linux/amd64 via 'dpm install package oci://...'

Resolves the sdk-version open question: 'dpm install package' accepts the OCI ref as a positional argument, requiring no project file or sdk-version.
Weekly (Mon 05:00 UTC) + manually-dispatchable workflow that proves the
published ghcr.io/bitdynamics-ab/canton-devkit DPM component is:

1. Anonymously pullable from GHCR (the package is genuinely Public).
2. Multi-arch in metadata — the OCI index lists linux/amd64,
   darwin/arm64, and windows/amd64.
3. Installable + runnable on linux/amd64 via 'dpm install package
   oci://...' and 'dpm localnet --help'.

The check uses only raw curl (v2 API with a self-fetched public pull
token) and the sha256-pinned DPM CLI — no Docker credentials, no Docker
images, no marketplace actions beyond curl/jq on the runner.

Also adds a '# Keep in sync with verify-public-oci.yml' cross-reference
comment in release.yml next to DPM_VERSION / DPM_LINUX_SHA256 so both
files are bumped together.
$GITHUB_PATH additions only take effect in subsequent steps, not in the
same step where the echo is done. Bare 'dpm --version' therefore failed
with exit 127. Use "$bindir/dpm" --version (matching release.yml:401).
Problem 1: 'dpm install package' requires a daml.yaml with a components:
entry in the current directory — there is no argument form. The step was
passing the OCI ref as a positional arg, which dpm does not support.

Problem 2: 'dpm install package' requires a strict semver OCI tag;
symbolic tags like 'latest' are rejected with 'invalid semantic version'.
The resolved version is extracted from the manifest's
org.opencontainers.image.version annotation (written by dpm publish) and
stored as INSTALL_VERSION for use in the install step.

Fix: after the anonymous fetch step, extract INSTALL_VERSION from
manifest.json. The smoke test creates a minimal daml.yaml (no sdk-version
to avoid the 'opt-in components + SDK bundle' conflict) in a RUNNER_TEMP
workdir, then runs 'dpm install package' and 'dpm localnet --help' from
that directory. Verified locally against latest (resolved to 0.10.1).
Bumps [undici](https://github.com/nodejs/undici) from 7.27.0 to 7.28.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](nodejs/undici@v7.27.0...v7.28.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.28.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Rename for clarity — the workflow name now describes what it tests
(canton-devkit end-to-end functions / Milestone 1 lifecycle) rather
than just 'E2E'. Updates the display name to match.
Updates display name to 'E2E: DPM Installation', job name to
e2e-test-dpm-installation, and the cross-reference comment in
release.yml. No logic changes.
The App config panel and JWT generator rendered tokens as <redacted>,
so copy-pasted env/json/yaml config and generated JWTs were unusable
against the running ledger.

LocalNet is loopback-only and signs with a shared dev secret (the
dev-secret warning already renders on the JWT panel), so surface the
raw token directly:

- api.ts: app-config fetchers request ?include_jwt=true
- DeveloperSetup: JWT panel issues with include_jwt=true on mount and
  shows the token immediately; drop the reveal/hide toggle in favor of
  a Copy-only button

Backend and CLI keep their redacted-by-default behavior; only the
LocalNet Web UI opts into raw tokens.
The unquoted 'name: E2E: ...' value was parsed as a nested mapping
(colon-space), making both e2e workflows invalid YAML so GitHub Actions
never ran them. Quote the value to fix parsing.
- Reconcile platform-support claim in faq.md with the tested
  matrix (macOS arm64, Linux amd64, Windows amd64).
- Drop the "Homebrew not yet published" hedge in getting-started;
  point to docs/homebrew.md (tap + formula automation already ship).
- Rewrite the limitations.md observability section: the host-level
  shared stack has shipped (observability.md is authoritative); the
  remaining limitation is the transitional per-instance dual stack.
- Remove brittle "proposal line 188" citations.
- Fix a broken link to a nonexistent docs/issues/*.md in the M1 e2e
  transcript.
- Align install-snippet version placeholders in packaging.md.
- Add observability.md to the README docs index.
- Move two stale internal docs out of the tree (content preserved in
  Linear BIT-234 and BIT-235): design/localnet-token-workspace.md
  (shipped; now covered by docs/tokens.md) and
  ux-improvement-followup.md (residual --name→positional TODO).
- Remove obsolete .claude/launch.json.
Remove documents that only made sense inside the development-fund
process: the original proposal, the proposal-deviation log, the reviewer
kit, the internal telemetry design proposal, and the per-milestone e2e
test scripts. Reframe the remaining docs in neutral OSS voice: no
milestone/acceptance framing, no internal reviewer process, working
links only.

Replace AGENTS.md with CONTRIBUTING.md: same engineering rules (build,
test, lint, testing requirements, the CLI/Web-UI parity convention,
commit and PR guidance) minus the internal process framing. Drop
CLAUDE.md and local tooling entries from .gitignore.
Remove comments that restate the code, historical narration, and
internal process references; compress verbose rationale to its load-
bearing core; delete provably dead unexported code; apply mechanical
simplifications (redundant else-after-return, unneeded conversions,
single-use trivial helpers) where equivalence is certain.

Deliberately unchanged: all exported identifiers and signatures, error
strings, exit codes, CLI flags and help text, JSON field names, HTTP
routes, log formats, and generated files. Genuine why/invariant/
security/concurrency comments stay. Full go test, golangci-lint,
tsc, and vitest suites pass identically.
New website/ directory with a Starlight site built from the repo docs:
landing page with quick start, guides (lifecycle, DAR, explorer,
observability, tokens), and reference (versions, packaging, telemetry,
limitations, troubleshooting). Builds with npm run build (16 pages).

Add a GitHub Pages deploy workflow (SHA-pinned actions) that publishes
the site on pushes to main touching website/ or docs/.
zheli and others added 28 commits July 12, 2026 18:58
Cleanup helpers unconditionally ran `docker compose down --volumes`
after `canton-devkit remove`, and that fallback left detached volumes
behind (compose down only removes volumes it created; the restore path
re-creates the postgres volume out of band via `docker run -v`, so it
is adopted as external and skipped) -- the leak that trips M1-RMV-001.

- Always use `canton-devkit remove` first; only fall back to docker when
  remove fails or resources actually remain (e2e_cleanup_instance +
  e2e_instance_resources_remain).
- When the docker fallback does run, remove everything including
  detached volumes via a name-prefix `docker volume rm -f` sweep
  (e2e_force_docker_cleanup).
- Rewire lib.sh cleanup, m1-snp-001, m1-up-002, and m1-rmv-001 fallbacks.
* feat(ui): show build commit id in side nav footer

Replace the non-actionable "loopback only" / "schema v1" footer text
with the short git commit the UI bundle was built from, so screenshots
reveal which UI commit a user is on. Injected via a Vite `define`
constant sourced from `git rev-parse --short HEAD` at build time,
falling back to "dev" outside a git checkout.

* feat(ui): keep schema version in footer alongside commit id

The schema version still guards the stale-tab-after-upgrade case, so
show it in the side nav footer next to the UI build commit id.

* feat(ui): label footer commit as "UI version"

* feat(ui): link footer UI version to the GitHub commit

Render the build commit id as a link to
github.com/bitdynamics-ab/canton-devkit/commit/<sha>. The "dev"
fallback (no git checkout / Vitest) stays plain text since there's no
commit to point at.
The per-test-job e2e pinned actions/download-artifact to a corrupted SHA
(65a9edc12816...) that no longer resolves, so every M1 test job failed at
'Unable to resolve action' and the suite never ran (red on main). Repin to
the real v4.1.7 commit (65a9edc58814...).
…nce sidecars (#264)

Every observability-enabled LocalNet also spun up its own Prometheus+Grafana
overlay on top of the host-shared stack, so N instances ran N+1 of each — pure
duplication that wastes ~600 MiB per environment and pressures a memory-capped
Docker host.

`up --observability-mode auto|shared|per-instance` (default auto) now prefers
the shared stack and skips the per-instance overlay when it is reachable;
per-instance remains the platform-independent fallback. The mode is persisted
so a re-up preserves it. When the overlay is skipped the metrics/dashboard
links resolve to the shared Grafana (filtered to the instance) rather than a
dead, allocated-but-unbound per-instance port.

Making shared the default and dropping the overlay entirely waits on validating
the shared stack's host.docker.internal scrape path on native Linux; until then
auto covers Docker Desktop and per-instance is the escape hatch.
…265)

* feat(observability): --observability-mode to skip redundant per-instance sidecars

Every observability-enabled LocalNet also spun up its own Prometheus+Grafana
overlay on top of the host-shared stack, so N instances ran N+1 of each — pure
duplication that wastes ~600 MiB per environment and pressures a memory-capped
Docker host.

`up --observability-mode auto|shared|per-instance` (default auto) now prefers
the shared stack and skips the per-instance overlay when it is reachable;
per-instance remains the platform-independent fallback. The mode is persisted
so a re-up preserves it. When the overlay is skipped the metrics/dashboard
links resolve to the shared Grafana (filtered to the instance) rather than a
dead, allocated-but-unbound per-instance port.

Making shared the default and dropping the overlay entirely waits on validating
the shared stack's host.docker.internal scrape path on native Linux; until then
auto covers Docker Desktop and per-instance is the escape hatch.

* feat(observability): expose --observability-mode in the Web UI create flow

Phase 1 added the CLI flag, but the Web UI create path always used the auto
default with no way to choose — a CLI-only feature. Wire the same control for
CLI ↔ Web UI parity: the create request carries observability_mode (validated
server-side with the same rule as the CLI), threaded into UpOptions, and the
create modal shows a "Sidecar stack" picker (auto | shared | per-instance)
when an observability profile is selected.

* feat(observability): health-probe auto fallback, shared-stack status, Linux e2e

Three robustness/coherence additions on top of the shared-only mode:

- auto now health-probes the shared Prometheus (/-/healthy) after ensuring the
  stack, so an instance isn't bound to a stack that is up but not serving —
  it falls back to the per-instance overlay instead.
- observability status reports a "Shared stack: registered" line (+ shared in
  the JSON) so a shared-only instance reads coherently rather than "off".
- scripts/e2e-observability.sh + a CI job validate the shared-only scrape path
  (incl. host.docker.internal reachability) on the self-hosted Linux runner —
  the gate for making shared the default.

* docs(observability): document mode persistence, status source, Web UI picker, auto health-probe

* fix(e2e): run localnet up via the binary, not the cli() function, under timeout

---------

Co-authored-by: Zhe Li <linuxcity.jn@gmail.com>
RunUp was refactored to take a Progress (RunUp(ctx, prog, opts)), but the
build-tagged integration_test.go still passed (ctx, os.Stdout, os.Stderr, opts).
Normal `go test` doesn't compile integration-tagged files, so it rotted
undetected — the Integration workflow has been red on main with
'too many arguments in call to RunUp'. Wrap stdout/stderr in NewTextProgress,
matching the CLI's RunUp call.
* test(e2e): add bats-core dpm localnet suite (DPM-DAR-001)

Reimplement the `dpm localnet` end-to-end suite on bats-core instead of
a hand-rolled shell harness. bats-core and its helper libraries are
vendored as pinned git submodules under e2e-tests/:

  e2e-tests/bats                         bats-core v1.13.0
  e2e-tests/test_helper/bats-support     v0.3.0
  e2e-tests/test_helper/bats-assert      v2.2.4

DevKit-specific helpers (component assembly + Daml project scaffolding)
live in e2e-tests/test_helper/dpm.bash; the generic pass/fail/skip and
summary machinery is now provided by bats itself.

DPM-DAR-001 (e2e-tests/dpm-dar-001.bats) is the first case: it builds a
local file-based component from the freshly built binary, scaffolds a
Daml project, and asserts `dpm localnet dar build-upload --build-only`
succeeds without the issue #230 "file exists" signature. --build-only
means no LocalNet is required.

Run locally with `make e2e-dpm` (initializes submodules on demand,
skips gracefully when dpm is absent). CI recreates the hermetic,
no-artifact single-runner workflow, now invoking bats via a composite
action with submodules: recursive checkout.

Note: milestone1 (scripts/e2e-milestone1.sh) remains on the old harness
and will migrate to bats in a follow-up.

* docs(e2e): document the bats-core dpm localnet e2e suite

Add docs/e2e-testing.md covering both E2E layers (the new bats-core
`dpm localnet` suite under e2e-tests/ and the Milestone 1 LocalNet
lifecycle suite): layout, the pinned bats submodules, running locally
via `make e2e-dpm`, writing new .bats tests with dpm.bash helpers, the
CI workflow, and how to bump pinned versions.

Publish it to the docs site: add a docs-map.mjs entry (reference/
e2e-testing) and a Reference sidebar link in astro.config.mjs.
… of band (#273)

* fix(localnet): restore requires a prior up; never create a volume out of band

RunRestore loaded a pg_dumpall stream into <project>_postgres via a
throwaway `docker run -v <vol>:...`. When the target instance had never
been `up`, that `docker run` CREATED the volume out of band, with no
`com.docker.compose.*` labels. A later `up` adopts it (the "volume ...
already exists but was not created by Docker Compose" warning), and
`docker compose down --volumes` (used by `down` and `remove --force`)
refuses to delete a volume Compose did not create — so the volume is
stranded. This is the root cause behind the M1-RMV-001 e2e failure
("FAIL step 3b: volumes remain after remove").

Require the target instance to already be registered (it was `up`, so
Compose owns the volume) before restoring; refuse otherwise and tell the
user to run `localnet up <name>` first. Applies to cross-name restore
too. This keeps the "never create a volume outside Compose" invariant.

The precondition is enforced via the registry (a proxy for "the
Compose-owned volume exists"). A stricter Docker volume-ownership label
check is deferred and documented in docs/limitations.md.

Tests: seed a stopped instance in the round-trip, cross-name, and
content-SHA restore tests; add TestRestore_RefusesUnknownInstance.

* test(e2e): M1-SNP-001 uses `down` (not `remove`) before restore

Restore now loads into the instance's EXISTING Compose-owned Postgres
volume and refuses to create one out of band. The snapshot/restore test
tore the instance down with `remove` (which reclaims the volume), then
restored — which the new contract correctly rejects with "instance not
found — run localnet up first". Switch the teardown to `down`, which
preserves the volume, so restore has a Compose-owned volume to load into.

* test(e2e): M1-UP-003 precleans e2e-version-test before up

The test brings up a fresh `e2e-version-test` but had no precondition
cleanup, so a leftover instance from a prior/aborted run caused step 1
to abort with "instance already running". Add a best-effort
`e2e_cleanup_instance` before `up`, matching the self-defending pattern
used by other lifecycle tests. Cleanup lives in the test's precondition
(not teardown) because M1-LST-001 depends on e2e-version-test staying up.
Reduce required fields from seven to three so reporters can file bugs
without filling separate expected/actual, surface, and OS dropdowns.
Splice's nginx routes by Host header. The bare host URL DevKit
advertises (http://localhost:<UI_PORT>) matches no *.localhost vhost, so
nginx falls through to the first server block on the port:

  - app-provider (52343): first block is ans.localhost -> served the
    Amulet Name Service instead of the wallet.
  - sv (52344): catch-all `server_name localhost _` served a
    non-existent static dir (/usr/share/nginx/sv-html) -> HTTP 404.

app-user already worked because its wallet block listed
`server_name localhost wallet.localhost`.

The upstream .conf files live in the content-hash-verified Splice cache
(shared across all instances), so editing them in place risks a hash
mismatch and is not per-instance safe. Instead, ship DevKit-owned copies
as embedded assets and bind-mount them over the upstream templates via a
per-instance compose overlay (WriteNginxVhostOverlay), mirroring the
existing loopback-ports/container-rename overlay pattern.

The DevKit copies add `localhost` to each wallet server block and drop
`localhost` from the sv catch-all. nginx.conf and the includes/ dir are
unchanged upstream, so the overlay keeps those pointed at the cache.

Verified end-to-end against localnet-2: all three bare wallet URLs now
return HTTP 200 and serve the wallet bundle, with ans.localhost,
scan.localhost, and sv.localhost vhosts still routing correctly.
…n existing assets (#240)

* Update artifact references in index.html and do not fallback to index.html for non existing assets

* Update comments

* Update comment
* docs: list full DPM SDK packages in daml.yaml examples

DPM projects need the Canton 3.5.2 SDK components alongside the DevKit
OCI package; the install examples previously omitted most of them.

* chore: ignore workspace files and keep tmp/ ignored

Exclude local *.code-workspace files from git and keep tmp/ listed with the other build/scratch ignores.

* docs: keep DPM SDK version as a generic placeholder

Use <your-sdk-version> in daml.yaml examples instead of a pinned
release so the install docs stay version-agnostic.

* docs: keep concrete 3.5.2 versions in README daml.yaml example

Leave the README install example pinned so readers can copy it as-is;
getting-started and packaging keep the generic placeholder.

* ci: skip e2e and integration workflows on doc-only PRs

Ignore docs/, website/, and Markdown paths on pull_request so labeled
or synchronized doc-only changes do not queue the heavy runners.
…280)

* feat(localnet): instance-scoped nginx vhosts for every Splice UI/API

Serve each Splice UI/API behind an instance-scoped virtual host of the
form <service>.<instance>.localhost (e.g. wallet.localnet-2.localhost)
instead of the flat *.localhost names, so URLs stay unambiguous across
concurrently running localnets.

- assets/nginx/{app-provider,app-user,sv}.conf: server_name is now a
  ${VHOST_*} placeholder (wallet, ans, scan, sv, json-ledger-api,
  grpc-ledger-api); the flat Splice names are dropped. The deprecated
  canton.localhost block is left flat.
- overlay.go: add instanceVHost() + VHostService* consts; thread the
  instance name into WriteNginxVhostOverlay and inject the per-instance
  VHOST_* values as nginx container env (envsubst expands them at boot).
- up.go / status.go: advertise wallet UIs at wallet.<instance>.localhost
  in the welcome screen and status endpoints.
- env.go: instance-scope CANTON_SCAN_UI_URL and emit per-role
  CANTON_<ROLE>_{JSON,GRPC}_LEDGER_API_URL plus unqualified
  CANTON_{JSON,GRPC}_LEDGER_API_URL aliases (app-provider), each behind
  the matching ledger-api vhost.
- token/registry_url.go + registry/client.go + registry/doc.go +
  canton integration test: thread the instance-scoped scan Host header
  through DevKit's own scan-registry client.
- ui_reachability.go: dial loopback (multi-label *.localhost does not
  resolve via the OS/Go resolver on macOS) but carry the wallet vhost as
  the Host header so the probe validates the real route.

Note the resolution caveat documented throughout: *.localhost resolves
to 127.0.0.1 in browsers, curl, and Go, but not in JVM/Node/Python
resolvers, which must send an explicit Host header (HTTP) / :authority
pseudo-header (gRPC) or add an /etc/hosts entry.

* feat(localnet): role-scoped nginx vhosts for wallet and ledger APIs

Serve role-specific Splice services at role-scoped instance vhosts of the
form <service>.<role>.<instance>.localhost (wallet, json-ledger-api,
grpc-ledger-api, ans) so per-role UIs/APIs are unambiguous. Single-per-
instance services (scan, sv) keep the shorter <service>.<instance> shape.

The longer role-scoped names overflow nginx's default 64-byte
server-name hash bucket, so add a DevKit-owned http-context tuning
snippet (00-devkit-tuning.conf) that bumps server_names_hash_bucket_size
to 128 and mount it straight into conf.d; without it nginx aborts on
boot with "could not build server_names_hash".
- Rename the test to make explicit it only exercises the build step
  of `dpm localnet dar build-upload` (--build-only); no LocalNet or
  upload RPC is involved.
- e2e-dpm-test action: tee bats TAP output and render a markdown
  pass/fail table to $GITHUB_STEP_SUMMARY so results are visible in
  the GitHub Actions run summary without opening logs.
…istory, allocations (DvP), batching (#277)

* feat(token): bundle Token Standard V2 DARs, interface consts, shared types

Foundation for CIP-0112 (Token Standard V2): auto-bundle the
allocation-v2, transfer-events-v2, and util-token-standard-wallet DARs
alongside the test-token DAR, add the V2 interface/choice name
constants, and define the schema-pinned API types shared by the CLI
--json output and the Web UI REST/SSE payloads so the two surfaces
cannot drift.

* feat(token): identity switcher, EventLog history, allocations/DvP, batching

Four CIP-0112 capabilities on both the CLI and the Web UI Tokens screen,
over one shared orchestration layer:
- identity/act-as picker: operate as app-user / app-provider / sv, with
  the role threaded through every token call.
- EventLog transaction history: per-instrument activity reconstructed
  from the V2 EventLog interface, newest-first.
- allocations / DvP: allocate, list, settle, withdraw, cancel via the V2
  AllocationFactory.
- opt-in atomic transfer+accept batching via BatchingUtilityV2.

* fix(token): live-validated V2 correctness and Tokens-page UX

Fixes found by exercising create -> mint -> transfer -> allocate on a
live LocalNet, plus Tokens-screen UX. Each lands on both the CLI and the
Web UI:
- authorize V2 ops: grant read-any + per-issuer act-as, resolve issuer
  aliases on create, onboard freshly-allocated parties to the synchronizer.
- self-custodial receiver account so V2 mint accept works; reject
  self-mint (issuer == receiver) with an actionable error.
- correct the allocation wire shapes to V2 (drop expectedAdmin;
  SettlementInfo and TransferLegSide v1 -> v2).
- list created-but-unminted instruments (discovery was holdings-only).
- drop phantom bare-alias parties from the readable set and matrix.
- Holdings matrix filter-by-token; copy party ids; paginate the Activity
  feed newest-first.
- --atomic batching: wire shapes corrected, but closed as experimental
  (ExecuteBatch does not rebind the accept leg to the transfer leg's
  intra-batch instruction) -- it fails with a clear error; sequential
  transfer+accept is the supported default.

* chore(token): trim comments and drop internal references

Open-source hygiene pass over the token subsystem: cut running commentary
and code-restating comments (~1000 fewer comment lines) while keeping the
load-bearing wire-shape and Daml-quirk rationale, and remove internal
ticket tags and notes. Comment-only -- no behavior change.

* fix(token): demo mints supply to holder, not the issuer

The V2 demo minted the initial supply to the issuer party, which the
self-mint guard added in this branch rejects (issuer == receiver) — so
token demo failed every time on a tokens-v2 instance. Mint the supply to
a distinct holder party instead, which both satisfies the guard and lands
a transferable balance in one step, dropping the redundant issuer->holder
faucet leg. The holder is now intrinsic to the V2 demo, so --seed-holder
is removed.

* fix(token): auto-resolve ledger endpoint for mint/burn/transfer/accept

mint and burn only took the live path when --endpoint was passed
explicitly; otherwise they fell through to ErrUnsupportedOnInstrument,
which also meant the self-mint guard (inside runMintLive) never ran. A
self-mint attempt without --endpoint therefore surfaced a misleading
'instrument doesn't implement mint/burn' error instead of the actionable
guard message. transfer/accept similarly returned ErrNeedsV2LocalNet on a
live instance when --endpoint was omitted.

Resolve the endpoint from the instance's captured participant_ledger_<role>
port (as RunBalance already does) in the shared action layer, so both the
CLI and Web UI behave consistently and these commands work flag-free on a
running LocalNet. Explicit --endpoint still wins; instance-down still falls
back to the correct remediation. Align the CLI --endpoint help text with
balances/balance.

* fix(token): allocate exercises the issuer's on-ledger AllocationFactory

`token allocate` posted the scan registry's allocation-factory endpoint,
which returns the network-default (DSO/Amulet) factory, then exercised
AllocationFactory_Allocate against it. For an issuer-created instrument
(token create) that fails on-ledger with

  AssertionFailed: The requirement 'Instrument-id must match the factory'

because the DAML impl asserts allocation.admin == tokenRules.admin and
the DSO factory's admin is not the issuer.

The issuer's own on-ledger TokenRules contract IS the V2 AllocationFactory
(interface instance V2.AllocationFactory for TokenRules), with admin ==
the instrument admin — exactly as it is the TransferFactory the on-ledger
transfer path already uses. Resolve TokenRules via findTokenRules(admin)
and exercise AllocationFactory_Allocate against it, building the choice
context locally (TokenRules + authorizer AccountConfig cids) and acting as
the authorizer's account parties plus the admin. The authorizer Account is
taken from the picked holdings' own account so it satisfies the impl's
inputHolding.account == allocation.authorizer check (self-custodial and
provider-scoped).

Verified live on a token-standard-v2 LocalNet: the previously-failing
allocate now finalizes an Allocation (committed and non-committed), which
`token allocations` lists. Scope limited to allocate; withdraw/cancel/
settle unchanged.

* fix(token): withdraw/cancel exercise the issuer's on-ledger Allocation

Allocation_Withdraw / Allocation_Cancel were malformed: the choice
argument omitted the required `actors : [Party]` controller field
(AllocationV2.daml), and the choice context was fetched from the scan
registry. The TestToken impl reads the local test-token context from
extraArgs (unlockTokenAllocationV2 -> getEventLogFromContext,
applyAllocationTransitions -> extractAccountConfigMap), so the registry
blob was rejected — the same root cause as the allocate fix.

Rework runAllocationAction to mirror the allocate factory path: fetch the
target Allocation's view to resolve the admin / authorizer account /
executors, resolve the issuer's on-ledger TokenRules (event-log +
AccountConfig source), build the local choice context, and exercise with
the controller `actors` the state machine requires (AccountConfig.daml):
withdraw -> the authorizer's account parties, cancel -> the executors.
Act as those actors plus the admin (co-signs the unlocked holdings).

Extend the Allocation view walker to surface the authorizer account
provider/id and the settlement executors. Drop the now-unused registry
withdraw/cancel choice-context paths.

Live-verified on a token-standard-v2 LocalNet: fresh allocate -> withdraw
and allocate -> cancel each consume the Allocation; a committed
allocation correctly refuses early withdrawal.

* fix(token): address PR review — activity feed correctness, DvP surface, atomic flag

Review fixes for the token V2 tightening PR:

- activity: keep the newest maxActivityScan events via a sliding ring
  buffer instead of breaking at the first cap (which returned the oldest
  slice). Applies to both the EventLog and netting paths.
- activity: dedup paired sender/receiver EventLog_HoldingsChange exercises
  by (updateID, sorted transferLegIds) so a transfer is not double-counted.
- activity: fall through to netting when the EventLog path fails in a
  fallback-safe way (stream open / malformed event); only a cancelled or
  expired context aborts.
- allocations: emit the shared types.AllocationsResponse from both the CLI
  JSON and the Web UI handler so the two surfaces cannot drift.
- allocations: disable the settle action (CLI, HTTP route, Web UI) until
  SettlementFactory_SettleBatch is functional on LocalNet; keep
  withdraw/cancel.
- ui: thread the active role through the allocations/transfer/allocate
  flows; add an experimental atomic-transfer control gated on auto-accept.
- docs: record the identity/allocations commands, the experimental
  --atomic flag, and the deferred settle verb in changes-from-proposal.md.

Adds tests for newest-first truncation (both paths), paired-side dedup,
and EventLog→netting fallback on stream error vs. context cancellation.

---------

Co-authored-by: Zhe Li <linuxcity.jn@gmail.com>
…284)

Keep the root README short (quick start, features, thin install) and
point users at https://bitdynamics-ab.github.io/canton-devkit/ as the
main website; rename website/README.md to DEVELOPMENT.md for Astro
maintainer notes.
* docs: restore command reference table in README

Re-add the area/commands table after Quick start so GitHub readers can
scan the full localnet surface without opening the docs site.

* docs: keep feature areas in README without listing commands

Restore lifecycle, preflight, app wiring, DAR, ledger, and snapshot
bullets in Features; leave specific verbs in the Commands table only.

* docs: simplify CLI/UI and observability feature bullets

Drop command-flag detail from Features; the Commands table already
covers the surface.
* feat(frontend): add Vite mock API for UI-only development

Introduce a mock API layer under frontend/mock/ with fixtures, in-memory
store, SSE hub, and route handlers so the Web UI can run via `npm run dev:mock`
without the Go backend or a LocalNet instance. Wire the plugin into Vite,
document mock mode in CONTRIBUTING.md, and add router/seed tests.

* fix(frontend): silence unused param in mock router

* docs: restructure Web UI dev setup into real vs mock paths

* fix(frontend): sync package-lock.json after adding tsx

npm ci failed in CI because the lockfile was out of sync with
package.json after the mock:seed script added tsx as a devDependency.

* fix(frontend): include Linux optional deps in package-lock.json

npm ci failed on the self-hosted Linux CI runner because the lockfile
was generated on macOS and omitted @emnapi optional peer entries that
Linux npm ci requires.

* Update mock data
* fix(env): export raw LocalNet JWTs by default

* fix(status): expose raw LocalNet JWTs

* fix(cli): remove env JWT redaction opt-in

* fix(cli): remove status JWT redaction flag

* fix(creds): show JWTs in default table output

* docs(cli): describe raw JWTs in creds table

* fix(ui): return raw JWTs from LocalNet auth APIs

* fix(ui): expose raw JWTs in instance details

* fix(ui): make JWT API responses raw by default

* fix(ui): remove JWT reveal query opt-in

* test(ui): keep mock LocalNet JWTs usable

* test(env): pin raw JWT export default

* test(cli): require raw JWTs without env opt-in

* test(status): require raw JWTs by default

* test(creds): require JWTs in default table

* test(ui): require raw JWT responses by default

* test(ui): remove JWT query opt-in expectations

* test(ui): pin raw JWT developer setup

* docs(ui): remove obsolete JWT query reference

* docs(test): clarify raw JWT API contract

* test(ui): use raw mock JWT in dashboard

* test(mock): provide raw fixture JWTs

* test(mock): update LocalNet fixture JWTs

* docs(ui): remove obsolete JWT query logging reference

* test(ui): update query privacy comment

* docs(ui): update credential projection comment

* docs: record raw LocalNet JWT defaults

* test(ui): seed raw JWT for app config coverage

* style: format JWT output changes

* docs(test): remove stale redaction wording

* docs(test): update env JWT expectation

* feat(creds): restore format hint on creds table

The raw-JWT table now wraps in a standard terminal because it appends the
full token as a trailing column. Restore a footer pointing users to the
env/raw formats, which emit clean single-line copy-pasteable output.
Foundation for integrating Certora's daml-analyzer (Apache-2.0), a static
analyzer for cross-package interactions in a compiled .dar. internal/analyzer
locates a Java runtime + the vendored jar, runs it, and maps the analyzer's
JSON onto schema-pinned internal/api/types. The 60MB fat jar is vendored under
third_party/ (gitignored here pending the commit-vs-build decision); runtime
discovers it by path or DAML_ANALYZER_JAR.
… jar)

Package Certora's daml-analyzer as a reproducible container built from a
pinned upstream commit, invoked via docker run with the .dar mounted
read-only and JSON read from stdout. This drops the 60MB in-git jar and the
host Java requirement: the image lives in a registry, pulled on demand. Adds
build/daml-analyzer/ (multi-stage Dockerfile + Apache-2.0 LICENSE + README), a
manual GHCR publish workflow, and make analyzer-image/analyzer-push. internal/
analyzer now discovers Docker + the image (DAML_ANALYZER_IMAGE override); the
report types, parsing, and API are unchanged.
Web UI surface (MountAnalyzer): GET /api/analyzer/status, POST /api/analyzer/
analyze (upload a .dar), and GET /api/instances/{name}/analyzer/{id} (analyze a
deployed DAR by fetching its bytes via the participant admin GetDar). Docker/
image-unavailable maps to a 503 ANALYZER_UNAVAILABLE the UI can render cleanly.

CLI: `dpm localnet dar analyze <dar> [--format text|json]` runs the same
analyzer on a local .dar. Both surfaces go through internal/analyzer, so their
reports are identical (CLI ↔ Web UI parity). Tests drive the real image on a
committed sample DAR and skip when DAML_ANALYZER_IMAGE is unset.
New top-level Analyzer screen: analyze a DAR deployed to the selected
instance (pick from its DAR list) or upload a .dar ad hoc, then render the
analyzed package, dependency count, by-type summary, and the caller→target
cross-package interaction table (consuming choices flagged). Gated on the
analyzer being available (Docker + image), with a clean not-configured
notice. Wires the nav entry, route, IcAnalyzer icon, and api.ts helpers/
types mirroring internal/api/types/analyzer.go.
Certora now publishes daml-analyzer as a DPM component
(oci://ghcr.io/certora/daml-analyzer). Resolve the runtime in priority
order: DAML_ANALYZER_BIN, then the DPM-installed component, then the
pinned Docker image.

The devkit execs the component's wrapper straight from the DPM cache
rather than shelling out to `dpm certora-analyze`, because that
subcommand only resolves inside a project directory while the devkit runs
from anywhere. With the component installed, analysis needs no Docker and
nothing vendored — but the component wraps a jar, so a JVM is still
required; Status() reports that up front instead of failing mid-analysis.

Status is now runtime-agnostic (runtime + source replace the docker/image
fields), mirrored in api.ts and the Analyzer tab, which shows the resolved
runtime and points at the component install when nothing is available.
Brings the tab up to what the analyzer's own viewer offers, over the same
report data:

- Highlights: derived findings — consuming exercises, interface
  implementations, cross-package creates, most-reached package, and
  interactions missing source info. Clicking one jumps to the filtered list.
- Summary: the target-package x interaction-type pivot with per-column and
  grand totals, a package filter box, and click-through (row = package,
  cell = package + type) into the interaction list.
- Interactions: adds the source file:line column, which the report already
  carried but the table dropped, plus an active-filter chip.
- Diff: compares two loaded reports by interaction identity and shows what
  a version added, removed, or kept.
- Multi-DAR: the upload accepts several .dar files and analyses them in
  sequence; loaded reports stay selectable, which is what makes diffing
  possible. Each report can be exported as JSON.

Diff and multi-report state live in the screen, so no new endpoints are
needed — the backend still analyses one DAR per call.
@zheli
zheli marked this pull request as draft August 7, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants