Skip to content

policy: enterprise managed_settings for Copilot clients#318623

Merged
joshspicer merged 32 commits into
mainfrom
agents/users-josh-git-agent-control-plane-docs-adrs-dfeea4a4
May 29, 2026
Merged

policy: enterprise managed_settings for Copilot clients#318623
joshspicer merged 32 commits into
mainfrom
agents/users-josh-git-agent-control-plane-docs-adrs-dfeea4a4

Conversation

@joshspicer
Copy link
Copy Markdown
Member

@joshspicer joshspicer commented May 27, 2026

What

Wires a new /copilot_internal/managed_settings endpoint into VS Code's existing AccountPolicyService / IPolicyData stack so an enterprise admin's .github/copilot/settings.json policy applies to three new plugin/marketplace settings.

API field VS Code setting Policy
enabledPlugins chat.plugins.enabledPlugins (new) ChatEnabledPlugins
extraKnownMarketplaces chat.plugins.extraMarketplaces (new, policy-only) ChatExtraMarketplaces
strictKnownMarketplaces chat.plugins.strictMarketplaces (new) ChatStrictMarketplaces

All three settings are tagged experimental. chat.plugins.extraMarketplaces is policy-only (included: there's no user-writable surface for it.false)

Architecture

  • No new AccountPolicyService's existing policy.value(policyData) callback flow handles everything natively.IPolicyService
  • DefaultAccountProvider fetches in parallel with getTokenEntitlements (shared sessions, shared IRequestService plumbing, shared 1-hour cache). 5 s request timeout.
  • Silent fallback to local-only policy on any non-2xx, network error, parse error, or missing managedSettingsUrl. An empty response ({}) is a successful "no policy file present" signal.
  • Shape adaptation in adaptManagedSettings (services/accounts/browser/managedSettings.ts): the API's Record<id, { source }> for extraKnownMarketplaces becomes an IExtraKnownMarketplaceEntry[], preserving the marketplace name so enabledPlugins["<plugin>@<marketplace>"] keys resolve consistently.
  • chat.plugins.extraMarketplaces stored as { [name]: url-or-shorthand } dict, which lets the Settings Editor's ComplexObject renderer display entries inline (read-only) when managed by policy.
  • Forward-compatible unknown response keys are silently ignored, malformed entries are skipped with a warn.parsing
  • Consumers union user + policy via a shared readConfiguredMarketplaces helper in marketplaceReference.ts. chat.plugins.marketplaces (user) and chat.plugins.extraMarketplaces (policy) are merged; parseMarketplaceReferences dedupes by canonical id.
  • Canonical-id parity: GitHub URL form (https://github.com/owner/repo.git), SSH form (git@github.com:owner/repo.git), and shorthand (owner/repo) all produce the same canonical id, so policy trust comparisons match regardless of which form the policy uses.
  • Strict marketplace when chat.plugins.strictMarketplaces is on, isMarketplaceTrusted derives trust only from chat.plugins.extraMarketplaces (policy slot). User-added marketplaces in chat.plugins.marketplaces do not grant trust under strict mode.mode
  • Plugin ChatEnabledPlugins is an allowlist; combined with strict mode it gates both marketplace-discovered plugins and Copilot-CLI-installed plugins (matched by install-path <marketplace>/<plugin> identity).enforcement

Rate limiting

The shared DefaultAccountProvider.request() applies a Retry-After-aware backoff that benefits every /copilot_internal/* call (entitlements, token entitlements, MCP registry, managed settings). Detects:

  • canonical 429 Too Many Requests
  • 403 with X-RateLimit-Remaining: 0 (primary quota exhaustion)
  • any non-2xx carrying Retry-After (secondary throttling)

Default 60 s when Retry-After is absent. Mirrors the public-API pattern in githubRepoFetcher. readHeaderandretryAfterFromHeadersare exported fromplatform/request/common/request.ts and shared by both call sites.ts

Telemetry

New event defaultaccount:managedSettings:fetch (owner: joshspicer):

  • outcome: ok / no-response / parse-error / status:NNN
  • rateLimitBackoffActive: whether the call was short-circuited by a prior Retry-After window

Not fired for no-url (would be noise from every non-Copilot environment).

Diagnostics

Developer: Policy Diagnostics (Cmd+Shift+P) grows a ## Managed Settings section showing:

  • fetch status (HTTP code / ok / no-response / parse-error / no-url)
  • last-fetched timestamp
  • a JSON dump of the applied managed-settings policy slice

Developer: Sync Account Policy invalidates the cache and refetches.

Tests

  • src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts covers adaptManagedSettings shape transformations (empty / partial / full / github vs git sources / ref handling) and resilience to malformed/unknown payloads.
  • src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts covers the new readConfiguredMarketplaces / extraKnownMarketplacesToConfigDict helpers, host-only git URLs, and the canonical-id parity guarantee for GitHub URL vs shorthand.
  • Existing mock implementations of IDefaultAccountService / IDefaultAccountProvider updated for the new readonly fields.

Distro

Companion vscode-distro PR: microsoft/vscode-distro#1422 adds managedSettingsUrl to mixin/{stable,insider,exploration}/product.json.

Out of scope

  • Org/repo policy delivery layers.
  • Hooks / disableAllHooks.
  • Server-side merging.
  • Auto-install of enterprise-enabled plugins (the SHOULD in the deferred as a follow-on.ADR)

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Copilot AI review requested due to automatic review settings May 27, 2026 19:39
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements ADR-002 enterprise-managed Copilot client settings by fetching /copilot_internal/managed_settings via the default account flow and surfacing the resulting values through the existing IPolicyService/AccountPolicyService policy pipeline, so enterprise .github/copilot/settings.json can influence chat plugin enablement and marketplace trust.

Changes:

  • Add managed settings fetch + adaptation (adaptManagedSettings) to DefaultAccountProvider, including shared Retry-After-aware backoff for /copilot_internal/* calls.
  • Introduce three new policies wired to chat settings: ChatEnabledPluginschat.pluginLocations, ChatPluginMarketplaceschat.plugins.marketplaces, and ChatStrictMarketplaceschat.plugins.strictMarketplaces.
  • Update plugin marketplace + discovery consumers to merge default + user + policy layers, add Policy Diagnostics output, and add unit tests for the managed settings adaptation.
Show a summary per file
File Description
src/vs/workbench/services/accounts/test/browser/defaultAccount.test.ts Adds unit tests for adaptManagedSettings transformations and dedup/ref handling.
src/vs/workbench/services/accounts/browser/defaultAccount.ts Fetches managed settings in parallel with token entitlements; adapts response into IPolicyData; adds Retry-After backoff logic.
src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts Merges default/user/policy marketplaces for fetch; adds strict trust mode based on configured marketplaces.
src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts Merges default/user/policy for plugin locations via observable; adds plugin-id resolution support.
src/vs/workbench/contrib/chat/common/constants.ts Adds ChatConfiguration.StrictMarketplaces key.
src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts Registers new strict marketplaces setting and wires three new policies to existing/new settings.
src/vs/workbench/browser/actions/developerActions.ts Extends Policy Diagnostics with an ADR-002 managed settings section.
src/vs/base/common/product.ts Extends IDefaultChatAgent with managedSettingsUrl.
src/vs/base/common/defaultAccount.ts Extends IPolicyData with managed settings-derived fields.
product.json Adds default managedSettingsUrl pointing to GitHub.com API endpoint.

Copilot's findings

  • Files reviewed: 10/10 changed files
  • Comments generated: 5

Comment thread src/vs/workbench/services/accounts/browser/defaultAccount.ts Outdated
Comment thread src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts Outdated
Comment thread src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts Outdated
Comment thread src/vs/workbench/browser/actions/developerActions.ts Outdated
Comment thread src/vs/base/common/product.ts
@joshspicer
Copy link
Copy Markdown
Member Author

Distro PR: microsoft/vscode-distro#1422 — adds managedSettingsUrl to defaultChatAgent in all product flavors (stable/insider/exploration).

@joshspicer joshspicer force-pushed the agents/users-josh-git-agent-control-plane-docs-adrs-dfeea4a4 branch from 22197b7 to 06cba15 Compare May 28, 2026 17:37
joshspicer and others added 2 commits May 28, 2026 10:39
…tMarketplaces settings

Adds three new chat.plugins.* settings, each policy-backed:

- chat.plugins.enabledPlugins (policy:  objectChatEnabledPlugins)
  mapping plugin IDs (`<plugin>@<marketplace>`) to enable/disable.
- chat.plugins.marketplaces (policy:  array ofChatPluginMarketplaces)
  marketplace references (GitHub shorthand or Git URI). User entries
  survive alongside policy entries.
- chat.plugins.strictMarketplaces (policy: ChatStrictMarketplaces)
  boolean restricting trust to listed marketplaces only.

All three are gated on `tags: ['experimental']`. Consumers (plugin
discovery, install, URL handler, marketplace service, quick-pick action)
now read via `inspect()` so default + user + policy layers all flow
through. A shared `readConfiguredMarketplaces` helper in
marketplaceReference.ts dedups the inspect pattern across 5 sites.

Adds three matching fields to IPolicyData so the policy framework has
slots to fill in once the wiring lands; until then they're undefined and
behave like an empty policy (no-op). Plugin discovery now distinguishes
filesystem-path entries (removable from UI) from enterprise plugin IDs
(non-removable) via a single shared loop; `IAgentPlugin.remove` is
optional accordingly.

build/lib/policies/policyData.jsonc regenerated for the new policy keys.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…wiring

Wires the previously-added chat.plugins.* policy slots to the new
`/copilot_internal/managed_settings` endpoint on the authenticated
Copilot host.

Core behavior in DefaultAccountProvider:
- Fetches managed_settings alongside entitlements; shares the 1-hour
  cache used by other account-policy fetches.
- Silent fallback to local-only policy on any non-2xx, network error,
  parse error, or missing managedSettingsUrl.
- Rate-limit-aware: backs off all /copilot_internal/* calls when the
  endpoint signals 429, 403 + X-RateLimit-Remaining: 0, or any non-2xx
  with Retry-After.
- adaptManagedSettings flattens the API's structured
  extraKnownMarketplaces map into the existing string-array shape that
  chat.plugins.marketplaces consumes; tolerates malformed entries and
  unknown response keys (forward-compatible).
- Telemetry: emits `defaultaccount:managedSettings:fetch` (owner:
  joshspicer) with an `outcome` bucket (ok / no-response / parse-error /
  status:NNN) and a `rateLimitBackoffActive` flag.

Surface area:
- IDefaultAccountProvider/Service expose managedSettingsFetchStatus and
  managedSettingsFetchedAt; ManagedSettingsFetchStatus is a named union.
- Developer: Policy Diagnostics shows a Managed Settings section with
  the URL status, last-fetched timestamp, and a JSON dump of the
  applied managed-settings policy slice.
- product.json adds a managedSettingsUrl key (populated via distro).

Refactor: `readHeader` and `retryAfterFromHeaders` are moved to
`platform/request/common/request.ts` so githubRepoFetcher.ts and this
new code share one implementation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@joshspicer joshspicer force-pushed the agents/users-josh-git-agent-control-plane-docs-adrs-dfeea4a4 branch from 06cba15 to 145bcd1 Compare May 28, 2026 17:40
@joshspicer joshspicer changed the title policy: implement ADR-002 enterprise managed_settings for Copilot clients policy: enterprise managed_settings for Copilot clients May 28, 2026
Pulls in managedSettingsUrl from microsoft/vscode-distro#1422.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 30/30 changed files
  • Comments generated: 3

Comment thread src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts Outdated
Comment thread src/vs/workbench/services/accounts/browser/defaultAccount.ts
Comment thread src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts Outdated
- Restore historical default for chat.plugins.marketplaces
  (['github/copilot-plugins', 'github/awesome-copilot#marketplace']) so
  existing users don't lose the two built-in marketplaces on update.
  Regenerate policyData.jsonc accordingly.

- Seed _managedSettingsFetchStatus = 'ok' on cache-hit so Policy
  Diagnostics reports the applied state after a process restart that
  warm-starts from cached policyData (instead of stuck at 'not yet
  fetched').

- Scope the <plugin>@<marketplace> ID-resolution rule to the enterprise
  ChatEnabledPlugins setting only. User-typed entries in
  chat.pluginLocations that happen to contain '@' are now treated as
  filesystem paths, as a user would expect, not silently rewritten to
  ~/.copilot/installed-plugins/<x>/<y>/. Split _resolvePluginPath into
  a path-only resolver and a dedicated _resolveEnterprisePluginId.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@joshspicer joshspicer marked this pull request as ready for review May 28, 2026 18:18
joshspicer and others added 5 commits May 28, 2026 11:26
chat.pluginLocations has no policy slot, so observableConfigValue
(which uses getValue() under the hood) is functionally equivalent to
the hand-rolled inspect() version. Reverting reduces diff  thechurn
inspect-based observable is now used only for _enterpriseEnabledPluginsConfig
where the default+user+policy merge actually matters.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds chat.plugins.extraMarketplaces (ChatExtraMarketplaces policy,
included: false so it's hidden from the Settings UI). This receives the
'extraKnownMarketplaces' payload from the managed_settings API.

Restores chat.plugins.marketplaces to its pre-PR shape: no policy slot,
no inspect()-juggling required in consumers, no risk of accidentally
clobbering user data. Users write to chat.plugins.marketplaces; the
enterprise writes to chat.plugins.extraMarketplaces; the effective set
is the union.

Consumer simplifications:
- readConfiguredMarketplaces returns { userValues, extraValues,
   two getValue() reads, no inspect() needed.effectiveValues }
- Write-back is now just [...userValues, refValue] in all three sites.
- 'Manage Plugin Marketplaces' still surfaces the 'managed by enterprise
  policy' badge by checking ref membership in extraValues.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- fetchMarketplacePlugins: drop the over-engineered pre-dedup-by-string;
  parseMarketplaceReferences already dedups by canonical id.
- agentPluginServiceImpl: pass source.remove directly to _toPlugin instead
  of wrapping in a null-asserted closure.
- adaptManagedSettings: use a Set for flatten-and-dedup (insertion order
  is preserved).
- getDefaultAccountFromAuthenticatedSessions: spread merge instead of
  three explicit field assignments.
- developerActions: collapse the 'ok' branch into the catch-all backtick
  wrap; same behavior, less code.
- marketplaceReference.ts: tighter JSDoc on IConfiguredMarketplaces.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…scovery

Previously the enterprise-managed policy values were delivered into the
policy framework but not  a plugin already installed locallyenforced
(e.g. via the marketplace discovery path) would remain active even when
the policy excluded it or strict-marketplace mode rejected its source.

Adds policy enforcement on AgentPluginService.plugins, applied after
discovery dedup/sort and gated by two observables:

- ChatEnabledPlugins policy: when set, filters the surfaced plugin set
  to only those whose '<name>@<marketplace>' ID appears in the policy
  map with value true. Plugins without a marketplace provenance
  (filesystem entries from chat.pluginLocations) are unaffected.

- ChatStrictMarketplaces: when on, filters out plugins whose source
  marketplace is not trusted. Trust is sourced ONLY from
  chat.plugins.extraMarketplaces (the policy-only  user-setslot)
  entries in chat.plugins.marketplaces do NOT grant trust under strict
  mode. This matches the ADR-002 semantics: strict mode hands full
  marketplace control to the enterprise.

Also updates the chat.plugins.strictMarketplaces description text to
match the new behavior (was still pointing at the user setting).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Moves IManagedSettingsResponse and adaptManagedSettings out of
defaultAccount.ts and into a new managedSettings.ts in the same folder.
Adapter is a pure transformation function with no service dependencies,
so it belongs in its own file alongside the HTTP/wiring code.

Renames the test file to managedSettings.test.ts to match what it
actually tests and tightens the suite name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@joshspicer joshspicer marked this pull request as draft May 28, 2026 19:00
joshspicer and others added 2 commits May 28, 2026 12:05
…scription

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Blocked plugins (ChatEnabledPlugins / strict marketplaces) now stay
visible but are forced disabled via their enablement observable, and the
enable affordance notifies the user instead of re-enabling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
joshspicer and others added 4 commits May 28, 2026 14:47
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…anges

pluginMarketplaceService.onDidChangeMarketplaces only listened for
PluginsEnabled and PluginMarketplaces config changes, so the
ExtraMarketplaces values delivered by the ChatExtraMarketplaces policy
never triggered a  the union was stale until the next user editrefetch
to chat.plugins.marketplaces or a workspace-trust change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tings

Move the enterprise-managed marketplace entry type out of defaultAccount.ts
into a dedicated managedSettings.ts so the type lives alongside other
managed-settings-specific code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Sync policyData.jsonc ChatExtraMarketplaces description with the
  source declaration in chat.shared.contribution.ts (object-form
  entries were missing from the policy artifact).
- Reorder Event import in agentPluginServiceImpl.ts to keep base/common
  imports alphabetical.
- Fix stale doc reference (COPILOT_CLI_INSTALLED_PLUGINS_DIR -> the
  function it actually mirrors).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

Base: 60525f78 Current: ce56e402

No screenshot changes.

joshspicer and others added 4 commits May 28, 2026 15:17
ADR-002 describes the `git` source `url` as a free-form `(string)`
the example happens to be a full clone URL, but the schema doesn't
require a repo path. Our marketplace-URI parser was rejecting host-only
HTTPS endpoints (e.g. `https://plugins.internal.example.com`), so
enterprise policy entries with marketplace-registry-style URLs were
silently dropped before they ever reached the UI.

Relax `parseUriMarketplaceReference` to accept host-only URLs and
treat them as a marketplace endpoint identified by host alone. The
canonical id becomes `git:<host>/` so distinct hosts still dedupe
correctly. Existing path-aware behavior is preserved unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…; fix test cloneUrl expectation

- Handle string-typed entries in extraKnownMarketplaces (IPolicyData allows string | IExtraKnownMarketplaceEntry)
- Fix test expectation: URI.parse normalizes host-only URLs to include trailing slash

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The setting schema is now `{ [name]: url-or-shorthand }` (object), so
readConfiguredMarketplaces must convert each entry to the nested
IExtraMarketplaceObjectEntry shape that parseMarketplaceReferences expects.
Uses a regex to detect GitHub shorthand (owner/repo[#ref]) vs URI.

 TypeError in CI:
'extraValues is not iterable' on [...userValues, ...extraValues].

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ssion tests for Settings Editor display

Extract the policy.value conversion for ChatExtraMarketplaces out of
chat.shared.contribution.ts into a reusable, unit-testable helper. The
helper converts the IExtraKnownMarketplaceEntry[] policy payload into the
{ [name]: url-or-shorthand } dict that:
  - the Settings Editor's ComplexObject renderer can display inline as
    key/value rows (instead of just 'Edit in settings.json'), and
  - readConfiguredMarketplaces reverses back into IExtraMarketplaceObjectEntry[]
    so parseMarketplaceReferences preserves displayLabel = name.

Tests added:
 undefined
 owner/repo
 owner/repo#ref
 raw URL (+ optional #ref)

    parseMarketplaceReferences flow (the regression test that catches the
    'extraValues is not iterable' bug we just hit in CI)
  - schema-shape: chat.plugins.extraMarketplaces is registered with
    type=object + additionalProperties.type=['string'], the exact shape
    the Settings Editor requires to render as ComplexObject

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-form entries

url dict, policy
entries always reach the marketplace fetcher as IExtraMarketplaceObjectEntry
objects (not strings). The validation loop was only accepting strings,
producing a 'Ignoring invalid marketplace entry: [object Object]' debug log
for every valid policy entry.

Validate using parseMarketplaceObjectEntry for object values so the warning
fires only for genuinely-unparseable entries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 35/35 changed files
  • Comments generated: 0 new

joshspicer and others added 3 commits May 29, 2026 08:04
…on commands

The schema-shape test for chat.plugins.extraMarketplaces imported the full
chat.shared.contribution module to populate the configuration registry.
This re-registered commands (already registered by the workbench under
test), producing 'Cannot register two commands with the same id:
workbench.action.chat.markHelpful' and cascading disposable leaks in
unrelated suites (EditorService, WorkingCopyBackupTracker).

The other 5 tests (extraKnownMarketplacesToConfigDict + end-to-end round
trip) cover the actual behavior that broke; the schema shape is exercised
implicitly by the round-trip test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…onical id

Plugin marketplace trust under strict mode compares canonicalId. A plugin
discovered from 'https://github.com/microsoft/vscode-team-kit.git' was
being blocked even though 'microsoft/vscode-team-kit' was in the trusted
list, because the URI parser produced 'git:github.com/microsoft/vscode-team-kit.git'
while the shorthand parser produced 'github:microsoft/vscode-team-kit'.

When parseUriMarketplaceReference / parseScpMarketplaceReference detect a
github.com authority, emit the same canonical id form the shorthand parser
uses so all three forms (shorthand, https URI, SCP) collapse to a single
trusted reference.

Existing dedup test now expects 1 entry instead of 2; ref-distinction test
collapses the https+#ref entry with its shorthand sibling. Added a focused
regression test asserting all four forms produce identical canonical ids.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@joshspicer joshspicer marked this pull request as ready for review May 29, 2026 17:06
@joshspicer joshspicer mentioned this pull request May 29, 2026
10 tasks
@joshspicer joshspicer merged commit b24c5e3 into main May 29, 2026
39 of 40 checks passed
@joshspicer joshspicer deleted the agents/users-josh-git-agent-control-plane-docs-adrs-dfeea4a4 branch May 29, 2026 21:03
@vs-code-engineering vs-code-engineering Bot added this to the 1.123.0 milestone May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants