feat: add operator CLI example and fix local two-stack observability setup - #385
feat: add operator CLI example and fix local two-stack observability setup#385stevep0z wants to merge 2 commits into
Conversation
WalkthroughThe PR configures authenticated server metrics for Prometheus and adds a TypeScript Guardian operator CLI that persists a Falcon key, authenticates, and lists account statuses. ChangesMetrics observability setup
Guardian operator CLI
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant OperatorCLI
participant MidenSDK
participant Guardian
OperatorCLI->>Guardian: Request authentication challenge
Guardian-->>OperatorCLI: Return challenge
OperatorCLI->>MidenSDK: Deserialize Falcon key and sign challenge
MidenSDK-->>OperatorCLI: Return signature
OperatorCLI->>Guardian: Verify operator signature with session cookies
Guardian-->>OperatorCLI: Establish authenticated session
OperatorCLI->>Guardian: Request up to 50 accounts
Guardian-->>OperatorCLI: Return account statuses and pause reasons
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/guides/observability/docker-compose.yml`:
- Line 74: Update the Grafana URL references in the surrounding Docker Compose
documentation from port 3001 to port 3002, matching the 127.0.0.1:3002:3000
binding while leaving unrelated port references unchanged.
In `@docs/guides/observability/prometheus/prometheus.yml`:
- Line 14: Update the Prometheus scrape target in the targets configuration to
use server:9464 instead of host.docker.internal:9464, matching the containerized
Compose topology.
In `@examples/operator-cli/list-accounts.ts`:
- Around line 102-105: Update the account listing flow around
client.listAccounts and totalLabel so it does not present the first page count
as the total. Either iterate through every nextCursor and aggregate all accounts
before logging, or clearly label the current count as a partial page when
pagination remains.
- Around line 57-60: Update the key-file creation in the first-run branch of the
surrounding account-listing flow to explicitly set restrictive owner-only
permissions when writing the serialized secret key. Ensure operator-key.bin is
not readable or writable by other users, while preserving the existing key
generation and persistence behavior.
- Around line 4-7: Update the header documentation for the first-run and
subsequent-run flows in the operator CLI example to consistently reference
operator-key.bin, matching the filename used by the CLI and its .gitignore
configuration.
- Line 65: Update the operator entry generated by the listAccounts CLI to grant
only read access; remove the accounts:pause permission from the permissions
array in the JSON.stringify call unless this CLI also adds a pause command.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6c64213a-b09f-455d-995c-7d1803c3c94f
📒 Files selected for processing (7)
docs/guides/miden-dashboard/docker-compose.ymldocs/guides/observability/docker-compose.ymldocs/guides/observability/prometheus/prometheus.ymlexamples/operator-cli/.gitignoreexamples/operator-cli/list-accounts.tsexamples/operator-cli/package.jsonexamples/operator-cli/tsconfig.json
| ports: | ||
| # 3001 avoids clashing with the server's HTTP port 3000. | ||
| - "127.0.0.1:3001:3000" | ||
| - "127.0.0.1:3002:3000" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Update the Grafana port references.
Grafana now binds to 127.0.0.1:3002:3000, but Lines 73 and 80 still refer to port 3001. Users following the instructions will open the wrong URL. Change those references to 3002.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/guides/observability/docker-compose.yml` at line 74, Update the Grafana
URL references in the surrounding Docker Compose documentation from port 3001 to
port 3002, matching the 127.0.0.1:3002:3000 binding while leaving unrelated port
references unchanged.
| # this at `host.docker.internal:9464` and add an `extra_hosts` mapping | ||
| # for it on this service. | ||
| - targets: ["server:9464"] | ||
| - targets: ["host.docker.internal:9464"] |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 4 \
'9464|host\.docker\.internal|extra_hosts|ports:' \
docs/guides/miden-dashboard/docker-compose.yml \
docs/guides/observability/docker-compose.yml \
docs/guides/observability/prometheus/prometheus.ymlRepository: OpenZeppelin/guardian
Length of output: 7961
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,120p' docs/guides/observability/docker-compose.yml
printf '\n--- prometheus config ---\n'
cat -n docs/guides/observability/prometheus/prometheus.yml
printf '\n--- miden dashboard relevant sections ---\n'
sed -n '1,110p' docs/guides/miden-dashboard/docker-compose.ymlRepository: OpenZeppelin/guardian
Length of output: 7810
🌐 Web query:
Docker Compose host-gateway extra_hosts host.docker.internal container network reach host container published port
💡 Result:
To access the host machine or services running on it from a container managed by Docker Compose, use the extra_hosts directive with the special host-gateway value. This configuration maps the hostname host.docker.internal to the host's internal IP address [1][2][3]. Configuration Add the following to your compose.yaml file under the desired service: services: your-service: #... other configuration extra_hosts: - "host.docker.internal:host-gateway" How it works 1. DNS Mapping: The extra_hosts directive adds an entry to the container's /etc/hosts file (e.g., 172.17.0.1 host.docker.internal), ensuring the hostname resolves correctly [4][2][3]. 2. host-gateway: On Linux, host-gateway dynamically resolves to the host's IP address on the default bridge network [1][3]. On Docker Desktop (macOS and Windows), host.docker.internal is already provided, making this configuration harmless and recommended for cross-platform compatibility [1][5][3]. 3. Connectivity: Once configured, applications inside the container can reach services running on the host by connecting to http://host.docker.internal:<port> [4][5]. Important Notes - Host Firewall: Ensure that the service on your host machine is configured to listen on the correct interface (e.g., 0.0.0.0 or the Docker bridge IP, not just 127.0.0.1) and that your host firewall (e.g., ufw, iptables) allows incoming connections from the Docker bridge network [1][6]. - Build Time: Historically, host-gateway does not resolve during the image build process (build: section of Compose). If you need to reach the host during a docker build, you must use build-time arguments or specific buildx flags instead [3].
Citations:
- 1: https://docs.docker.com/compose/how-tos/networking/
- 2: https://nickjanetakis.com/blog/connect-to-a-service-running-on-your-docker-host-from-a-container
- 3: https://www.dash0.com/faq/how-to-access-a-host-port-from-inside-a-docker-container
- 4: https://www.baeldung.com/ops/docker-compose-add-host
- 5: https://devops-daily.com/posts/connect-to-host-localhost-from-docker
- 6: https://stackoverflow.com/questions/31324981/how-to-access-host-port-from-docker-container
🌐 Web query:
Docker host-gateway host.docker.internal can container reach service outside Docker network Linux firewall docker0
💡 Result:
On Linux, you can enable the use of host.docker.internal to reach the host machine by adding the flag --add-host=host.docker.internal:host-gateway to your docker run command [1][2][3], or by adding extra_hosts: - "host.docker.internal:host-gateway" to your service definition in a docker-compose.yml file [4][2][5]. This feature is available in Docker Engine 20.10 and later [2][3]. Even when this is configured, there are two common reasons why a container might fail to reach a service running on the host: 1. Service Binding: The service on the host must be configured to listen on the host's IP address (e.g., the IP of the docker0 interface, often 172.17.0.1) or on all interfaces (0.0.0.0) [1][2]. If the service is bound only to the local loopback interface (127.0.0.1), it will not accept connections from the Docker network, as the container's loopback address is distinct from the host's loopback address [4][6]. 2. Firewall Configuration: Linux host firewalls (such as UFW or iptables) often restrict traffic between the Docker bridge (e.g., docker0) and the host [1][4]. Because traffic from a container enters the host via the bridge interface, the firewall may block these incoming connections if rules are not explicitly set to permit them [1]. Troubleshooting often involves temporarily disabling the firewall to verify connectivity [1] or adjusting net.bridge.bridge-nf-call-iptables sysctl settings if traffic is being dropped by bridge-level rules [4]. Alternatively, for situations where you need full access to the host's network namespace, you can run the container with the host networking driver using the --network host flag, which allows the container to share the host's network stack directly (including access to services bound to localhost) [5][7].
Citations:
- 1: Cannot fetch host.docker.internal from container docker/for-linux#1536
- 2: https://www.baeldung.com/ops/docker-compose-add-host
- 3: https://nickjanetakis.com/blog/connect-to-a-service-running-on-your-docker-host-from-a-container
- 4: https://stackoverflow.com/questions/65496504/docker-compose-host-gateway-on-linux-cannot-connect-to-rpc-v20-10-1
- 5: https://stackoverflow.com/questions/31324981/how-to-access-host-port-from-docker-container
- 6: https://stackoverflow.com/questions/76667364/docker-cannot-access-service-running-at-localhost-from-container-in-linux
- 7: https://docs.docker.com/engine/network/drivers/host/
🌐 Web query:
Docker Compose host-gateway port forwarding 127.0.0.1 published localhost inside container
💡 Result:
To access a service running on your host machine from inside a Docker Compose container, you must navigate the fact that localhost inside a container refers to the container itself, not the host machine [1][2][3]. The standard, recommended approach for enabling cross-platform host access is using the host-gateway feature [4][5]. Configuration in Docker Compose You can enable this by adding an extra_hosts entry to your service definition in your docker-compose.yml file [6][4]: services: app: image: myapp:latest extra_hosts: - "host.docker.internal:host-gateway" How it works 1. The host-gateway value is a special instruction that tells Docker to resolve the hostname (host.docker.internal) to the host machine's internal IP address on the default bridge network [7][8]. 2. When this entry is present, the container's /etc/hosts file is updated to map host.docker.internal to that IP [4][5][2]. 3. Applications inside the container can then connect to services on the host using the address http://host.docker.internal:[PORT] instead of localhost [9][10][1]. Important requirements * Host Service Binding: For the container to successfully connect, the service on the host must be listening on all interfaces (0.0.0.0) or specifically on the IP address assigned to the Docker bridge network [9][11]. If the host service is strictly bound to 127.0.0.1, it may reject connections coming from the container's IP [9]. * Compatibility: This method works on Linux (Docker Engine 20.10+) and is automatically available on Docker Desktop (Mac and Windows), where host.docker.internal is provided by default [6][10][11]. Including the extra_hosts configuration is considered a best practice for portability, as it is harmless on systems that already provide it and necessary on Linux [8][1]. * Alternative (Host Networking): If you prefer to bypass Docker networking entirely, you can use network_mode: host [12][1][2]. This allows the container to share the host's network namespace, meaning localhost inside the container will then correctly refer to the host's localhost [10][2]. Note that this has significant implications for network isolation and is typically reserved for specialized use cases [12][2].
Citations:
- 1: https://www.dash0.com/faq/how-to-access-a-host-port-from-inside-a-docker-container
- 2: https://devops-daily.com/posts/connect-to-host-localhost-from-docker
- 3: https://collabnix.com/how-to-reach-localhost-on-host-from-docker-container/
- 4: https://www.baeldung.com/ops/docker-compose-add-host
- 5: https://nickjanetakis.com/blog/connect-to-a-service-running-on-your-docker-host-from-a-container
- 6: https://stackoverflow.com/questions/70725881/what-is-the-equivalent-of-add-host-host-docker-internalhost-gateway-in-a-comp
- 7: https://docs.docker.com/compose/how-tos/networking/
- 8: https://oneuptime.com/blog/post/2026-02-08-how-to-use-docker-compose-extrahosts-configuration/view
- 9: https://stackoverflow.com/questions/31324981/how-to-access-host-port-from-docker-container
- 10: https://stackoverflow.com/questions/24319662/from-inside-of-a-docker-container-how-do-i-connect-to-the-localhost-of-the-mach
- 11: https://eastondev.com/blog/en/posts/dev/20251217-docker-host-access/
- 12: https://docs.docker.com/compose/how-tos/networking
🌐 Web query:
Docker Compose port publish 127.0.0.1:3000:3000 host-gateway container port
💡 Result:
To publish a container port only to the host's loopback interface (localhost) using Docker Compose, you must specify the IP address in the ports mapping configuration [1][2][3]. Use the following syntax in your docker-compose.yml file: services: your-service: ports: - "127.0.0.1:3000:3000" In this configuration, the first 3000 is the port on your host machine, and the second 3000 is the port inside the container [1][4]. By prefixing the host port with 127.0.0.1, you restrict access so that only the host machine can connect to the container service; it will not be reachable from other machines on your network [2][5]. If you additionally need your container to connect back to the host machine (e.g., to access a database or service running directly on your host), you can use the extra_hosts configuration with the special host-gateway value [6][7][8]. This resolves a hostname to the host's IP address: services: your-service: extra_hosts: - "host.docker.internal:host-gateway" This adds an entry to the container's /etc/hosts file (typically 172.17.0.1 host.docker.internal), allowing your application to reach services on the host by connecting to host.docker.internal [8][9]. It is important to note that if you omit the 127.0.0.1 prefix (e.g., using only "3000:3000"), Docker binds to all interfaces (0.0.0.0), which exposes the service to your local network and potentially the internet if your host has a public IP address [1][2][3]. Explicitly pinning 127.0.0.1 is a recommended security best practice for local development [2][10].
Citations:
- 1: https://docs.docker.com/reference/compose-file/services
- 2: https://www.local-environment-automation.com/containerized-local-environments-docker-compose-patterns/local-network-port-mapping/
- 3: https://brokkr.net/2022/03/29/publishing-docker-ports-to-127-0-0-1-instead-of-0-0-0-0/
- 4: https://oneuptime.com/blog/post/2026-02-08-how-to-use-docker-compose-ports-long-syntax/view
- 5: https://docs.docker.com/engine/network/port-publishing/
- 6: https://docs.docker.com/compose/how-tos/networking/
- 7: https://stackoverflow.com/questions/70725881/what-is-the-equivalent-of-add-host-host-docker-internalhost-gateway-in-a-comp
- 8: https://www.baeldung.com/ops/docker-compose-add-host
- 9: https://nickjanetakis.com/blog/connect-to-a-service-running-on-your-docker-host-from-a-container
- 10: https://vccolombo.github.io/blog/how-to-stop-docker-exposing-your-containers-to-the-world/
Use server:9464 for the server in this Compose stack.
docs/guides/observability/docker-compose.yml does not publish port 9464, so Prometheus must scrape the server container directly. host.docker.internal:9464 only works for a guardian server running directly on the host, not for the containerized topology described here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/guides/observability/prometheus/prometheus.yml` at line 14, Update the
Prometheus scrape target in the targets configuration to use server:9464 instead
of host.docker.internal:9464, matching the containerized Compose topology.
| * First run (no key file): generates a Falcon keypair, writes the secret key | ||
| * to ./operator-key.txt, prints the operators.json entry, and exits. | ||
| * | ||
| * Subsequent runs: reads the key from ./operator-key.txt and authenticates. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the key file name in the header.
Lines 5 and 7 specify operator-key.txt. The CLI and examples/operator-cli/.gitignore use operator-key.bin. A user who creates the documented file will cause an unexpected new key generation.
Proposed fix
- * to ./operator-key.txt, prints the operators.json entry, and exits.
+ * to ./operator-key.bin, prints the operators.json entry, and exits.
...
- * Subsequent runs: reads the key from ./operator-key.txt and authenticates.
+ * Subsequent runs: reads the key from ./operator-key.bin and authenticates.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| * First run (no key file): generates a Falcon keypair, writes the secret key | |
| * to ./operator-key.txt, prints the operators.json entry, and exits. | |
| * | |
| * Subsequent runs: reads the key from ./operator-key.txt and authenticates. | |
| * First run (no key file): generates a Falcon keypair, writes the secret key | |
| * to ./operator-key.bin, prints the operators.json entry, and exits. | |
| * | |
| * Subsequent runs: reads the key from ./operator-key.bin and authenticates. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/operator-cli/list-accounts.ts` around lines 4 - 7, Update the header
documentation for the first-run and subsequent-run flows in the operator CLI
example to consistently reference operator-key.bin, matching the filename used
by the CLI and its .gitignore configuration.
| if (!existsSync(KEY_FILE)) { | ||
| // First run: generate a keypair, save the secret key to a file, exit. | ||
| const secretKey = AuthSecretKey.rpoFalconWithRNG(undefined); | ||
| writeFileSync(KEY_FILE, secretKey.serialize()); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Restrict permissions on the generated secret key.
writeFileSync uses a default mode that depends on the process umask. A permissive umask can make operator-key.bin readable by other local users. This key can authenticate as the operator.
Proposed fix
- writeFileSync(KEY_FILE, secretKey.serialize());
+ writeFileSync(KEY_FILE, secretKey.serialize(), { mode: 0o600 });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!existsSync(KEY_FILE)) { | |
| // First run: generate a keypair, save the secret key to a file, exit. | |
| const secretKey = AuthSecretKey.rpoFalconWithRNG(undefined); | |
| writeFileSync(KEY_FILE, secretKey.serialize()); | |
| if (!existsSync(KEY_FILE)) { | |
| // First run: generate a keypair, save the secret key to a file, exit. | |
| const secretKey = AuthSecretKey.rpoFalconWithRNG(undefined); | |
| writeFileSync(KEY_FILE, secretKey.serialize(), { mode: 0o600 }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/operator-cli/list-accounts.ts` around lines 57 - 60, Update the
key-file creation in the first-run branch of the surrounding account-listing
flow to explicitly set restrictive owner-only permissions when writing the
serialized secret key. Ensure operator-key.bin is not readable or writable by
other users, while preserving the existing key generation and persistence
behavior.
| const commitment = secretKey.publicKey().toCommitment().toHex(); | ||
| const pubKeyHex = '0x' + bytesToHex(secretKey.publicKey().serialize().slice(1)); | ||
|
|
||
| const entry = JSON.stringify({ public_key: pubKeyHex, permissions: ['dashboard:read', 'accounts:pause'] }, null, 2); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not grant accounts:pause to this read-only CLI.
Line 65 generates an operator entry with mutation permission, but this CLI only calls listAccounts. Remove accounts:pause unless this example also implements an account pause command.
Proposed fix
- const entry = JSON.stringify({ public_key: pubKeyHex, permissions: ['dashboard:read', 'accounts:pause'] }, null, 2);
+ const entry = JSON.stringify({ public_key: pubKeyHex, permissions: ['dashboard:read'] }, null, 2);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const entry = JSON.stringify({ public_key: pubKeyHex, permissions: ['dashboard:read', 'accounts:pause'] }, null, 2); | |
| const entry = JSON.stringify({ public_key: pubKeyHex, permissions: ['dashboard:read'] }, null, 2); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/operator-cli/list-accounts.ts` at line 65, Update the operator entry
generated by the listAccounts CLI to grant only read access; remove the
accounts:pause permission from the permissions array in the JSON.stringify call
unless this CLI also adds a pause command.
| const { items, nextCursor } = await client.listAccounts({ limit: 50 }); | ||
| const totalLabel = `${items.length}${nextCursor ? '+' : ''}`; | ||
|
|
||
| console.log(`Accounts on ${GUARDIAN_URL} (${totalLabel} total)`); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not label the first page as the total.
nextCursor means another page exists, but the CLI only renders the first 50 accounts. Either follow the cursor until completion or label this output as a partial page.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/operator-cli/list-accounts.ts` around lines 102 - 105, Update the
account listing flow around client.listAccounts and totalLabel so it does not
present the first page count as the total. Either iterate through every
nextCursor and aggregate all accounts before logging, or clearly label the
current count as a partial page when pagination remains.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Adds a minimal Node.js CLI script under
examples/operator-cli/that demonstrates the full operator authentication flow and account listing using@openzeppelin/guardian-operator-client.On first run it generates a Falcon keypair and prints the
operators.jsonentry. On subsequent runs it authenticates via the challenge-response flow and lists all accounts on the target Guardian instance. TheGUARDIAN_URLenv var points it at any instance.Also fixes three config issues that prevented the local two-stack setup (dashboard + observability running simultaneously) from working correctly:
docs/guides/miden-dashboard/docker-compose.yml: expose metrics port and addGUARDIAN_METRICS_*env vars so Prometheus has something to scrapedocs/guides/observability/docker-compose.yml: move Grafana to port 3002 to avoid clash with the dashboard on 3001, addextra_hostsso Prometheus can reach Guardian by hostnamedocs/guides/observability/prometheus/prometheus.yml: change scrape target fromserver:9464tohost.docker.internal:9464to match the separate-network topologySummary by CodeRabbit
New Features
Documentation