Skip to content

fix(onboard): propagate rotated messaging credentials to sandbox L7 proxy#1967

Merged
ericksoa merged 16 commits into
mainfrom
fix/credential-rotation-propagation
Apr 16, 2026
Merged

fix(onboard): propagate rotated messaging credentials to sandbox L7 proxy#1967
ericksoa merged 16 commits into
mainfrom
fix/credential-rotation-propagation

Conversation

@ericksoa

@ericksoa ericksoa commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fixes credential rotation bug where re-running nemoclaw onboard --non-interactive with a new messaging token (e.g. TELEGRAM_BOT_TOKEN) reports success but the sandbox continues using the old credential
  • Stores SHA-256 hashes of messaging credentials in the sandbox registry at creation time; on reuse, compares current tokens against stored hashes to detect rotation
  • When rotation is detected, automatically rebuilds the sandbox (backup → delete → create with new providers → restore), preserving workspace state

Closes #1903

Changes

File Change
src/lib/registry.ts Add providerCredentialHashes field to SandboxEntry
src/lib/onboard.ts Add hashCredential(), detectMessagingCredentialRotation(), modify reuse path to detect rotation and trigger rebuild with backup/restore, store hashes at registration
test/credential-rotation.test.ts 11 unit tests for hash and rotation detection logic
test/e2e/test-token-rotation.sh E2E test: onboard with token A → rotate to token B (verify rebuild + workspace preservation) → re-onboard with same token B (verify reuse)

Design Notes

  • Legacy sandboxes (created before this fix) have no stored hashes → detectMessagingCredentialRotation returns changed: false (conservative, no false rebuilds)
  • Workspace preservation uses existing sandbox-state.ts backup/restore machinery (same as sandboxRebuild)

Test plan

  • npx vitest run test/credential-rotation.test.ts — 11/11 pass
  • npx vitest run test/onboard.test.ts — 117/117 existing tests pass (no regressions)
  • Full test suite — 1583/1583 tests pass
  • tsc -p tsconfig.cli.json --noEmit — clean
  • All pre-commit and pre-push hooks pass
  • E2E: test/e2e/test-token-rotation.sh with real Telegram tokens

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Detects messaging credential rotation (e.g., Telegram tokens), persists per-credential hashes, and triggers sandbox rebuilds when rotation is found
    • Attempts workspace backup before rebuild and restores workspace state after recreation; aborts rebuild if backup/restore cannot proceed safely
  • Tests

    • Added unit tests for hashing and rotation detection
    • Added end-to-end test validating token rotation, rebuild behavior, and workspace preservation
  • Chores

    • Nightly E2E workflow updated to run the token-rotation job

…roxy

When a user re-runs `nemoclaw onboard` with a rotated Telegram bot token,
the provider update succeeds at the gateway level but the running sandbox's
L7 proxy continues using the old credential. OpenShell v0.0.26 only attaches
providers at `sandbox create` time and has no hot-reload mechanism.

Fix: store SHA-256 hashes of messaging credentials in the sandbox registry
at creation time. On reuse, compare current tokens against stored hashes.
When rotation is detected, automatically rebuild the sandbox (backup →
delete → create with new providers → restore) to propagate the new
credential through the L7 proxy.

Closes #1903
Ref: nvbug 6083165

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e08c72ad-bc86-4329-9657-0ed30c23cd56

📥 Commits

Reviewing files that changed from the base of the PR and between 80c804e and 9a098da.

📒 Files selected for processing (1)
  • src/lib/onboard.ts

📝 Walkthrough

Walkthrough

Adds SHA‑256 credential hashing and rotation detection; blocks sandbox reuse on messaging-credential changes; performs workspace backup/restore around credential-triggered sandbox rebuilds; persists per‑provider credential hashes in the sandbox registry; adds unit and E2E tests and a nightly CI job for token-rotation E2E.

Changes

Cohort / File(s) Summary
Onboarding logic & exports
src/lib/onboard.ts
Added and exported hashCredential() and detectMessagingCredentialRotation(); integrated rotation detection into createSandbox flow to block reuse when messaging tokens changed; implemented backup/restore around rotation-driven sandbox rebuild; persist/update provider credential hashes after registration.
Registry types & persistence
src/lib/registry.ts
Extended exported SandboxEntry with optional providerCredentialHashes?: Record<string,string> and updated registerSandbox() to persist this field when provided.
Unit tests
test/credential-rotation.test.ts
New Vitest suite exercising hashCredential and detectMessagingCredentialRotation across legacy/no-hashes, matching, differing, multi-provider, null-token, and missing-sandbox cases using spies/mocks.
E2E script
test/e2e/test-token-rotation.sh
New Bash E2E that runs three onboarding phases (initial token A, rotate to token B expecting rebuild + restore, re-onboard expecting reuse); includes setup, teardown, log capture and runtime checks for provider/sandbox and stored providerCredentialHashes.
CI workflow
.github/workflows/nightly-e2e.yaml
Added token-rotation-e2e job to nightly E2E, wired it into notify-on-failure.needs and failure condition; uploads install log on failure.

Sequence Diagram

