Skip to content

fix: enforce FileHelper policy for shared infrastructure - #4951

Open
mnriem wants to merge 9 commits into
github:mainfrom
mnriem:mnriem-filehelper-shared-infrastructure
Open

mnriem wants to merge 9 commits into
github:mainfrom
mnriem:mnriem-filehelper-shared-infrastructure

Conversation

@mnriem

@mnriem mnriem commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

Description

Migrate project-side filesystem operations owned by install_shared_infra() and refresh_shared_templates() to explicit, root-scoped FileHelper operations, including their shared manifest reads, hashes, recording and saves.

Denied symlink destinations previously warned and silently skipped installation, even with force. They now fail explicitly. Allowed contained targets retain link identity and existing missing-only, managed/unmodified refresh, customization/recovery and force-overwrite decisions. Stale leaf links remain preserved customizations.

The bounded migration adds policy-aware metadata access and explicit per-operation mutation observers, optional helper-backed manifest I/O, and helper handoff from init and integration install/upgrade/switch. Init uses the current requested flag before persistence; other consumers read current project policy. Bootstrap config hierarchy remains deny-only.

Lifecycle changes validate exactly the shared scripts/templates directory scopes and managed .specify/.gitignore file scope through the selected helper before nofollow census. File rollback journals actual contained targets; separate directory bookkeeping removes only identified newly created empty directories, without claiming their contents. Installer/journal containment checks, archive verification, locking, extension/preset registration and integration uninstall remain unchanged. External adapter transactions still reject linked .specify/integrations scope roots.

The baseline includes persisted-policy merge 117fb577fde50d4b787748d24877743367c25aa5. This PR changes 17 files: 11 source, five test files and design/file-helper.md; it does not change template or script content.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (if applicable)

The existing suite was run with the worktree-owned virtualenv instead of the checklist's generic pytest invocation. Public adapter and real lifecycle tests exercise temporary projects; no additional manual sample-project run was performed.

Command Result
uv sync --extra test --quiet Passed; restored the initially missing worktree virtualenv
uv run specify --help Passed
.venv/bin/python -m pytest tests/specify_cli/test_shared_infra.py -q before production changes, with the initial six deny regressions Six failures: DID NOT RAISE SymlinkDeniedError, establishing the prior skip-success behavior
.venv/bin/python -m pytest tests/specify_cli/test_shared_infra.py -k real_lifecycle -q before lifecycle fixes Five failed, three passed; exposed linked scope rejection and leaked new target parents
.venv/bin/python -m pytest tests/specify_cli/test_shared_infra.py tests/test_file_helper.py tests/integrations/test_cli.py tests/test_shared_infra_gitignore.py -q 383 passed, 61 skipped
.venv/bin/python -m pytest tests/specify_cli/integrations/test_installed_adapters.py -k 'shared_helper_allowed_links or current_deny_flag' -q 14 passed
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests/specify_cli/integrations/test_installed_adapters.py tests/specify_cli/integrations/test_command_switch.py tests/specify_cli/test_shared_infra.py -k 'external_init_package_directories or package_post_copy_failure or rejects_symlinked_parent or shared_helper_allowed_links or current_deny_flag or real_lifecycle or directory_' -q 112 passed
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests -q 12,348 passed, 79 skipped, 62 warnings
.venv/bin/python -m pytest --collect-only -q Collection increased from 12,271 to 12,427, with no decrease
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests/specify_cli/test_shared_infra.py tests/test_file_helper.py tests/specify_cli/integrations/test_installed_adapters.py -q -k 'shared_helper_allowed_links or current_deny_flag or real_lifecycle or directory_ or stat or observer or manifest or stale or allowed_links' Independent parent review: 172 passed, eight skipped, 864 deselected
uvx ruff@0.15.0 check src tests All checks passed using the CI-pinned version
git diff --check Passed

Regression coverage includes leaf/parent links, contained/external/dangling/cyclic targets, nofollow metadata, missing/nondirectory parents, callback ordering/failures, overwrite/recovery decisions, manifest formatting and policy errors, stale cleanup, current init flag precedence, and real external-lifecycle rollback restoring bytes/modes and preserving links and unrelated directory contents.

