Skip to content

Setup experience software policy checks - #47075

Merged
getvictor merged 21 commits into
mainfrom
victor/45309-setup-experience-policy-checks
Jun 10, 2026
Merged

Setup experience software policy checks#47075
getvictor merged 21 commits into
mainfrom
victor/45309-setup-experience-policy-checks

Conversation

@getvictor

@getvictor getvictor commented Jun 8, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves #45309

If software is linked to policies, we run the policy during setup experience to determine if software should be installed. We install on failing policies.

Checklist for submitter

If some of the following don't apply, delete the relevant line.

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

  • Timeouts are implemented and retries are limited to avoid infinite loops

Testing

  • Added/updated automated tests
  • QA'd all new/changed functionality manually

Summary by CodeRabbit

  • New Features
    • Windows/Linux setup experience installers can be gated by team policies: setup will run a policy check and skip installing if the policy already passes; if the policy fails, the installer runs as part of setup.
    • After gated setup completes, the host’s policy set is re-evaluated promptly so subsequent policy actions run immediately.

getvictor added 4 commits June 8, 2026 11:25
…ftware (#45309)

Gate Windows/Linux setup-experience software on an associated team policy
(a policy whose install-software automation points at the same installer):

- Migration adds setup_experience_status_results.policy_id (FK to policies,
  ON DELETE SET NULL). Internal column (json:"-"), no API change.
- EnqueueSetupExperienceItems resolves the gating policy at enqueue time for
  Windows/Linux installers only (lowest id on ties; teamID 0 maps to global
  policies). VPP and Apple-platform rows are never gated.
- During setup, policyQueriesForHost un-skips ONLY the host's gating policies
  via PolicyQueriesForHostFiltered, never the whole team policy set.
- SetupExperienceNextStep uses the policy as a gate only: pass -> skip the
  install (success); fail -> install via the normal ForSetupExperience path so
  the item inherits the setup-experience retry count and RequireAllSoftware-
  Windows handling. Out-of-scope gating policy falls back to installing.
- processSoftwareForNewlyFailingPolicies suppresses the automation for in-setup
  hosts so the software is not double-installed.
- Result freshness is enforced via policy_membership.updated_at >=
  last_enrolled_at; init requests a refetch when gated items exist so a
  re-enrolled host gets a fresh result promptly.

Premium, Windows/Linux only. The whole path is cron-free.
#45309)

- changes/ changelog entry.
- Datastore tests: enqueue records policy_id for an associated Windows/Linux
  installer and NULL for un-gated/macOS items; No-team host gated by its
  team_id=0 policy; GetSetupExperiencePolicyIDsForHost (pending vs terminal);
  GetSetupExperiencePolicyResult freshness (updated_at >= last_enrolled_at);
  PolicyQueriesForHostFiltered scoping and out-of-scope exclusion.
- Service tests: gate control flow (pass->skip, fail->ForSetupExperience
  install with no PolicyID, awaiting-policy holds running, out-of-scope falls
  back to install, running item re-checked each poll).
- osquery test: policy automation suppressed for in-setup hosts (no double
  install), fires normally otherwise.

Also fixed the enqueue team scoping to match GetPoliciesWithAssociatedInstaller
(team_id = teamID; No-team uses 0, not NULL).
)

- Datastore test: an install linked to a non-terminal setup-experience row
  (what the gated install path produces) is returned by GetSoftwareInstallDetails
  with MaxRetries = setupExperienceSoftwareInstallsRetries (3 attempts), while a
  non-setup install gets 0. This is the retry parity linchpin.
- Service test: SaveHostSoftwareInstallResult on a setup-experience install
  treats an intermediate failure (retries_remaining > 0) as recorded-only, not
  updating the setup item or canceling setup; the final failure cancel (under
  require_all_software_windows) is covered by the existing windows cancel test.
@getvictor getvictor changed the title Victor/45309 setup experience policy checks Setup experience policy checks Jun 8, 2026
@getvictor getvictor changed the title Setup experience policy checks Setup experience software policy checks Jun 8, 2026
@getvictor
getvictor requested a review from Copilot June 8, 2026 13:02
@getvictor

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@getvictor