sequenceDiagram
    participant User
    participant Onboard as Onboard Process
    participant Registry
    participant State as WorkspaceState
    participant Sandbox

    User->>Onboard: run onboard (with messaging tokens)
    Onboard->>Registry: get sandbox entry
    Registry-->>Onboard: sandbox entry (may include providerCredentialHashes)
    Onboard->>Onboard: hash current tokens, compare to stored hashes
    alt rotation detected
        Onboard->>State: backup workspace
        State-->>Onboard: backup artifact (or fail)
        Onboard->>Sandbox: delete/recreate sandbox (upsert providers)
        Onboard->>Registry: register sandbox + new providerCredentialHashes
        Registry-->>Onboard: confirm registration
        Onboard->>State: restore workspace into new sandbox (if backup available)
        State-->>Sandbox: workspace restored
    else no rotation
        Onboard->>Sandbox: reuse existing sandbox
    end
    Onboard-->>User: onboarding complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I trim and hash each secret string,

if keys have hopped I drum and spring,
I pack your burrow, build anew,
then tuck your files and welcome you,
a crunchy hop — the job’s done true.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly summarizes the main change: propagating rotated messaging credentials to the sandbox L7 proxy, which is the core fix in this PR.
Linked Issues check ✅ Passed Code changes fully satisfy #1903 requirements: credential hashing/rotation detection implemented, auto-rebuild on rotation triggers provider upsert with new credentials, and comprehensive test coverage validates the token rotation workflow.
Out of Scope Changes check ✅ Passed All changes are directly scoped to credential rotation: core hashing and detection logic, registry persistence, unit tests, E2E validation, and CI integration. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/credential-rotation-propagation

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/registry.ts (1)

153-169: ⚠️ Potential issue | 🔴 Critical

Persist providerCredentialHashes when registering a sandbox.

createSandbox() now passes providerCredentialHashes, but this constructor drops the field. Every new sandbox is therefore recorded as a legacy entry, so detectMessagingCredentialRotation() never sees stored hashes and the rotation-rebuild path won't fire on the next onboard run.