Validation was local macOS. Portable Windows/reparse and newline cases are covered; native Windows filesystem/junction cases were skipped and require CI validation. These checks do not claim race-free filesystem confinement. Earlier unpinned Ruff diagnostics differed from the CI-pinned tool; pinned Ruff passes.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (fill in the disclosure below)

AI disclosure: GitHub Copilot, powered by GPT-6.1 Sol (gpt-6.1-sol), implemented the code, tests and documentation, ran validation, and authored the commit and PR text on behalf of @mnriem. The agent executed autonomously within user-approved scope and explicit design approval gates, with coordinating-agent technical review; this is not a claim of human line-by-line review. Reasoning effort used the runtime setting and was not explicitly selected in the request.

Route shared infrastructure and scoped manifest operations through explicit project helpers. Preserve allowed link identity and lifecycle rollback without recursively claiming newly created directories.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 084bd222-8ae8-474a-9dcc-6e6c44e031f4
Copilot AI balanced review requested due to automatic review settings October 13, 2026 17:58

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.

🟡 Changes recommended

Template-only refresh can incorrectly fail because helper-backed manifest checks traverse unrelated tracked scripts.

1 open finding
What changed in this PR

Migrates shared infrastructure filesystem operations to policy-aware FileHelper access with transactional rollback support.

Changes:

  • Adds policy-aware metadata and mutation observers.
  • Routes shared manifests and lifecycle commands through scoped helpers.
  • Adds extensive symlink, rollback, and policy regression coverage.
File Description
design/​file-helper.md Documents shared-infrastructure policy and observers.
src/​specify_cli/​__init__.py Passes helpers through shared wrappers.
src/​specify_cli/​command_init.py Uses the current init symlink flag.
src/​specify_cli/​file_helper.py Adds scoped stat and mutation observers.
src/​specify_cli/​shared_infra.py Migrates shared filesystem operations.
src/​specify_cli/​integrations/​_file_changes.py Adds directory observation.
src/​specify_cli/​integrations/​_helpers.py Propagates project policy.
src/​specify_cli/​integrations/​_lifecycle.py Journals helper mutations and directories.
src/​specify_cli/​integrations/​command_install.py Supplies the project helper.
src/​specify_cli/​integrations/​command_switch.py Supplies the project helper.
src/​specify_cli/​integrations/​command_upgrade.py Supplies the project helper.
src/​specify_cli/​integrations/​manifest.py Adds optional helper-backed manifest I/O.
tests/​integrations/​test_cli.py Updates shared-infrastructure failure expectations.
tests/​specify_cli/​integrations/​test_command_switch.py Verifies denied links abort switching.
tests/​specify_cli/​integrations/​test_installed_adapters.py Covers public lifecycle rollback and policy.
tests/​specify_cli/​test_shared_infra.py Adds comprehensive shared-infrastructure regressions.
tests/​test_file_helper.py Covers stat and observer behavior.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/specify_cli/shared_infra.py Resolved
Restrict manifest modification queries to destinations selected by the refresh caller. Preserve default queries and unrelated tracked entries without expanding FileHelper or lifecycle responsibilities.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 084bd222-8ae8-474a-9dcc-6e6c44e031f4
Copilot AI balanced review requested due to automatic review settings October 13, 2026 18:31
@mnriem

mnriem commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 5507872999 in bfe1311.

The refresh caller now restricts modification checks to the destinations it actually selects. Unrelated tracked scripts and other excluded entries are not inspected; their recorded hashes and ownership/recovery records are preserved. The optional manifest query filter leaves default consumers unchanged. FileHelper remains domain-neutral symlink-aware CRUD; this correction changes neither its implementation nor the lifecycle implementation. Template-specific documentation is on the refresh function, not added to the FileHelper contract.

Regression evidence: the pre-fix test failed because refresh inspected an unrelated tracked script. The new matrix covers seven excluded path categories, deny/allow, default/force, and contained/dangling/external/cyclic links, while selected denied paths still fail explicitly.