Copy link
Copy Markdown
Member Author

/agentic_review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 8, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)

Grey Divider


Remediation recommended

1. Polling causes extra DB writes ✓ Resolved 🐞 Bug ➹ Performance
Description
When a policy-gated setup-experience item has no fresh policy result yet, SetupExperienceNextStep
persists the same "running" state on every poll via UpdateSetupExperienceStatusResult, causing
unnecessary UPDATE traffic while waiting. This can add measurable write load during setup experience
polling, especially if hosts poll frequently and the policy result is delayed.
Code

ee/server/service/setup_experience.go[R389-403]

+	switch {
+	case passes == nil:
+		// No fresh result yet. If the gating policy's platform/label scope excludes the host, it will never be delivered or
+		// answered, so fall back to installing instead of waiting forever. Otherwise keep waiting (item stays running).
+		deliverable, err := svc.ds.PolicyQueriesForHostFiltered(ctx, host, []uint{*sw.PolicyID})
+		if err != nil {
+			return ctxerr.Wrap(ctx, err, "check gating policy deliverability")
+		}
+		if len(deliverable) == 0 {
+			svc.logger.InfoContext(ctx, "setup experience gating policy not applicable to host; installing item",
+				"host_id", host.ID, "policy_id", *sw.PolicyID, "software_installer_id", *sw.SoftwareInstallerID)
+			return svc.enqueueSetupExperienceSoftwareInstall(ctx, host, sw)
+		}
+		return svc.ds.UpdateSetupExperienceStatusResult(ctx, sw)
+
Evidence
The code explicitly runs the awaiting-policy check on every poll, and in the no-result-yet/in-scope
path it persists the status via UpdateSetupExperienceStatusResult. The datastore update issues a
full UPDATE statement, so repeating it per poll creates write amplification.

ee/server/service/setup_experience.go[226-239]
ee/server/service/setup_experience.go[389-403]
server/datastore/mysql/setup_experience.go[750-787]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`advancePolicyGatedSetupExperienceItem` calls `UpdateSetupExperienceStatusResult` in the `passes == nil` + in-scope branch, and `SetupExperienceNextStep` invokes this path on every poll for an awaiting-policy item. This produces repeated DB UPDATEs even when the row already has the correct `running` status and no fields have changed.
### Issue Context
This happens for policy-gated Windows/Linux setup-experience software items that are waiting for a fresh policy result.
### Fix Focus Areas
- ee/server/service/setup_experience.go[226-239]
- ee/server/service/setup_experience.go[375-415]
### Suggested fix
- Only persist the status transition to `running` once (e.g., when transitioning from `pending` to `running`).
- In the `passes == nil` + deliverable/in-scope branch, skip `UpdateSetupExperienceStatusResult` if the row is already `running` and `HostSoftwareInstallsExecutionID` is still nil (i.e., nothing changed).
- Alternatively, gate the update with a simple "dirty" flag or compare relevant fields before calling the datastore update.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Migration has no rollback ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
Down_20260608110503 is a no-op even though Up_20260608110503 alters setup_experience_status_results
by adding policy_id and a foreign key, preventing clean rollbacks of this schema change. This makes
operational rollback harder if the feature needs to be reverted.
Code

server/datastore/mysql/migrations/tables/20260608110503_AddPolicyGateToSetupExperienceResults.go[R29-30]

+func Down_20260608110503(tx *sql.Tx) error {
+	return nil
Evidence
The migration’s Up alters the table schema, while the Down function returns nil without reversing
those changes.

server/datastore/mysql/migrations/tables/20260608110503_AddPolicyGateToSetupExperienceResults.go[13-31]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The migration adds a new column and FK constraint but the Down migration is empty, so rolling back leaves the schema changed.
### Issue Context
`Up_20260608110503` adds `policy_id` and `fk_setup_experience_status_results_policy_id`.
### Fix Focus Areas
- server/datastore/mysql/migrations/tables/20260608110503_AddPolicyGateToSetupExperienceResults.go[13-31]
### Suggested fix
- Implement `Down_20260608110503` to drop the FK constraint and the `policy_id` column (in the correct order), e.g.:
- `ALTER TABLE setup_experience_status_results DROP FOREIGN KEY fk_setup_experience_status_results_policy_id;`
- `ALTER TABLE setup_experience_status_results DROP COLUMN policy_id;`
- If irreversible migrations are a deliberate project convention, add a short comment explaining why `Down` is intentionally a no-op.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Copilot AI 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.

Pull request overview

This PR adds “policy-gated” setup experience installs for Windows and Linux: when a setup-experience software installer is also referenced by an install-software policy automation, Fleet will run that policy during setup and skip the install if it passes, while preventing duplicate installs from policy automation during setup.

Changes:

  • Add policy_id to setup_experience_status_results and record the gating policy at enqueue time (Windows/Linux only).
  • During setup experience, distribute only gating-policy queries (filtered) and suppress policy automation installs that would double-install.
  • Implement server-side gating logic in setup experience: wait for a fresh policy result, skip on pass, install on fail, and fall back to install when policy scope excludes the host.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
server/service/setup_experience_test.go Adds regression coverage for intermediate install failures not canceling setup experience.
server/service/osquery.go Limits policy distribution during setup experience to gating policies and suppresses policy automation installs for gated items.
server/service/osquery_test.go Updates/extends tests for setup-experience policy distribution and automation suppression behavior.
server/mock/datastore_mock.go Extends datastore mock with filtered policy queries + setup-experience policy gating helpers.
server/fleet/setup_experience.go Adds PolicyID to setup experience status model for internal gating.
server/fleet/datastore.go Extends datastore interface with setup-experience policy gating/query APIs.
server/datastore/mysql/setup_experience.go Records policy_id on enqueued Windows/Linux setup-experience software items; adds query to list gating policy IDs.
server/datastore/mysql/setup_experience_test.go Adds MySQL integration tests for gating behavior, freshness, and retry inheritance.
server/datastore/mysql/schema.sql Updates schema to include policy_id column + FK/index on setup experience status results.
server/datastore/mysql/policies.go Implements PolicyQueriesForHostFiltered and GetSetupExperiencePolicyResult.
server/datastore/mysql/migrations/tables/20260608110503_AddPolicyGateToSetupExperienceResults.go Migration adding policy_id column and FK to setup_experience_status_results.
server/datastore/mysql/migrations/tables/20260608110503_AddPolicyGateToSetupExperienceResults_test.go Migration test validating FK behavior (ON DELETE SET NULL) and nullability.
ee/server/service/setup_experience.go Implements policy-gated setup experience flow (wait/skip/install + out-of-scope fallback).
ee/server/service/setup_experience_test.go Unit tests for the policy-gated setup experience next-step behavior.
ee/server/service/orbit.go Requests host refetch when setup experience has gated items to ensure prompt policy distribution.
changes/45309-setup-experience-policy-checks Adds release-note entry for the new Premium feature behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/service/setup_experience_test.go Outdated
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7a30db4d-b7cf-4efc-910c-6b818b2489c6

📥 Commits

Reviewing files that changed from the base of the PR and between a3a1982 and 7658f44.

📒 Files selected for processing (2)
  • server/datastore/mysql/migrations/tables/20260609081645_AddPolicyGateToSetupExperienceResults.go
  • server/datastore/mysql/setup_experience.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/datastore/mysql/migrations/tables/20260609081645_AddPolicyGateToSetupExperienceResults.go
  • server/datastore/mysql/setup_experience.go

Walkthrough

This PR implements policy-gated Windows/Linux setup-experience installations: a DB schema/field marks gated installer rows; datastore APIs expose filtered policy queries, per-policy results, and helpers to clear membership/reset host policy clocks; enqueue SQL computes/persists the gating marker; osquery filtering and automation suppression limit distributed queries and installs during setup; and the setup-experience service polls, advances gated items (pass → skip, fail → install, pending → wait with timeout), and resets host policy state after completion.

Possibly related PRs

  • fleetdm/fleet#45999: Modifies server/service/osquery.go failing-policy automation; both PRs touch automation flow and processSoftwareForNewlyFailingPolicies.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 title 'Setup experience software policy checks' clearly and concisely describes the main change: adding policy-check logic to setup experience software installation on Windows/Linux.
Description check ✅ Passed The PR description includes the related issue (#45309), completed checklist items (changes file, timeouts, tests, manual QA), but omits security validation and database migration considerations from the template.
Linked Issues check ✅ Passed The PR fully implements issue #45309 requirements: policy-gated setup-experience for Windows/Linux installers, fail-open after timeout, skip on policy pass, install on policy fail, platform/label scoping, macOS excluded, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes directly support policy-gated setup-experience: datastore methods for policy queries/results, service logic for gating decisions, schema for policy tracking, migrations, tests, and mocks. No unrelated changes 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 victor/45309-setup-experience-policy-checks

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.

❤️ Share

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

🧹 Nitpick comments (2)
server/fleet/datastore.go (2)

942-944: ⚡ Quick win

Define the empty-filter contract explicitly.

PolicyQueriesForHostFiltered is the guardrail that keeps setup from un-skipping unrelated team policies. Please document that policyIDs == nil or len(policyIDs) == 0 must return an empty map rather than falling back to PolicyQueriesForHost, so implementations and mocks can't diverge 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 `@server/fleet/datastore.go` around lines 942 - 944, The comment for
PolicyQueriesForHostFiltered must explicitly state the empty-filter contract:
when policyIDs is nil or has length zero the function MUST return an empty map
(and no error) rather than delegating to or falling back to
PolicyQueriesForHost; update the doc comment for PolicyQueriesForHostFiltered to
document this behavior so all implementations and mocks (e.g., any mock of
PolicyQueriesForHostFiltered) follow the contract and do not accidentally return
team-wide policies.

946-948: ⚡ Quick win

Pin this to the latest definitive result.

The contract currently says “a” fresh pass/fail result after since. If multiple evaluations exist after re-enrollment or repeated policy runs, callers need the most recent definitive result; otherwise setup can skip or install based on stale data.

🤖 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 `@server/fleet/datastore.go` around lines 946 - 948, The
GetSetupExperiencePolicyResult contract must return the latest definitive
(non-null pass/fail) evaluation recorded at or after the provided since
timestamp, not just any fresh result; update the implementation of
GetSetupExperiencePolicyResult to query/filter evaluations for the given hostID
and policyID with recorded_at >= since (or equivalent timestamp column), require
the result be definitive (i.e., pass/fail not NULL), order by the evaluation
timestamp/updated_at DESC and LIMIT 1, and return that single most recent bool
value (or nil if none); reference the GetSetupExperiencePolicyResult function to
locate and change the SQL/ORM query and result handling.
🤖 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 `@server/datastore/mysql/policies.go`:
- Around line 1253-1269: GetSetupExperiencePolicyResult is using
policy_membership.updated_at to decide freshness but RecordPolicyQueryExecutions
(and policiesNeedingMembershipWrite) intentionally skip writes when the pass
value is unchanged, which can leave gating policies forever returning nil; fix
by aligning the freshness signal with write semantics — either update a
per-policy "last_reported" timestamp whenever a host reports a policy (even if
pass/fail is unchanged) in
RecordPolicyQueryExecutions/policiesNeedingMembershipWrite, or add and maintain
a dedicated reported_at column and change GetSetupExperiencePolicyResult to
compare against that reported_at instead of updated_at so re-enrollment without
value change still counts as fresh.

---

Nitpick comments:
In `@server/fleet/datastore.go`:
- Around line 942-944: The comment for PolicyQueriesForHostFiltered must
explicitly state the empty-filter contract: when policyIDs is nil or has length
zero the function MUST return an empty map (and no error) rather than delegating
to or falling back to PolicyQueriesForHost; update the doc comment for
PolicyQueriesForHostFiltered to document this behavior so all implementations
and mocks (e.g., any mock of PolicyQueriesForHostFiltered) follow the contract
and do not accidentally return team-wide policies.
- Around line 946-948: The GetSetupExperiencePolicyResult contract must return
the latest definitive (non-null pass/fail) evaluation recorded at or after the
provided since timestamp, not just any fresh result; update the implementation
of GetSetupExperiencePolicyResult to query/filter evaluations for the given
hostID and policyID with recorded_at >= since (or equivalent timestamp column),
require the result be definitive (i.e., pass/fail not NULL), order by the
evaluation timestamp/updated_at DESC and LIMIT 1, and return that single most
recent bool value (or nil if none); reference the GetSetupExperiencePolicyResult
function to locate and change the SQL/ORM query and result handling.
🪄 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

Run ID: fa2ca06e-2af7-4a2a-9dca-568a8ca28b70

📥 Commits

Reviewing files that changed from the base of the PR and between 89965f4 and 7685c17.

📒 Files selected for processing (16)
  • changes/45309-setup-experience-policy-checks
  • ee/server/service/orbit.go
  • ee/server/service/setup_experience.go
  • ee/server/service/setup_experience_test.go
  • server/datastore/mysql/migrations/tables/20260608110503_AddPolicyGateToSetupExperienceResults.go
  • server/datastore/mysql/migrations/tables/20260608110503_AddPolicyGateToSetupExperienceResults_test.go
  • server/datastore/mysql/policies.go
  • server/datastore/mysql/schema.sql
  • server/datastore/mysql/setup_experience.go
  • server/datastore/mysql/setup_experience_test.go
  • server/fleet/datastore.go
  • server/fleet/setup_experience.go
  • server/mock/datastore_mock.go
  • server/service/osquery.go
  • server/service/osquery_test.go
  • server/service/setup_experience_test.go

Comment thread server/datastore/mysql/policies.go Outdated
Comment thread server/service/osquery.go
@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.43946% with 57 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.22%. Comparing base (097c82a) to head (7658f44).
⚠️ Report is 20 commits behind head on main.

Files with missing lines Patch % Lines
ee/server/service/setup_experience.go 75.90% 10 Missing and 10 partials ⚠️
server/service/osquery.go 67.56% 8 Missing and 4 partials ⚠️
server/datastore/mysql/policies.go 77.27% 5 Missing and 5 partials ⚠️
ee/server/service/orbit.go 22.22% 5 Missing and 2 partials ⚠️
...609081645_AddPolicyGateToSetupExperienceResults.go 66.66% 3 Missing and 1 partial ⚠️
server/datastore/mysql/setup_experience.go 88.88% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #47075      +/-   ##
==========================================
+ Coverage   67.16%   67.22%   +0.05%     
==========================================
  Files        2926     2927       +1     
  Lines      226389   226714     +325     
  Branches    11683    11683              
==========================================
+ Hits       152050   152400     +350     
+ Misses      60600    60549      -51     
- Partials    13739    13765      +26     
Flag Coverage Δ
backend 68.82% <74.43%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

getvictor added 3 commits June 8, 2026 15:54
…perience-policy-checks

# Conflicts:
#	server/datastore/mysql/schema.sql
…perience-policy-checks

# Conflicts:
#	server/datastore/mysql/schema.sql

Copilot AI 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.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.

Comment thread server/service/osquery_test.go
@getvictor

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@getvictor

Copy link
Copy Markdown
Member Author

/agentic_review

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

Comment thread ee/server/service/setup_experience.go Fixed
@getvictor
getvictor marked this pull request as ready for review June 9, 2026 20:32
@getvictor
getvictor requested a review from a team as a code owner June 9, 2026 20:32
Comment thread server/datastore/mysql/policies.go
Comment thread server/datastore/mysql/setup_experience.go Outdated
Comment thread ee/server/service/setup_experience.go
@getvictor

Copy link
Copy Markdown
Member Author

@ksykulev ready for re-review

@getvictor
getvictor merged commit 251093f into main Jun 10, 2026
42 checks passed
@getvictor
getvictor deleted the victor/45309-setup-experience-policy-checks branch June 10, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Run policy checks before installing Windows & Linux setup experience software

4 participants