Suggested fix
     data.sandboxes[entry.name] = {
       name: entry.name,
       createdAt: entry.createdAt || new Date().toISOString(),
       model: entry.model || null,
       nimContainer: entry.nimContainer || null,
       provider: entry.provider || null,
       gpuEnabled: entry.gpuEnabled || false,
       policies: entry.policies || [],
       policyTier: entry.policyTier || null,
       agent: entry.agent || null,
       dangerouslySkipPermissions:
         entry.dangerouslySkipPermissions === true ? true : undefined,
       agentVersion: entry.agentVersion || null,
+      providerCredentialHashes:
+        entry.providerCredentialHashes &&
+        Object.keys(entry.providerCredentialHashes).length > 0
+          ? { ...entry.providerCredentialHashes }
+          : undefined,
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/registry.ts` around lines 153 - 169, registerSandbox is not
persisting providerCredentialHashes from the incoming SandboxEntry so new
sandboxes are recorded without hashes and break
detectMessagingCredentialRotation; update the object assigned to
data.sandboxes[entry.name] inside registerSandbox to include
providerCredentialHashes: entry.providerCredentialHashes || null (or undefined
as appropriate) so the stored entry retains the hashes passed by createSandbox;
ensure the property name matches SandboxEntry and any downstream checks that
read providerCredentialHashes from load().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/onboard.ts`:
- Around line 2699-2715: The code calls
sandboxState.backupSandboxState(sandboxName) but only treats thrown errors; if
backup.success === false the code should abort the automatic rebuild to avoid
destroying the workspace. After calling sandboxState.backupSandboxState inside
the try, check backup.success and if false log the failure (include
backup.message or details), do not set pendingStateRestore, and abort the
rebuild flow (for example return early or throw an error) so that
credentialRotation.changed does not trigger sandbox destruction; reference the
symbols credentialRotation.changed, sandboxState.backupSandboxState,
pendingStateRestore, existingSandboxState, and sandboxName when making the
change.

In `@test/e2e/test-token-rotation.sh`:
- Around line 156-163: The exported env vars in the script (OPENAI_BASE_URL,
NEMOCLAW_INFERENCE_PROVIDER, NEMOCLAW_INFERENCE_MODEL) don't match the
non-interactive env var names consumed by the onboarding logic in
src/lib/onboard.ts, so onboarding can ignore the fake endpoint; update the
test/e2e/test-token-rotation.sh exports to set the exact non-interactive env var
names that onboard reads (replace the three ignored vars with the corresponding
env var names used in src/lib/onboard.ts) so the fake OPENAI endpoint and
provider/model are actually bound during onboarding, and remove the stale/unused
exports.
- Around line 190-223: The probe writes the marker to /tmp which is outside the
agent-backed stateDirs handled by backupSandboxState()/restoreSandboxState(),
and the test currently only logs info if the marker is missing; change the test
to write the marker into one of the backed-up state dirs (use the agent's
writable/state directory used by restoreSandboxState(), e.g. the same dir
referenced by stateDirs) instead of /tmp and then assert failure if the marker
is absent after rotation (replace the info branch that logs missing marker with
a fail call); update references to the marker variable (MARKER) and the
openshell sandbox exec calls that read the marker so they point at the backed-up
path rather than /tmp/rotation-test-marker.
- Around line 196-235: After detecting rotation in ONBOARD_OUTPUT and confirming
sandbox rebuild, add an active verification inside the sandbox using openshell
sandbox exec "$SANDBOX_NAME" to call the Telegram getMe endpoint with the old
token (token A) and the new token (token B): run getMe (or an equivalent HTTP
call) from within the sandbox to assert token A now fails and token B succeeds;
place these checks after the "Sandbox running after rotation" block and before
the Phase 3 re-onboard assertions, reusing NEMOCLAW_CMD, SANDBOX_NAME and the
existing openshell helper to locate where to run the in-sandbox commands and
perform pass/fail on their responses.

---

Outside diff comments:
In `@src/lib/registry.ts`:
- Around line 153-169: registerSandbox is not persisting
providerCredentialHashes from the incoming SandboxEntry so new sandboxes are
recorded without hashes and break detectMessagingCredentialRotation; update the
object assigned to data.sandboxes[entry.name] inside registerSandbox to include
providerCredentialHashes: entry.providerCredentialHashes || null (or undefined
as appropriate) so the stored entry retains the hashes passed by createSandbox;
ensure the property name matches SandboxEntry and any downstream checks that
read providerCredentialHashes from load().
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: da46481e-75fe-4c94-b643-43bcde0d5782

📥 Commits

Reviewing files that changed from the base of the PR and between 121148c and 8114d31.

📒 Files selected for processing (4)
  • src/lib/onboard.ts
  • src/lib/registry.ts
  • test/credential-rotation.test.ts
  • test/e2e/test-token-rotation.sh

Comment thread src/lib/onboard.ts
Comment thread test/e2e/test-token-rotation.sh Outdated
Comment thread test/e2e/test-token-rotation.sh Outdated
Comment thread test/e2e/test-token-rotation.sh Outdated
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Brev E2E (messaging-providers): FAILED on branch fix/credential-rotation-propagationSee logs

@wscurran wscurran added integration: telegram Telegram integration or channel behavior fix labels Apr 16, 2026
Wires test/e2e/test-token-rotation.sh into the nightly-e2e workflow
with fake Telegram tokens. Validates that rotating a messaging credential
and re-running onboard triggers sandbox rebuild and propagates the new
token.

Ref: #1903

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/nightly-e2e.yaml:
- Around line 587-588: The notify-on-failure aggregation is missing the
shields-config-e2e job: add "shields-config-e2e" to the needs: [...] list and
include "needs.shields-config-e2e.result == 'failure' ||" into the long if:
conditional so any failure of shields-config-e2e will trigger the
notify-on-failure job; update the existing if expression where other
needs.*.result checks appear to include this new check in the same OR chain.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: faa12449-191f-4c3a-9f00-6fb3e047720a

📥 Commits

Reviewing files that changed from the base of the PR and between d2d8135 and c93c339.

📒 Files selected for processing (1)
  • .github/workflows/nightly-e2e.yaml

Comment on lines +587 to +588
needs: [cloud-e2e, cloud-experimental-e2e, messaging-providers-e2e, token-rotation-e2e, sandbox-survival-e2e, hermes-e2e, skip-permissions-e2e, sandbox-operations-e2e, inference-routing-e2e, snapshot-commands-e2e, rebuild-openclaw-e2e, upgrade-stale-sandbox-e2e, rebuild-hermes-e2e, gpu-e2e]
if: ${{ always() && (needs.cloud-e2e.result == 'failure' || needs.cloud-experimental-e2e.result == 'failure' || needs.messaging-providers-e2e.result == 'failure' || needs.token-rotation-e2e.result == 'failure' || needs.sandbox-survival-e2e.result == 'failure' || needs.hermes-e2e.result == 'failure' || needs.skip-permissions-e2e.result == 'failure' || needs.sandbox-operations-e2e.result == 'failure' || needs.inference-routing-e2e.result == 'failure' || needs.snapshot-commands-e2e.result == 'failure' || needs.rebuild-openclaw-e2e.result == 'failure' || needs.upgrade-stale-sandbox-e2e.result == 'failure' || needs.rebuild-hermes-e2e.result == 'failure' || needs.gpu-e2e.result == 'failure') }}

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — shields-config-e2e was added in a separate PR and isn't part of this change, but I'll include it in the notify-on-failure aggregation since we're already touching that line.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericksoa, sounds good! The patch in the review comment has the exact lines ready — just drop shields-config-e2e into the needs: list and add needs.shields-config-e2e.result == 'failure' || to the if: expression at the same position (after snapshot-commands-e2e), and you'll be all set.


🧠 Learnings used
Learnt from: ericksoa
Repo: NVIDIA/NemoClaw PR: 1967
File: test/e2e/test-token-rotation.sh:0-0
Timestamp: 2026-04-16T18:34:27.427Z
Learning: In `test/e2e/test-token-rotation.sh` (NemoClaw project), the E2E test intentionally focuses only on messaging credential rotation detection and sandbox rebuild triggering. Workspace/state preservation across rotation is not tested here; it is covered by the `sandboxRebuild` unit tests and `test/e2e/test-sandbox-rebuild.sh`. Do not flag the absence of a workspace-preservation assertion in this script as a test gap.

Learnt from: ericksoa
Repo: NVIDIA/NemoClaw PR: 1849
File: src/lib/sandbox-config.ts:354-361
Timestamp: 2026-04-16T17:09:43.616Z
Learning: In `src/lib/sandbox-config.ts` (TypeScript, NemoClaw CLI), the `appendAuditEntry` call inside `configSet` intentionally uses `action: "shields_down"` rather than a dedicated `"config_set"` action. This is by design: `config set` is only permitted while shields are down, so the audit entry records the shields-down authorization context rather than the config mutation itself. Do not flag this as a wrong action type in future reviews. A dedicated `config_set` audit action type is deferred to a follow-up.

Learnt from: ericksoa
Repo: NVIDIA/NemoClaw PR: 1849
File: test/shields-audit.test.ts:29-31
Timestamp: 2026-04-16T17:09:26.055Z
Learning: In `test/shields-audit.test.ts` (NemoClaw project), the local `appendAuditEntry()` reimplementation is intentional: (1) the test's purpose is to validate JSONL format and field structure only, (2) redaction is covered separately in `test/secret-redaction.test.ts`, and (3) the CJS/ESM boundary in `src/lib/shields-audit.ts` prevents calling the real logger directly from Vitest. Do not flag the local reimplementation as a test-coverage gap in future reviews.

Learnt from: jayavenkatesh19
Repo: NVIDIA/NemoClaw PR: 839
File: .github/workflows/nightly-e2e.yaml:97-101
Timestamp: 2026-03-25T17:41:58.209Z
Learning: In this repo’s GitHub Actions workflows, it is intentional and correct to set `NEMOCLAW_RECREATE_SANDBOX=1` in a job `env:` block when the job later runs `install.sh` in `--non-interactive` mode. `install.sh --non-interactive` orchestrates `nemoclaw onboard --non-interactive`, which reads `NEMOCLAW_RECREATE_SANDBOX` from the environment; the variable is documented in `install.sh` usage/help output, so it should not be flagged as an unknown or unused env var in workflows that call `install.sh`.

registerSandbox() explicitly constructs the stored object from known
fields rather than spreading the entry. The providerCredentialHashes
field was added to the SandboxEntry interface but not to the object
literal in registerSandbox(), so hashes were silently dropped.

Also fixes the e2e test to use install.sh for proper NemoClaw
bootstrapping on bare CI runners, and fixes shellcheck warning.

Verified locally: token A onboard stores hashes, token B onboard
detects rotation and rebuilds, token B re-onboard reuses sandbox.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
test/e2e/test-token-rotation.sh (2)

202-214: ⚠️ Potential issue | 🟠 Major

Workspace preservation is not asserted after rebuild.

The PR objective includes preserving workspace state across rebuild, but this script has no marker write/read assertion around the rotation-triggered rebuild path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/test-token-rotation.sh` around lines 202 - 214, The test lacks an
assertion that workspace state is preserved across the rebuild in "Phase 3:
Re-onboard with same token", so add a marker file write/read check around the
onboarding/re-onboarding path: before the first onboard create a unique marker
file (e.g., write a GUID to /tmp or the sandbox workspace) and commit its
expected value; after the re-onboard step in the block that checks
ONBOARD_OUTPUT (both the "reusing it" branch and the else branch where rebuild
may have occurred), read the marker and assert it matches the original value
(use the same helper functions pass/fail/info), failing the test if the marker
is missing or altered; reference the variables/functions ONBOARD_OUTPUT, pass,
fail, info and the "Phase 3" section to locate where to add the marker write
before initial onboarding and the marker read/assert after the re-onboard.

10-12: ⚠️ Potential issue | 🟠 Major

This E2E still misses the core in-sandbox Telegram verification for rotation.

The script explicitly avoids Telegram API validation (Lines 10-12), and Phase 2 only checks onboarding logs and sandbox presence. That does not prove the running sandbox’s L7 proxy uses token A before rotation and token B after rotation (issue #1903 objective).

Also applies to: 170-201

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/test-token-rotation.sh` around lines 10 - 12, The E2E script
test-token-rotation.sh omits an in-sandbox verification that the sandbox L7
proxy actually uses token A before rotation and token B after rotation; add a
Phase 2 check that runs a request from inside the running sandbox (e.g.,
exec/curl into the sandbox process or run the test helper that hits the Telegram
API path /bot<TOKEN>/getMe) and assert the fake Telegram server received
requests authenticated with TOKEN_A pre-rotation and TOKEN_B post-rotation; use
the existing TOKEN_A/TOKEN_B variables and the test’s sandbox identifier to
locate the running container/process and record the token observed by the fake
Telegram endpoint, failing the test if the observed token does not match
expected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/e2e/test-token-rotation.sh`:
- Around line 161-166: The current test checks for "providerCredentialHashes"
anywhere in $REGISTRY which can false-pass; update the assertion to verify the
credential hash is stored specifically under the current sandbox key
($SANDBOX_NAME). Locate the registry check that uses REGISTRY and replace the
broad grep with a sandbox-scoped check (e.g., parse the registry JSON and assert
that the object for SANDBOX_NAME contains a providerCredentialHashes field or
grep for a pattern that ties providerCredentialHashes to the SANDBOX_NAME key)
so the test only passes if providerCredentialHashes exists for "$SANDBOX_NAME".
- Around line 175-179: The script captures ONBOARD_OUTPUT and onboard_exit but
never fails the test when onboarding returns non-zero; update the block that
sets ONBOARD_OUTPUT and onboard_exit (and the similar Phase 3 capture) to check
if onboard_exit != 0 and, if so, print the captured ONBOARD_OUTPUT for debugging
and exit the test with the same non-zero code (or call the test-failure helper
used elsewhere), ensuring a non-zero onboarding result fails the e2e test
immediately.

---

Duplicate comments:
In `@test/e2e/test-token-rotation.sh`:
- Around line 202-214: The test lacks an assertion that workspace state is
preserved across the rebuild in "Phase 3: Re-onboard with same token", so add a
marker file write/read check around the onboarding/re-onboarding path: before
the first onboard create a unique marker file (e.g., write a GUID to /tmp or the
sandbox workspace) and commit its expected value; after the re-onboard step in
the block that checks ONBOARD_OUTPUT (both the "reusing it" branch and the else
branch where rebuild may have occurred), read the marker and assert it matches
the original value (use the same helper functions pass/fail/info), failing the
test if the marker is missing or altered; reference the variables/functions
ONBOARD_OUTPUT, pass, fail, info and the "Phase 3" section to locate where to
add the marker write before initial onboarding and the marker read/assert after
the re-onboard.
- Around line 10-12: The E2E script test-token-rotation.sh omits an in-sandbox
verification that the sandbox L7 proxy actually uses token A before rotation and
token B after rotation; add a Phase 2 check that runs a request from inside the
running sandbox (e.g., exec/curl into the sandbox process or run the test helper
that hits the Telegram API path /bot<TOKEN>/getMe) and assert the fake Telegram
server received requests authenticated with TOKEN_A pre-rotation and TOKEN_B
post-rotation; use the existing TOKEN_A/TOKEN_B variables and the test’s sandbox
identifier to locate the running container/process and record the token observed
by the fake Telegram endpoint, failing the test if the observed token does not
match expected.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 383aa178-b277-429a-a854-b284f03a972e

📥 Commits

Reviewing files that changed from the base of the PR and between c93c339 and 5bf0af0.

📒 Files selected for processing (2)
  • src/lib/registry.ts
  • test/e2e/test-token-rotation.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/registry.ts

Comment thread test/e2e/test-token-rotation.sh Outdated
Comment thread test/e2e/test-token-rotation.sh
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
test/e2e/test-token-rotation.sh (3)

175-179: ⚠️ Potential issue | 🟠 Major

Fail immediately when nemoclaw onboard returns non-zero.

Line 176 captures onboard_exit but never enforces it; Phase 3 has the same gap at Line 206. A failed onboard can still produce partial output and trigger misleading pass conditions.

Suggested fix
 ONBOARD_OUTPUT=$(nemoclaw onboard --non-interactive 2>&1)
 onboard_exit=$?
 
 info "onboard exit code: $onboard_exit"
+if [ "$onboard_exit" -ne 0 ]; then
+  fail "Phase 2 onboard failed (exit $onboard_exit)"
+  echo "$ONBOARD_OUTPUT" | tail -40
+  exit "$onboard_exit"
+fi
@@
 ONBOARD_OUTPUT=$(nemoclaw onboard --non-interactive 2>&1)
+onboard_exit=$?
+if [ "$onboard_exit" -ne 0 ]; then
+  fail "Phase 3 onboard failed (exit $onboard_exit)"
+  echo "$ONBOARD_OUTPUT" | tail -40
+  exit "$onboard_exit"
+fi

Also applies to: 206-207

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/test-token-rotation.sh` around lines 175 - 179, The script captures
the output and exit code of `nemoclaw onboard` into `ONBOARD_OUTPUT` and
`onboard_exit` but does not act on non-zero exits; update the blocks that run
`nemoclaw onboard` (the lines assigning `ONBOARD_OUTPUT`/`onboard_exit` and the
Phase 3 equivalent) to immediately fail the test when `onboard_exit` is non-zero
by printing the captured `ONBOARD_OUTPUT` to stderr and calling `exit
$onboard_exit` (or `fail`/appropriate test-failure helper) so a failed onboard
cannot produce a false-positive pass.

161-166: ⚠️ Potential issue | 🟡 Minor

Scope the registry-hash assertion to "$SANDBOX_NAME" only.

Line 162 currently matches providerCredentialHashes anywhere in $REGISTRY, which can false-pass due to unrelated entries. Assert the field on the current sandbox record specifically.

Suggested fix
-# Verify credential hashes are stored in registry
-if [ -f "$REGISTRY" ] && grep -q "providerCredentialHashes" "$REGISTRY"; then
+# Verify credential hashes are stored for this sandbox entry
+if [ -f "$REGISTRY" ] && node -e '
+const fs = require("fs");
+const [registryPath, sandboxName] = process.argv.slice(1);
+const data = JSON.parse(fs.readFileSync(registryPath, "utf8"));
+const entry =
+  (data && data[sandboxName]) ||
+  (data && data.sandboxes && data.sandboxes[sandboxName]) ||
+  (Array.isArray(data) ? data.find((x) => x && x.name === sandboxName) : null);
+process.exit(entry && entry.providerCredentialHashes ? 0 : 1);
+' "$REGISTRY" "$SANDBOX_NAME"; then
   pass "Credential hashes stored in registry"
 else
   fail "Credential hashes not found in registry"
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/test-token-rotation.sh` around lines 161 - 166, The current check
searches for "providerCredentialHashes" anywhere in $REGISTRY which can
false-pass; change it to assert the field exists for the current sandbox only by
scoping to "$SANDBOX_NAME". Update the test so it queries the registry entry for
SANDBOX_NAME (e.g., using jq to test
.sandboxes["$SANDBOX_NAME"].providerCredentialHashes or by grepping the
SANDBOX_NAME block and then checking for providerCredentialHashes) and keep the
pass/fail logic around that scoped check (reference: the variables REGISTRY and
SANDBOX_NAME and the key providerCredentialHashes).

10-12: ⚠️ Potential issue | 🟠 Major

Add in-sandbox Telegram getMe assertions to validate the real regression path.

Lines 10-11 explicitly skip Telegram-response validation, but the issue objective is about the running sandbox L7 proxy using the rotated token. Log greps alone do not prove that behavior. Add active in-sandbox checks: token A should fail (4xx) before/after rotation as applicable, and token B should return 200 after rotation.

Also applies to: 196-214

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/test-token-rotation.sh` around lines 10 - 12, Update the test script
to perform active in-sandbox Telegram getMe calls instead of only grepping logs:
call the sandbox L7 proxy (use the script's existing sandbox invocation helper
or curl against the sandbox endpoint) with TOKEN_A and assert it returns a 4xx
before rotation and still fails after rotation when appropriate, then call with
TOKEN_B after rotation and assert it returns HTTP 200; reference and modify the
token variables (e.g., TOKEN_A, TOKEN_B) and the sandbox invocation helper
(e.g., the existing curl/sandbox runner used elsewhere in
test-token-rotation.sh) to add these explicit assertions around the rotation
step so the test verifies the actual proxy behavior rather than only log output.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/e2e/test-token-rotation.sh`:
- Around line 175-179: The script captures the output and exit code of `nemoclaw
onboard` into `ONBOARD_OUTPUT` and `onboard_exit` but does not act on non-zero
exits; update the blocks that run `nemoclaw onboard` (the lines assigning
`ONBOARD_OUTPUT`/`onboard_exit` and the Phase 3 equivalent) to immediately fail
the test when `onboard_exit` is non-zero by printing the captured
`ONBOARD_OUTPUT` to stderr and calling `exit $onboard_exit` (or
`fail`/appropriate test-failure helper) so a failed onboard cannot produce a
false-positive pass.
- Around line 161-166: The current check searches for "providerCredentialHashes"
anywhere in $REGISTRY which can false-pass; change it to assert the field exists
for the current sandbox only by scoping to "$SANDBOX_NAME". Update the test so
it queries the registry entry for SANDBOX_NAME (e.g., using jq to test
.sandboxes["$SANDBOX_NAME"].providerCredentialHashes or by grepping the
SANDBOX_NAME block and then checking for providerCredentialHashes) and keep the
pass/fail logic around that scoped check (reference: the variables REGISTRY and
SANDBOX_NAME and the key providerCredentialHashes).
- Around line 10-12: Update the test script to perform active in-sandbox
Telegram getMe calls instead of only grepping logs: call the sandbox L7 proxy
(use the script's existing sandbox invocation helper or curl against the sandbox
endpoint) with TOKEN_A and assert it returns a 4xx before rotation and still
fails after rotation when appropriate, then call with TOKEN_B after rotation and
assert it returns HTTP 200; reference and modify the token variables (e.g.,
TOKEN_A, TOKEN_B) and the sandbox invocation helper (e.g., the existing
curl/sandbox runner used elsewhere in test-token-rotation.sh) to add these
explicit assertions around the rotation step so the test verifies the actual
proxy behavior rather than only log output.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 57afdba5-2a54-4576-b495-92ccd32c4433

📥 Commits

Reviewing files that changed from the base of the PR and between 5bf0af0 and a09848f.

📒 Files selected for processing (2)
  • .github/workflows/nightly-e2e.yaml
  • test/e2e/test-token-rotation.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/nightly-e2e.yaml

ericksoa and others added 4 commits April 16, 2026 11:32
- Abort credential rotation rebuild when state backup fails (returns
  success: false or throws), falling back to reuse with a warning
- Tighten registry hash assertion in e2e test to verify the hash is
  stored under the specific sandbox name, not just anywhere in the file
- Enforce onboard exit codes in phases 2 and 3 of the e2e test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jyaunches jyaunches self-requested a review April 16, 2026 20:13
@ericksoa ericksoa self-assigned this Apr 16, 2026
@jyaunches

Copy link
Copy Markdown
Contributor

PR Review: fix/credential-rotation-propagation (#1967)

Files Changed: 6
Lines: +553 / -3

Architectural Assessment

The PR patches live, correct code. The sandbox reuse path in createSandbox() (onboard.ts ~L2790) and the registry schema (registry.ts) have not been restructured since this branch was forked. The merge commit (4b368c35) shows the branch is up-to-date with main. No codebase drift detected.

Overlapping PRs:

Verdict: The PR is architecturally sound and well-scoped.


🔴 Blockers (must fix before merge)

  1. test/e2e/test-token-rotation.sh:97-100 — silent failure if install.sh fails before creating the log file

    bash install.sh --non-interactive >"$INSTALL_LOG" 2>&1 &
    install_pid=$!
    tail -f "$INSTALL_LOG" --pid=$install_pid 2>/dev/null &

    If install.sh fails before creating the log file, tail -f will error silently and the test may report a false green. Consider adding touch "$INSTALL_LOG" before the backgrounded install.

  2. onboard.ts ~L2870-2888: credential rotation abort path doesn't update registry hashes

    When backup fails, the code falls through to upsertMessagingProviders(messagingTokenDefs) which pushes the new token to the gateway, then returns the sandbox name. But the registry still has the old hash. On the next nemoclaw onboard, the rotation will be detected again — triggering another backup attempt, another failure, another upsert. This creates an infinite re-detection loop on every subsequent onboard.

    Suggested fix: After upsertMessagingProviders in both abort paths, also update the registry hashes:

    const updatedHashes = {};
    for (const { envKey, token } of messagingTokenDefs) {
      if (token) updatedHashes[envKey] = hashCredential(token);
    }
    if (Object.keys(updatedHashes).length > 0) {
      registry.updateSandbox(sandboxName, { providerCredentialHashes: updatedHashes });
    }

🟡 Warnings (should fix)

  1. onboard.ts:830hashCredential trims whitespace before hashing

    String(value).trim() means " token " and "token" hash identically. But the actual credential passed to OpenShell is not trimmed. So the hash says "same" while the gateway gets a different value. Consider trimming at the input layer (when reading from env) rather than inside the hash, or document the trade-off.

  2. runner.ts — OpenShell origin hint duplicated three times

    The same 2-line console.error(...) block appears in run(), runArrayCmd(), and runInteractive(). There's also a subtle inconsistency: run() and runInteractive() use a regex (/^\s*openshell\s/) while runArrayCmd() uses equality (cmd[0] === "openshell"). Extract a helper to keep the detection logic and message text in one place.

  3. nightly-e2e.yamlactions/checkout@v6 inconsistency

    The new token-rotation-e2e job uses actions/checkout@v6, but every other job in this workflow uses @v4. PR build(deps): bump actions/checkout from 4 to 6 #1927 (open) is the batch bump for checkout. Use @v4 here and let build(deps): bump actions/checkout from 4 to 6 #1927 bump everything together.

  4. test/credential-rotation.test.ts:1@ts-nocheck

    Consistent with repo pattern for root-level tests, but the require("../dist/lib/onboard.js") import means export renames break at runtime, not at type-check time. Low risk but worth noting.


🔵 Suggestions (nice to have)

  1. test/e2e/test-token-rotation.sh:162-167 — registry assertion uses require('$REGISTRY')

    The $REGISTRY path is shell-expanded into a Node require(). If HOME contains spaces, this breaks. Use JSON.parse(require('fs').readFileSync(...)) instead.

  2. E2E test could verify the registry hash actually changed after Phase 2. Currently it checks rotation was detected and rebuild triggered, but not that the new hash was persisted. Adding a before/after hash comparison would close the loop.

  3. The getRequestedProviderHint() early-validation call (L5460) is a nice fix but unrelated to credential rotation — it's cleanly in its own commit (ca99cb92), which is good. ✅


✅ What's Good

  • Defense-in-depth, not overstatement. The rotation detection uses SHA-256 hashes in the local registry — correctly doesn't claim to validate the token against the provider API, just detects change.
  • Conservative on legacy sandboxes. Returns changed: false when no hashes are stored, avoiding false-positive rebuilds after upgrade.
  • Fail-safe backup logic. If backup fails, the sandbox stays running with the old config rather than being destroyed with data loss. This is the right call.
  • Strong test coverage. Unit tests cover all edge cases (null tokens, missing sandbox, multi-provider, legacy). E2E test proves the full round-trip across three phases (create → rotate → no-op reuse).
  • Clean commit hygiene. Separate commits for provider validation, CodeRabbit feedback, registry persistence, and core rotation logic. All conventional commits.
  • The runner.ts OpenShell origin hint is a welcome UX improvement — users frequently confuse NemoClaw errors with OpenShell errors.

Recommendation

Merge after fixing blockers. The credential rotation abort → re-detection loop (blocker #2) is the highest-priority issue — it creates an annoying (but non-destructive) UX bug for any user whose backup path fails. The actions/checkout@v6 inconsistency (warning #3) is a quick one-line fix. Everything else is solid work.

ericksoa and others added 2 commits April 16, 2026 13:26
Blockers:
- Touch install log before backgrounded install to prevent silent
  tail -f failure if install.sh dies before creating the file
- Update registry hashes in both backup-abort paths so the next
  onboard doesn't re-detect rotation in an infinite loop

Warnings:
- Use actions/checkout@v4 in token-rotation-e2e job to match the
  rest of the nightly workflow (v6 bump is tracked in #1927)
- Use fs.readFileSync with process.argv in registry assertion to
  handle HOME paths with spaces

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ericksoa

Copy link
Copy Markdown
Contributor Author

Thanks Julie — great review. All addressed in 9cd2b14:

Blockers (both fixed):

  1. touch "$INSTALL_LOG" before backgrounded install — prevents silent tail -f failure
  2. ✅ Both backup-abort paths now update registry hashes after upsertMessagingProviders, breaking the re-detection loop

Warnings:
3. ✅ actions/checkout@v6@v4 to match the rest of the nightly workflow
4. Re: hashCredential trimming — the normalizeCredentialValue() used at input time also trims, so the hash and the gateway credential see the same value. No divergence in practice.
5. Re: runner.ts OpenShell origin hint — agreed on the duplication, but out of scope for this PR.
6. Re: @ts-nocheck — consistent with repo pattern, noted.

Suggestions:

  1. ✅ Registry assertion now uses fs.readFileSync with process.argv instead of require('$REGISTRY') — handles paths with spaces
  2. Good idea on before/after hash comparison — will add in a follow-up if needed. The e2e test already implicitly validates this (phase 3 reuse proves the new hash was stored, otherwise it would re-detect rotation).

ericksoa and others added 5 commits April 16, 2026 13:52
- Fix createTarball test writing output inside collectDir, causing
  GNU tar "Can't add archive to itself" failure on Ubuntu CI
- Address jyaunches review blockers: touch install log before
  background install, update registry hashes in backup-abort paths
  to prevent re-detection loop
- Address jyaunches warnings: checkout@v4, fs.readFileSync for
  paths with spaces

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…otation)

Both branches added new fields to SandboxEntry and registerSandbox —
keep both providerCredentialHashes (this branch) and messagingChannels
(main). Keep both hashCredential/detectMessagingCredentialRotation and
makeConflictProbe helper functions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ericksoa ericksoa merged commit 8c770ad into main Apr 16, 2026
10 of 11 checks passed
miyoungc added a commit that referenced this pull request Apr 17, 2026
Refresh user-facing docs against the 34 commits merged between v0.0.17
and v0.0.18. Highlights:

- Replace the Ollama 0.0.0.0 binding guidance with the new authenticated
  reverse proxy on 127.0.0.1:11435 (#1922).
- Document the compatible-endpoint provider defaulting to
  /v1/chat/completions and the NEMOCLAW_PREFERRED_API=openai-responses
  opt-in (#1984).
- Add the new nemoclaw upgrade-sandboxes command with --check, --auto,
  and --yes flags (#1943).
- Note the cross-sandbox messaging overlap warning and 409 detection in
  nemoclaw <name> status (#1953).
- Document the messaging-token rotation auto-rebuild flow (#1967).
- Cover new troubleshooting entries for the Ollama auth proxy, IPv6
  localhost resolution, orphan SSH port-forward cleanup on re-onboard,
  and rotated messaging credentials (#1978, #1950).
- Note tar failure exit code for nemoclaw debug --output (#1770) and the
  orphaned openshell process cleanup in nemoclaw uninstall (#1940).

Also:

- Extend docs/.docs-skip to exclude the experimental sandbox-mgmt
  shields and config commands (#1976).
- Fix a sphinx-autobuild infinite rebuild loop in docs/conf.py by
  writing docs/project.json only when its contents change.
- Bump the docs version switcher preferred entry to 0.0.18.
- Regenerate nemoclaw-user-* agent skills from docs/.

Signed-off-by: Miyoung Choi <miyoungc@nvidia.com>
Made-with: Cursor
miyoungc added a commit that referenced this pull request Apr 17, 2026
## Summary

Refresh user-facing documentation against the 34 commits merged between
v0.0.17 and v0.0.18, bump the docs version switcher to v0.0.18, and fix
a
`sphinx-autobuild` infinite-rebuild loop triggered by `docs/conf.py`.

## Changes

- **Ollama authenticated reverse proxy** (#1922): Replace the
`0.0.0.0:11434` guidance in `docs/inference/use-local-inference.md` with
the new token-gated proxy on `127.0.0.1:11435`, including persisted
token,
health-check exemption, and sandbox provider wiring. Replace the
matching
  troubleshooting entry in `docs/reference/troubleshooting.md`.
- **Compatible-endpoint default API path** (#1984): Document that the
compatible-endpoint provider now defaults to `/v1/chat/completions` and
  update `NEMOCLAW_PREFERRED_API` to describe `openai-responses` as the
  opt-in instead of `openai-completions`. Updates in
  `use-local-inference.md`, `switch-inference-providers.md`, and
  `troubleshooting.md`.
- **`nemoclaw upgrade-sandboxes` command** (#1943): Add a new reference
entry in `docs/reference/commands.md` covering `--check`, `--auto`, and
  `--yes` flags.
- **Messaging token rotation auto-rebuild** (#1967, #1953): Note the
  automatic rebuild behavior and cross-sandbox overlap warning in
  `docs/deployment/set-up-telegram-bridge.md`, `commands.md`, and
  `troubleshooting.md`.
- **Other troubleshooting additions**:
  - `localhost` → `127.0.0.1` IPv6 note (#1978)
  - Orphan SSH port-forward cleanup on re-onboard (#1950)
  - Orphan `openshell` process cleanup in `nemoclaw uninstall` (#1940)
  - Non-zero exit on tar failure in `nemoclaw debug --output` (#1770)
- **Skip list**: Extend `docs/.docs-skip` to exclude the experimental
  sandbox-mgmt shields and config commands feature (#1976), which was
  explicitly merged as not-yet-documented.
- **Build stability**: `docs/conf.py` now writes `docs/project.json`
only
when contents change, so `make docs-live` / `sphinx-autobuild` no longer
detects its own generated file as a source change and enters an infinite
  rebuild loop.
- **Version switcher**: Bump `docs/versions1.json` and
`docs/project.json`
preferred entry to v0.0.18 so this refresh renders under the new
version.
- **Agent skills**: Regenerate `nemoclaw-user-*` skills from `docs/`
with
  `scripts/docs-to-skills.py`.

## Type of Change

- [ ] Code change (feature, bug fix, or refactor)
- [ ] Code change with doc updates
- [x] Doc only (prose changes, no code sample modifications)
- [ ] Doc only (includes code sample changes)

## Verification

- [x] `npx prek run --all-files` passes (ran via pre-commit hook on
staged files)
- [ ] `npm test` passes
- [ ] Tests added or updated for new or changed behavior
- [x] No secrets, API keys, or credentials committed
- [x] Docs updated for user-facing behavior changes
- [x] `make docs` builds without warnings (doc changes only)
- [x] Doc pages follow the [style
guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md)
(doc changes only)
- [ ] New doc pages include SPDX header and frontmatter (new pages only)

## AI Disclosure

- [x] AI-assisted — tool: Cursor

---

Signed-off-by: Miyoung Choi <miyoungc@nvidia.com>

Made with [Cursor](https://cursor.com)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added `nemoclaw upgrade-sandboxes` command to rebuild sandboxes when
base-image digests change.
* Introduced authenticated reverse proxy for local Ollama inference with
token-based access control.
* Automatic sandbox backup, recreation, and restore when messaging
credentials are updated.
* Cross-sandbox messaging token overlap detection with status warnings.

* **Improvements**
* Compatible-endpoint provider now defaults to `/v1/chat/completions`
API path.
* Enhanced troubleshooting documentation with new diagnostics sections.

* **Documentation**
  * Updated onboarding and configuration guides.
  * Expanded version documentation to 0.0.18.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Miyoung Choi <miyoungc@nvidia.com>
@wscurran wscurran added bug-fix PR fixes a bug or regression and removed fix labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug-fix PR fixes a bug or regression integration: telegram Telegram integration or channel behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Validate bot token rotation

3 participants