Local validation on the published correction:

  • LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests/specify_cli/test_shared_infra.py tests/integrations/test_manifest.py tests/integrations/test_cli.py tests/specify_cli/integrations/test_installed_adapters.py tests/specify_cli/integrations/test_command_switch.py -q: 1,079 passed.
  • uvx ruff@0.15.0 check src tests: all checks passed.
  • .venv/bin/python -m pytest --collect-only -q: 12,543 collected, up 116 from the previous PR head.
  • git diff --check: passed before commit.

CI/CD was not awaited, as explicitly requested; this comment makes no claim about its result. Validation is local macOS, not native Windows.

AI disclosure: posted on behalf of @mnriem by GitHub Copilot using GPT-6.1 Sol (gpt-6.1-sol), autonomous execution within user-approved scope and design gates, with runtime reasoning settings not explicitly selected. The agent authored the bounded code/tests/docs correction, ran local validation, and authored the commit and this summary; no human line-by-line review is claimed.

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.

🟡 Changes recommended

Public install/switch coverage and consistent lifecycle design documentation are still needed.

1 open finding
1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Low severity Add positive install and switch tests for persisted symlink policy

tests/​specify_cli/​integrations/​test_installed_adapters.py:192

This public-lifecycle regression only covers init and upgrade, so the new persisted-policy handoffs in integration install and integration switch remain unverified. A deny-only switch test cannot catch either command accidentally constructing a deny-mode helper or dropping files; add positive public-path cases showing those commands update a contained linked shared target when the project policy allows symlinks, as required by CONTRIBUTING.md:227-240.

🧠 Review effort: Balanced

Comment thread design/file-helper.md
Comment on lines +247 to +254
The integration lifecycle journals file/package mutations through its existing
observer and tracks directory creation separately without claiming contents.
Rollback restores observed target bytes/modes while preserving links, then
removes only identified empty new directories; unrelated contents survive.
Shared scope census retains lexical link entries and does not traverse linked
scope roots or descendants. Other adapter/package/registry/lock restrictions
are unchanged; operational permission does not waive those boundaries.
In particular, external integration transactions still reject a linked

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in commit 09ddb195266f9f237f11e577f7ab357cab9adf51. The integration lifecycle document now explicitly distinguishes FileHelper-owned shared infrastructure from the other host write helpers: shared operations honor the selected deny/allow setting, while adapter-package/registry validation, configuration bootstrap, legacy manifest consumers, and uninstall keep their existing restrictions.

This documents the caller behavior already implemented in this PR; it does not change FileHelper's symlink-aware CRUD contract or add ownership, overwrite, rollback, or concurrency responsibilities. No further code change is needed for this finding.

AI disclosure: reply posted on behalf of @mnriem by GitHub Copilot using GPT-6.1 Sol (gpt-6.1-sol), autonomous execution under the requested scope, with the runtime reasoning setting not explicitly selected. The agent verified the existing documentation correction and authored this reply; no human line-by-line review is claimed.

Add public install/switch allow and deny cases while preserving existing overwrite behavior. Qualify integration lifecycle documentation for the already-migrated shared infrastructure without changing FileHelper or runtime code.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 084bd222-8ae8-474a-9dcc-6e6c44e031f4
Copilot AI balanced review requested due to automatic review settings October 13, 2026 18:50

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.

🟡 Changes recommended

Stale-path policy failures can occur after shared files have already been overwritten, leaving non-transactional installs partially updated.

2 open findings

🧠 Review effort: Balanced

Comment thread src/specify_cli/shared_infra.py Resolved
Check stale-path policy during shared infrastructure planning so static policy failures precede file overwrites. Retain post-write leaf and ownership checks, without changing FileHelper or lifecycle behavior.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 084bd222-8ae8-474a-9dcc-6e6c44e031f4
Copilot AI balanced review requested due to automatic review settings October 13, 2026 19:11

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.

🟡 Changes recommended

Stale manifest validation can permit drive-relative path escape on Windows.

2 open findings
1 resolved since last review

🧠 Review effort: Balanced

