Skip to content

fix(hermes): enforce runtime env secret guard on direct hermes gateway (#4975)#4981

Merged
cv merged 5 commits into
NVIDIA:mainfrom
abhi-0906:fix/issue-4975-hermes-gateway-secret-guard
Jun 10, 2026
Merged

fix(hermes): enforce runtime env secret guard on direct hermes gateway (#4975)#4981
cv merged 5 commits into
NVIDIA:mainfrom
abhi-0906:fix/issue-4975-hermes-gateway-secret-guard

Conversation

@abhi-0906

@abhi-0906 abhi-0906 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

The Hermes runtime-env secret boundary (refuse startup when raw secret-shaped env vars like SLACK_BOT_TOKEN=xoxb-real-… are present) ran only on the nemoclaw-start entrypoint and the host-side recovery path. A direct docker exec … hermes gateway run invokes the CLI without crossing either, so the gateway started with raw secrets and no [SECURITY] rejection. This wraps the hermes CLI so the same guard runs for the gateway subcommand however it is invoked.

Related Issue

Fixes #4975

Root cause

validate_hermes_runtime_env_secret_boundary (agents/hermes/start.sh) and the recovery path both call the single-source-of-truth validator agents/hermes/validate-env-secret-boundary.py runtime-env, but the guard is attached to those entrypoints — not to the hermes gateway command — so a direct invocation bypasses it (as the issue notes, "implemented only for a different entrypoint").

Changes

  • agents/hermes/hermes-wrapper.sh (new) — installed at /usr/local/bin/hermes; runs the existing validator for the gateway subcommand, then execs the real binary. All other subcommands pass straight through.
  • agents/hermes/Dockerfile — relocate the real binary to hermes.real, install the wrapper, re-assert hermes --version through it so a broken relocation fails the build.
  • test/hermes-gateway-wrapper.test.ts (new) — Linux-gated tests against the real validator + a hermes.real stub.

Design notes for review: the wrapper reuses the existing validator (no second copy of the security logic); it resolves the validator and real binary from fixed installed paths, never from env (it guards against a malicious env, so env must not redirect it); exec preserves the PID so start.sh's wait/respawn still track the gateway; and the trusted path already runs the identical guard before launch with placeholder-only env, so legitimate startup is not regressed.

Type of Change

  • Code change (bug fix)

Verification

Validated on Ubuntu 24.04: reporter's exact repro now refused (non-zero, [SECURITY], gateway never started, token not leaked); placeholder/allow-listed env still starts the gateway; non-gateway subcommands and --version pass through; Dockerfile relocation preserves the symlink→venv binary; guard is not env-redirectable; exec preserves PID.

  • Tests added for new behavior (Linux-gated)
  • No secrets or credentials committed
  • Full npm test / npx prek run --all-files deferred to CI (ran targeted vitest + typecheck + test-size budget locally)

Summary by CodeRabbit

  • New Features

    • Direct gateway invocations now enforce an environment-secret boundary; other commands (including version) continue to pass through normally.
    • Gateway runs that violate the boundary are refused to prevent accidental secret exposure.
  • Tests

    • Added tests validating gateway acceptance/rejection, resilience to PATH-based bypass attempts, and normal pass-through for non-gateway commands.

Signed-off-by: abhi-0906 abhimanyukumar7290@gmail.com

…ay` (NVIDIA#4975)

The Hermes runtime-env secret boundary (refuse raw secret-shaped env vars like
SLACK_BOT_TOKEN=xoxb-real-...) ran only on the nemoclaw-start entrypoint and the
host-side recovery path. A direct `docker exec ... hermes gateway run` invokes
the CLI without crossing either, so the gateway started with raw secrets in its
environment and no [SECURITY] rejection.

Wrap /usr/local/bin/hermes: relocate the real binary to hermes.real and install
a thin wrapper that runs the existing single-source-of-truth validator
(validate-env-secret-boundary.py runtime-env) for the `gateway` subcommand
before exec'ing the real binary. Every other subcommand passes straight through.

SECURITY: the wrapper resolves the validator and the real binary from fixed
installed paths, never from the environment — it exists to reject a malicious
runtime env, so it must not let that env redirect the guard or the binary it
protects (a dev fallback resolves against the script's own directory for
checkouts, matching start.sh's _HERMES_BOUNDARY_VALIDATOR resolution). The guard
is identical to the one start.sh already runs before launch, and the placeholder
refresh only emits allow-listed openshell:resolve:env:* values, so the trusted
startup path is not regressed; only a genuinely raw secret-shaped var is
rejected.

Add Linux-gated tests that run the wrapper against the real validator and a
hermes.real stub: gateway+raw-secret is refused (exit 1, value not leaked, real
never invoked), gateway+placeholders is allowed, and non-gateway subcommands
(dashboard, --version) pass through even with raw secrets present.

Signed-off-by: Abhimanyu Kumar <abhimanyukumar7290@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Jun 8, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Hermes CLI wrapper that gates the gateway subcommand with a runtime-env validator, installs the wrapper into the image by moving the original binary to hermes.real, and adds Vitest tests verifying rejection, PATH-shadowing resistance, and pass-through behaviors.

Changes

Hermes Gateway Env-Guard Wrapper

Layer / File(s) Summary
Gateway guard wrapper logic
agents/hermes/hermes-wrapper.sh
Adds /usr/local/bin/hermes wrapper that computes its directory, resolves hermes.real and validate-hermes-env-secret-boundary.py (with dev fallbacks), selects a trusted absolute python3 to run the validator only for gateway, exits non-zero on validator failure (127 if no trusted interpreter), and execs the real Hermes binary otherwise.
Container binary path wiring
agents/hermes/Dockerfile
Copies the wrapper into the image, renames the original /usr/local/bin/hermes to /usr/local/bin/hermes.real, installs the wrapper at /usr/local/bin/hermes, and runs hermes --version during build to validate the wrapper wiring.
Wrapper behavior validation tests
test/hermes-gateway-wrapper.test.ts
Adds Linux + python3 gated Vitest tests with a temp-dir harness that copies wrapper/validator, installs a hermes.real stub, and asserts: gateway rejection on raw-secret env, PATH shadowing cannot bypass guard, gateway allowed for placeholders/allow-listed keys, non-gateway passthrough, and --version passthrough.

Sequence Diagram(s)

sequenceDiagram
  participant Wrapper as /usr/local/bin/hermes (wrapper)
  participant Py as python3
  participant Validator as validate-hermes-env-secret-boundary.py
  participant HermesReal as /usr/local/bin/hermes.real

  Wrapper->>Py: invoke Validator runtime-env (only for `gateway`)
  Py->>Validator: execute validation
  Validator-->>Py: exit code (0 or non-zero)
  Py-->>Wrapper: return exit code
  alt validator succeeds
    Wrapper->>HermesReal: exec with original args
  else validator fails
    Wrapper-->>Wrapper: exit with validator status (non-zero)
  end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through shells where secrets hide,
I wrapped the gate and stood inside.
If tokens leak, I thump "not through!"
Trusted python checks, then off we flew,
Tests nod safe as stubs report 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
Title check ✅ Passed The PR title clearly and specifically describes the main change: enforcing the runtime env secret guard on direct hermes gateway invocations, which directly addresses the root cause identified in issue #4975.
Linked Issues check ✅ Passed The PR fully implements the objectives from issue #4975: wrapper enforces secret boundary validation on hermes gateway subcommand with [SECURITY] messages, rejects raw secret-shaped env vars while permitting allow-listed keys, uses fixed interpreter paths to prevent PATH bypass, and comprehensive tests verify all validation scenarios.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #4975: Dockerfile relocation of the real binary and wrapper installation, the hermes-wrapper.sh implementation for gateway env-secret enforcement, and corresponding Linux-gated tests with validator stub. No unrelated modifications detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 1

🧹 Nitpick comments (1)
agents/hermes/Dockerfile (1)

98-109: Run the secret-boundary E2E lane on this branch before merge.

This hunk changes the gateway startup security boundary path; run the boundary-focused job explicitly since nightly coverage is not PR-triggered.

gh workflow run nightly-e2e.yaml --ref <branch> -f jobs=hermes-secret-boundary-e2e

As per coding guidelines, changes under agents/hermes/** should be validated with Hermes E2E coverage, and the referenced workflow docs identify hermes-secret-boundary-e2e as the boundary contract check.

🤖 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 `@agents/hermes/Dockerfile` around lines 98 - 109, This change relocates the
Hermes binary and installs a wrapper (hermes-wrapper.sh ->
/usr/local/bin/hermes, original moved to /usr/local/bin/hermes.real) which
affects the gateway secret boundary; before merging, run the Hermes
secret-boundary E2E lane to validate behavior by executing the nightly-e2e
workflow for the hermes-secret-boundary-e2e job (use: gh workflow run
nightly-e2e.yaml --ref <branch> -f jobs=hermes-secret-boundary-e2e), verify the
wrapper correctly delegates non-gateway subcommands to hermes.real and that
`/usr/local/bin/hermes --version` succeeds in the built image, and address any
failures found by updating hermes-wrapper.sh or the Dockerfile install/move
steps.

Source: Coding guidelines

🤖 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 `@agents/hermes/hermes-wrapper.sh`:
- Around line 36-37: The wrapper currently invokes the guard with an unpinned
"python3" from PATH; change it to use a fixed interpreter path (e.g.
/usr/bin/python3) and verify it exists/executable before running to enforce the
"never from the environment" intent. Replace the invocation of python3 "$GUARD"
in hermes-wrapper.sh with a pinned interpreter variable (e.g.
PINNED_PYTHON=/usr/bin/python3), check [ -x "$PINNED_PYTHON" ] and then run
"$PINNED_PYTHON" "$GUARD" runtime-env || exit $?, so the guard always uses the
known system Python rather than PATH-resolved python3.

---

Nitpick comments:
In `@agents/hermes/Dockerfile`:
- Around line 98-109: This change relocates the Hermes binary and installs a
wrapper (hermes-wrapper.sh -> /usr/local/bin/hermes, original moved to
/usr/local/bin/hermes.real) which affects the gateway secret boundary; before
merging, run the Hermes secret-boundary E2E lane to validate behavior by
executing the nightly-e2e workflow for the hermes-secret-boundary-e2e job (use:
gh workflow run nightly-e2e.yaml --ref <branch> -f
jobs=hermes-secret-boundary-e2e), verify the wrapper correctly delegates
non-gateway subcommands to hermes.real and that `/usr/local/bin/hermes
--version` succeeds in the built image, and address any failures found by
updating hermes-wrapper.sh or the Dockerfile install/move steps.
🪄 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: Enterprise

Run ID: 3d414101-6efa-489f-8be6-733a60cd06e4

📥 Commits

Reviewing files that changed from the base of the PR and between 16470fa and c287d2d.

📒 Files selected for processing (3)
  • agents/hermes/Dockerfile
  • agents/hermes/hermes-wrapper.sh
  • test/hermes-gateway-wrapper.test.ts

Comment thread agents/hermes/hermes-wrapper.sh Outdated
…apper

Address CodeRabbit review on NVIDIA#4981: the wrapper invoked the secret-boundary
guard via PATH-resolved `python3`. Because PATH is part of the untrusted runtime
environment this wrapper exists to reject, an attacker could shadow `python3`
(e.g. `docker exec -e PATH=/tmp/evil:...`) with a script that exits 0 and no-op
the guard, starting the gateway with raw secrets.

Resolve python3 from a fixed set of absolute paths (/usr/bin, /usr/local/bin,
the hermes venv), never via PATH, and fail closed if none is executable. The
interpreter join the validator and real binary as env-independent, fixed-path
resolutions. Add a test that plants a PATH-shadowed python3 and asserts the
guard still refuses a raw secret-shaped gateway launch.

Signed-off-by: Abhimanyu Kumar <abhimanyukumar7290@gmail.com>
@abhi-0906

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — good catch on the interpreter.

Addressed in 7148a54: the wrapper resolved python3 via PATH, which (as you note) is part of the untrusted environment this wrapper exists to reject — a shadowed python3 could no-op the guard. It now resolves the interpreter from a fixed set of absolute paths (/usr/bin/python3, /usr/local/bin/python3, the hermes venv), never via PATH, and fails closed if none is executable. Added a test that plants a PATH-shadowed python3 (exit 0) and asserts a raw secret-shaped gateway launch is still refused (real binary never invoked).

On the hermes-secret-boundary-e2e lane: agreed it should run against this change, but it's nightly-only and not PR-triggered, and as an external fork PR the NVIDIA-runner workflows are gated behind copy-pr-bot vetting — I can't dispatch it. Flagging for a maintainer to run nightly-e2e.yaml -f jobs=hermes-secret-boundary-e2e against the branch during review.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the current state of the wrapper script after the fix in commit 7148a5462
gh pr view 4981 --repo NVIDIA/NemoClaw --json commits,headRefName 2>/dev/null | jq .
# Try to fetch the latest wrapper content
gh api repos/NVIDIA/NemoClaw/contents/agents/hermes/hermes-wrapper.sh?ref=fix/issue-4975-hermes-gateway-secret-guard 2>/dev/null | jq -r '.content' | base64 -d 2>/dev/null || echo "Could not fetch file"

Length of output: 4043


🏁 Script executed:

#!/bin/bash
# Fetch the current wrapper script content at the latest commit
gh api "repos/NVIDIA/NemoClaw/contents/agents/hermes/hermes-wrapper.sh?ref=7148a54621c16645b6395e043a85ac713cabd870" 2>/dev/null | jq -r '.content' | base64 -d 2>/dev/null || echo "Could not fetch file"

Length of output: 2902



@abhi-0906 Thanks for the detailed follow-up and for verifying the fix against the latest commit.

The updated wrapper looks correct — the three security-critical resolutions (interpreter, validator, real binary) now all use the same fixed-path-first / dev-fallback pattern and are fully isolated from the untrusted runtime environment:

for _candidate in /usr/bin/python3 /usr/local/bin/python3 /opt/hermes/.venv/bin/python3; do
  if [ -x "$_candidate" ]; then PYTHON3="$_candidate"; break; fi
done
if [ -z "$PYTHON3" ]; then
  echo "[SECURITY] Refusing hermes gateway: no python3 at a trusted absolute path ..." >&2
  exit 127
fi

Fail-closed on a missing interpreter (exit 127 + [SECURITY] log) is exactly the right posture. The shadow-python3 test is a solid regression guard for this specific vector.

On the hermes-secret-boundary-e2e lane — the constraint is understood; gating external fork PRs behind copy-pr-bot vetting is a sensible policy. The maintainer flag in the PR description should be enough to get it queued during review.

🐇✨

@wscurran wscurran added the bug-fix PR fixes a bug or regression label Jun 8, 2026
@prekshivyas prekshivyas self-assigned this Jun 10, 2026

@prekshivyas prekshivyas 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.

LGTM on the code — solid security fix for #4975. Wrapping the hermes binary closes the docker exec ... hermes gateway run bypass so every gateway-start path crosses the same env-secret-boundary validator. Security hygiene is right: python3/validator/real-binary all resolved from fixed absolute paths (never $PATH — exactly what this guard must resist), fail-closed if no trusted python3 (exit 127), guard failure propagates (|| exit $?), and exec $REAL_HERMES only runs after the guard passes. Build re-asserts --version through the wrapper, and there's a Linux+python3 test covering the enforcement.

One thing before merge (especially for a security change): CI shows only 4 checks — the bash/python test + shellcheck/vitest suite appears aged on this older PR. Recommend a rebase to re-trigger the full suite and confirm the new test + shellcheck go green.

@prekshivyas prekshivyas added the v0.0.63 Release target label Jun 10, 2026
prekshivyas and others added 2 commits June 9, 2026 18:29
Apply shfmt (-i 2 -ci -bn) to hermes-wrapper.sh and Biome to the test so
static-checks' formatter hooks leave them unchanged. Formatting only; the
wrapper's gateway secret-boundary logic is unchanged.

Co-authored-by: abhi-0906 <abhimanyukumar7290@gmail.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@prekshivyas

Copy link
Copy Markdown
Contributor

@abhi-0906 one thing left to make CI green:

dco-check needs a sign-off line in the PR description. Edit this PR's description (the top box) and add this line at the bottom:

Signed-off-by: abhi-0906 <abhimanyukumar7290@gmail.com>

Only you (the author) can sign off. Once that line is in the description, dco-check goes green.

(I pushed a small shfmt + Biome formatting commit to clear static-checks — formatting only, your wrapper logic is unchanged.)

It has a shebang but was committed non-executable, which trips the check-shebang-scripts-are-executable hook (surfaced once the formatter drift was cleared).

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@abhi-0906

Copy link
Copy Markdown
Contributor Author

Thanks @prekshivyas — done. I've added the sign-off line to the PR description:

Signed-off-by: abhi-0906 <abhimanyukumar7290@gmail.com>

The edit triggered a fresh dco-check run, but it's sitting in action_required (the fork-PR workflow-approval gate) — it should go green once a maintainer approves the run. Appreciate the shfmt/Biome + executable-bit cleanup too.

@cv cv enabled auto-merge (squash) June 10, 2026 17:43
@cv cv merged commit 4b5c277 into NVIDIA:main Jun 10, 2026
35 of 37 checks passed
miyoungc added a commit that referenced this pull request Jun 11, 2026
## Summary
- Add the v0.0.63 release-note section using the published development
note as source context.
- Update source docs for sandbox recovery, OpenClaw config restore
safety, managed vLLM selection, Slack Socket Mode conflict handling, and
host diagnostics.
- Refresh generated `nemoclaw-user-*` skills from the updated Fern MDX
docs.
- Update the release-doc refresh skill so post-release docs for version
`n` look up the matching announcement discussion and use the `n+1` patch
release label.
- Fix CLI/docs parity by avoiding a `--from <Dockerfile>` flag mention
inside the `upgrade-sandboxes` command section.

## Source summary
- #5034 -> `docs/reference/troubleshooting.mdx`,
`docs/about/release-notes.mdx`: Document safer stale-sandbox recovery
through `rebuild --yes` before recreating from scratch.
- #5091 -> `docs/reference/troubleshooting.mdx`,
`docs/about/release-notes.mdx`: Document Docker-driver post-reboot
recovery from OpenShell container labels.
- #5101, #5174, #5177 -> `docs/manage-sandboxes/backup-restore.mdx`,
`docs/about/release-notes.mdx`: Document OpenClaw `openclaw.json`
preservation, merge behavior, and fail-safe restore handling.
- #5102 -> `docs/reference/commands.mdx`,
`docs/reference/commands-nemohermes.mdx`,
`docs/manage-sandboxes/lifecycle.mdx`, `docs/about/release-notes.mdx`:
Document `upgrade-sandboxes` image-fingerprint drift detection.
- #4201 -> `docs/reference/troubleshooting.mdx`,
`docs/about/release-notes.mdx`: Document the installer diagnostic for
unexpected Docker daemon access outside the `docker` group.
- #5038 -> `docs/inference/inference-options.mdx`,
`docs/inference/use-local-inference.mdx`,
`docs/about/release-notes.mdx`: Document the interactive managed-vLLM
model picker and non-interactive override behavior.
- #5040, #5041 -> `docs/reference/troubleshooting.mdx`,
`docs/about/release-notes.mdx`: Document Ollama auth-proxy recovery and
host DNS preflight diagnostics.
- #4986, #5039 -> `docs/manage-sandboxes/messaging-channels.mdx`,
`docs/about/release-notes.mdx`: Document Slack validation and duplicate
Slack Socket Mode sandbox handling.
- #4981, #5168 -> `docs/about/release-notes.mdx`: Capture Hermes gateway
secret-guard and wrapped-argv startup hardening in the release surface.
- Follow-up ->
`.agents/skills/nemoclaw-contributor-update-docs/SKILL.md`: Record the
post-release docs workflow, discussion-announcement lookup, and
next-patch release label rule.
- Follow-up -> `docs/reference/commands.mdx`,
`docs/reference/commands-nemohermes.mdx`: Reword custom Dockerfile
sandbox text so CLI parity does not treat `--from` as an
`upgrade-sandboxes` flag.

## Verification
- `python3 scripts/docs-to-skills.py docs/ .agents/skills/ --prefix
nemoclaw-user --doc-platform fern-mdx`
- `npm run docs`
- `npm run build:cli`
- `bash test/e2e/e2e-cloud-experimental/check-docs.sh --only-cli`
- Skip-term scan for `docs/.docs-skip` blocked terms across generated
user skills

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

## Summary by CodeRabbit

* **Documentation**
* Enhanced local inference setup with interactive model selection
prompts and environment variable overrides
* Improved sandbox upgrade detection using build fingerprints and
version checks
* Clarified configuration restore behavior preserving user settings
during rebuild/restore
  * Added gateway authentication as fifth security layer
  * Expanded Slack messaging validation with live credential checking
* Enhanced troubleshooting guidance for Docker access, DNS issues, and
sandbox recovery
* Updated release notes for v0.0.63 featuring sandbox recovery and
inference improvements

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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 v0.0.63 Release target

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[All Platforms][Security] Hermes gateway accepts raw SLACK_BOT_TOKEN env without security rejection in NemoHermes sandbox

4 participants