Comment thread src/specify_cli/shared_infra.py Resolved
Reject anchored native path components before constructing stale shared destinations. Add portable Windows parsing regressions and preserve valid POSIX colon filenames without changing FileHelper or lifecycle code.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 084bd222-8ae8-474a-9dcc-6e6c44e031f4
Copilot AI balanced review requested due to automatic review settings October 13, 2026 19:34

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.

🟡 Changes recommended

Observed directory creation can misattribute and later remove a concurrently created directory.

2 open findings
1 resolved since last review

🧠 Review effort: Balanced

Comment thread src/specify_cli/file_helper.py Resolved
@mnriem mnriem added the triage-can-wait Verdict: valid and in-scope but deprioritized; held behind the evidence gate label Oct 14, 2026
@mnriem
mnriem requested a balanced review from Copilot October 14, 2026 14:04

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.

🔵 Needs a closer look

Security-sensitive filesystem and rollback behavior spans many boundaries, while native Windows junction behavior still requires CI validation.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Decode helper-written shared targets as UTF-8 and assert the missing target and shared-install failure context without relying on Unix-specific error wording.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 29, 2026 21:24
@mnriem

mnriem commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the five Windows CI failures in commit d9275bf. The affected tests now read helper-written shared targets explicitly as UTF-8, and the dangling-parent CLI assertion checks the missing target and shared-install failure context rather than Unix-specific error wording. Runtime code and the previously agreed FileHelper/lifecycle behavior are unchanged. The four encoding failures and the error-wording failure were reproduced locally before the fix; all 50 affected parameterized cases then passed with the same simulated Windows defaults. LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests/specify_cli/test_shared_infra.py tests/specify_cli/integrations/test_installed_adapters.py -q --tb=short passed all 927 tests, and focused Ruff and diff checks passed. Native Windows execution was not performed locally; CI confirmation remains pending. AI disclosure: posted on behalf of @mnriem by GitHub Copilot using GPT-6.1 Sol (gpt-6.1-sol), autonomous execution within the user-approved scope, with the runtime reasoning setting not explicitly selected. The agent authored the test-only correction, ran the local checks, and authored the commit and this summary; no human line-by-line review is claimed.

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.

🔵 Needs a closer look

Human assessment is needed because critical cleanup and switch-failure paths can delete live bundled files or leave the outgoing integration removed without installing its replacement.

1 open issue

Review effort: Balanced

Protect current bundled file identities from stale cleanup through allowed parent aliases. Validate the outgoing manifest and finish the initial shared refresh before integration teardown, preserving the existing later setup/fallback recovery behavior.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 30, 2026 12:15
@mnriem

mnriem commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 5705232892 in commit 9362408. Both destructive paths described in the review were reproduced: an allowed stale parent alias could delete a still-bundled file, and a shared-path failure during a built-in switch could occur after outgoing integration teardown. Shared cleanup now preserves file identities belonging to current bundled destinations while still removing genuinely obsolete files, including obsolete files with identical bytes. Switch validates the outgoing manifest and completes its initial target shared refresh before teardown, preserving the outgoing files and metadata when that refresh fails. FileHelper is unchanged; later setup/fallback recovery and existing transaction boundaries are unchanged. The existing lifecycle-design clarification remains in place, and the two caller guarantees are now documented explicitly. All 29 new parameterized regression cases failed on the previous PR head and pass with this correction. Local macOS validation passed 284 shared/switch tests, 57 selected external-adapter lifecycle tests, and all 164 legacy integration CLI/manifest tests; focused Ruff and diff checks passed, and 12,610 tests collect. Native Windows confirmation is left to CI. AI disclosure: posted on behalf of @mnriem by GitHub Copilot using GPT-6.1 Sol (gpt-6.1-sol), autonomous execution within the user-approved scope, with the runtime reasoning setting not explicitly selected. The agent investigated the review, authored the code/tests/docs correction, ran local checks, and authored the commit and this summary; no human line-by-line review is claimed.

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.

🔵 Needs a closer look

Human assessment is needed because linked-path and overlapping-destination interactions can leave permissions changed after rollback or managed-file manifests inconsistent with disk.

1 open issue

Review effort: Balanced

Observe execute-bit adjustments at their actual file targets so lifecycle rollback restores prior permissions, including preserved scripts beneath allowed directory links. Record written shared destinations after the full write phase so overlapping aliases retain hashes for the final bytes without changing write order or ownership decisions.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 30, 2026 14:27
@mnriem

mnriem commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 5712164388 in commit ba88c03. Both concerns were reproduced on the reviewed implementation: execute bits on preserved scripts remained changed after rollback, and earlier manifest hashes became stale when later writes reached the same target through an allowed alias. Host execute-bit changes now participate in the existing journal using the actual file target, including preserved user scripts beneath linked shared roots, without adding uninstall ownership. Shared installation and template-only refresh now record written destinations after their full write phase; write order, overwrite/customization decisions, and unselected manifest entries remain unchanged. FileHelper and transaction boundaries are unchanged, and the related design guidance is updated. Regression evidence covers direct and linked permission targets, successful completion and late failure, public external init/upgrade rollback after chmod, overlapping template aliases, and overlapping script/template destinations. Local macOS validation passed 262 shared-infrastructure/public-rollback cases, 57 selected external lifecycle cases, 210 switch/legacy CLI/manifest cases, and three existing extension execute-bit cases; focused Ruff and diff checks passed, and 12,624 tests collect, including 14 new cases. Native Windows confirmation is left to CI. AI disclosure: posted on behalf of @mnriem by GitHub Copilot using GPT-6.1 Sol (gpt-6.1-sol), autonomous execution within the user-approved scope, with the runtime reasoning setting not explicitly selected. The agent investigated the review, authored the code/tests/docs correction, ran local checks, and authored the commit and this summary; no human line-by-line review is claimed.

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.

🔵 Needs a closer look

Human assessment is needed because shared symlink targets can cause preserved files to be overwritten without --force, while linked scripts may miss executable-permission repair.

1 open issue

Review effort: Balanced

Protect preserved selected file identities from alias writes unless forced. Repair allowed shared script links through explicit helper handoffs, policy-checked directory reads, bounded shebang reads and observed mode updates. Keep selection, ownership, executable-bit decisions and rollback in the callers.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 30, 2026 17:08
@mnriem

mnriem commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 5713833716 in commit 13f469e. Both reported interactions were reproduced, with 11 failing regression cases on the reviewed implementation. Without force, shared installation and template refresh now protect preserved selected file identities from writes through other selected aliases; customized, recovered and untracked targets remain intact while unrelated destinations still refresh. Force retains its existing overwrite behavior, and selected hard-linked entries are conservatively preserved too. Shared-script permission repair now follows allowed contained leaf and parent links, avoids revisiting directory identities, and reports denied, external, dangling, cyclic or unsupported accessed links without modifying them. Init/install/switch/upgrade explicitly pass the operation helper. FileHelper gains only neutral directory-name reads, bounded byte reads and observed regular-file mode updates; path selection, preservation, executable-bit decisions, ownership and rollback remain in callers. Extension-script scanning retains its legacy behavior, and the applicable design guidance is updated. Local macOS validation passed 531 foundation/shared-infrastructure cases, all 698 external-adapter lifecycle cases, 276 integration command/legacy manifest cases, 56 init cases and three extension execute-bit cases; 79 platform-specific cases were skipped. Focused Ruff and diff checks passed; 12,711 tests collect, up 87 from the reviewed head. Native Windows confirmation is left to CI. AI disclosure: posted on behalf of @mnriem by GitHub Copilot using GPT-6.1 Sol (gpt-6.1-sol), autonomous execution within the user-approved scope, with the runtime reasoning setting not explicitly selected. The agent investigated the review, authored the code/tests/docs correction, ran local checks, and authored the commit and this summary; no human line-by-line review is claimed.

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.

🟡 Changes recommended

Permission repair duplicates relative project paths when locating shared scripts, leaving them non-executable for callers such as ExtensionManager.

2 open issues

Review effort: Balanced

from .file_helper import FileHelperError
from .integrations._file_changes import changing_file, observe_file_change

files = files if files is not None else project_files(project_path)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage-can-wait Verdict: valid and in-scope but deprioritized; held behind the evidence gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants