Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ad4628f
feat(sync): negotiate encryption per peer (WIP)
pjwerneck Oct 7, 2026
644ea00
fix(sync): address review findings in encryption negotiation
pjwerneck Oct 7, 2026
b478e76
fix(sync): hold only pending messages and reload policy on event writes
pjwerneck Oct 7, 2026
5e7f55c
fix(sync): keep required peers required and encrypt at rest before wr…
pjwerneck Oct 8, 2026
107b398
fix(sync): harden legacy policy checks, unknown peers and bundle reads
pjwerneck Oct 8, 2026
efc097b
refactor(sync): drop legacy-peer handling and trim encryption code
pjwerneck Oct 8, 2026
e460f59
fix(sync): reload the key file policy on every write at rest
pjwerneck Oct 8, 2026
a9e63e5
fix(sync): strict reads at rest, delta writes for required peers, rej…
pjwerneck Oct 8, 2026
9555080
fix(sync): accept plaintext at rest written before the pass, harden r…
pjwerneck Oct 8, 2026
6de6f37
refactor(sync): simplify encryption negotiation
pjwerneck Oct 8, 2026
967df46
fix(sync): make encryption waits and drops visible
pjwerneck Oct 8, 2026
935f0b9
refactor(sync): address review standards issues
pjwerneck Oct 8, 2026
0e299ca
Merge branch 'dev' into pjwerneck/auto-encryption-settings
pjwerneck Oct 14, 2026
cbc7f7c
fix: update model configuration to forbid extra fields in PeerStore a…
pjwerneck Oct 14, 2026
650e93b
fix: send the held job before the owner syncs; format docs and notebooks
pjwerneck Oct 14, 2026
3ca82ed
test: drop repeated encryption policy tests; fix debug enclave policy…
pjwerneck Oct 14, 2026
6934e4c
Merge remote-tracking branch 'origin/dev' into pjwerneck/auto-encrypt…
pjwerneck Oct 20, 2026
cffcb24
chore: retrigger checks
pjwerneck Oct 20, 2026
6c84133
Merge remote-tracking branch 'origin/dev' into pjwerneck/auto-encrypt…
pjwerneck Oct 20, 2026
b16fd00
Merge remote-tracking branch 'origin/pjwerneck/revert-peer-key-pinnin…
pjwerneck Oct 23, 2026
7d3159c
refactor: always create keys and keep only our identity in the key file
pjwerneck Oct 23, 2026
fc9f3d0
test: drop pin names and cases without own keys
pjwerneck Oct 23, 2026
dbe8882
refactor: import an uploaded key file at login
pjwerneck Oct 23, 2026
4a128ab
refactor: send nothing until the peer policy is known
pjwerneck Oct 23, 2026
0059cae
refactor: take the encryption policy from login only
pjwerneck Oct 23, 2026
015cd42
fix: raise when changes cannot be sent and check the published bundle
pjwerneck Oct 23, 2026
9c8ce42
refactor: remove the outdated peer handling
pjwerneck Oct 24, 2026
47bf16d
refactor: always require encryption in the enclave and type the key file
pjwerneck Oct 24, 2026
0679db9
refactor: send queued DS changes as one message, as on dev
pjwerneck Oct 24, 2026
ec1122f
fix: send queued DS changes to each DO in its own message
pjwerneck Oct 24, 2026
644c07a
test: remove the test for the removed use_encryption keyword
pjwerneck Oct 24, 2026
9fafee7
refactor: remove the off encryption policy
pjwerneck Oct 24, 2026
2ee3c48
fix: keep keys after a wipe and raise when a DO send cannot go
pjwerneck Oct 24, 2026
52e54a1
fix: check the published bundle only for pairs that encrypt
pjwerneck Oct 24, 2026
6fa9bb9
Merge branch 'dev' into pjwerneck/auto-encryption-settings
koenvanderveen Oct 26, 2026
d2e1ebd
Merge branch 'dev' into pjwerneck/auto-encryption-settings
koenvanderveen Oct 26, 2026
d0f8827
refactor: publish PeerInfo instead of adding the encryption policy to…
koenvanderveen Oct 26, 2026
a0dc5ff
Merge branch 'dev' into pjwerneck/auto-encryption-settings
koenvanderveen Oct 26, 2026
16fe1f2
Merge branch 'dev' into pjwerneck/auto-encryption-settings
koenvanderveen Oct 26, 2026
383e81f
feat: keep one encryption policy per SyftBox
koenvanderveen Oct 26, 2026
63eb2d8
Merge remote-tracking branch 'origin/pjwerneck/auto-encryption-settin…
koenvanderveen Oct 26, 2026
b3ecb96
test: fix remove-peer CLI tests and macOS /tmp paths in test folders
koenvanderveen Oct 26, 2026
8c21942
refactor: keep per-peer state on Peer, look up peer schemas and polic…
koenvanderveen Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/connections.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,25 @@ Syft uses Google Drive as its file-based transport layer. All peer-to-peer commu

---

## Encryption Policy

Each client advertises an encryption policy in `SYFT_version.json`:

| Policy | Set with | Effect |
| ---------- | -------------------------------------------- | ----------------------------------------- |
| `optional` | default on a new SyftBox, `encryption=False` | encrypt only with a peer that requires it |
| `required` | `encryption=True` | encrypt with every peer; refuse plaintext |

A pair encrypts when one side is `required`.

A SyftBox has one policy, kept in `<syftbox>/<email>/private/syft_settings.json`. The first client to start on a new SyftBox writes it: the `encryption` it logged in with, or `optional`. A later client, the notebook or syft-bg, logs in without `encryption` and gets the policy on disk. A client that asks for another policy raises `EncryptionPolicyMismatchError`, and so does a running client at its next sync when the file changed under it. Files at rest are read with the current policy, so to change the policy, delete the SyftBox (`client.delete_syftbox()`) and log in again. `syft-bg init --encryption ...` sets the policy the same way as a login.

Nothing is sent to a peer until its policy is known, that is, until it accepts and shares its version file. A scientist's change for a peer whose policy or bundle is not known is dropped, and the send raises. An owner's send to that peer raises, and the change stays pending, so a later sync sends it. An owner keeps a message it cannot read yet in the inbox, with a warning.

The format of a message must match the pair: plaintext in a pair that encrypts is refused, and so is an envelope in a pair that does not. The owner archives such a message unread, and a scientist skips it, each with a warning.

A peer whose version file has no policy, written by a syft older than `PeerInfo`, counts as `optional`. A peer whose version file is not read yet has a policy that is not known: nothing is exchanged with it.

## Peer Requests

DS creates inbox/outbox folders on their own drive and grants the DO write access. The DO discovers these folders by searching for `syft_datasite#{my_email}#` folders they don't own.
Expand Down
1 change: 1 addition & 0 deletions notebooks/ai_audit/internal/DO_Tutorial_V3.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,7 @@
"\n",
"syft_bg.init(\n",
" do_email=DO_EMAIL,\n",
" encryption=False,\n",
" token_path=str(TOKEN_PATH),\n",
" settings=settings,\n",
")\n"
Expand Down
1 change: 1 addition & 0 deletions notebooks/ai_audit/internal/test_email_e2e.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
"source": [
"syft_bg.init(\n",
" do_email=DO_EMAIL,\n",
" encryption=False,\n",
" token_path=do_token_path,\n",
" settings={\n",
" \"email_approve\": {\"gcp_project_id\": GCP_PROJECT_ID},\n",
Expand Down
2 changes: 2 additions & 0 deletions notebooks/ai_audit/internal/test_multiple_clients.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@
"source": [
"syft_bg.init(\n",
" do_email=DO_EMAIL,\n",
" encryption=False,\n",
" token_path=str(DO_TOKEN),\n",
" settings={\n",
" \"email_approve\": {\"gcp_project_id\": GCP_PROJECT_ID},\n",
Expand Down Expand Up @@ -363,6 +364,7 @@
"syft_bg.reset()\n",
"syft_bg.init(\n",
" do_email=DO_EMAIL,\n",
" encryption=False,\n",
" token_path=str(DO_TOKEN),\n",
" settings={\n",
" \"email_approve\": {\"gcp_project_id\": GCP_PROJECT_ID},\n",
Expand Down
8 changes: 5 additions & 3 deletions notebooks/ai_audit/internal/test_syft_bg.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@
"source": [
"syft_bg.init(\n",
" do_email=DO_EMAIL,\n",
" encryption=False,\n",
" token_path=do_token_path,\n",
" settings={\n",
" \"email_approve\": {\"gcp_project_id\": GCP_PROJECT_ID},\n",
Expand Down Expand Up @@ -282,7 +283,7 @@
"outputs": [],
"source": [
"syft_bg.reset()\n",
"syft_bg.init(do_email=DO_EMAIL, token_path=do_token_path)\n",
"syft_bg.init(do_email=DO_EMAIL, encryption=False, token_path=do_token_path)\n",
"\n",
"syft_bg.ensure_running(services=[\"email_approve\"])\n",
"time.sleep(3) # wait for subprocess to fail\n",
Expand All @@ -309,7 +310,7 @@
"outputs": [],
"source": [
"syft_bg.reset()\n",
"syft_bg.init(do_email=DO_EMAIL, token_path=do_token_path,\n",
"syft_bg.init(do_email=DO_EMAIL, encryption=False, token_path=do_token_path,\n",
" settings={\"email_approve\": {\"gcp_project_id\": GCP_PROJECT_ID}})\n",
"\n",
"SERVICES = [\"sync\", \"notify\", \"approve\"]\n",
Expand Down Expand Up @@ -447,6 +448,7 @@
"syft_bg.reset()\n",
"syft_bg.init(\n",
" do_email=DO_EMAIL,\n",
" encryption=False,\n",
" token_path=do_token_path,\n",
" settings={\n",
" \"email_approve\": {\"gcp_project_id\": GCP_PROJECT_ID},\n",
Expand Down Expand Up @@ -679,7 +681,7 @@
"bad_token = Path(tempfile.mktemp(suffix=\".json\"))\n",
"bad_token.write_text(json.dumps({\"invalid\": True}))\n",
"\n",
"syft_bg.init(do_email=DO_EMAIL, token_path=str(bad_token),\n",
"syft_bg.init(do_email=DO_EMAIL, encryption=False, token_path=str(bad_token),\n",
" settings={\"email_approve\": {\"gcp_project_id\": GCP_PROJECT_ID}})\n",
"\n",
"syft_bg.ensure_running(services=[\"sync\"])\n",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,18 @@
"metadata": {},
"outputs": [],
"source": [
"CRYPTO_KEYS_PATH = None\n",
"# NOTE: Re-storing Private Key from local_file, if you have it, uncomment the following line and upload the file.\n",
"KEY_FILE = None\n",
"# To restore the encryption key of an earlier session, uncomment the next two lines and upload crypto_keys.json.\n",
"# from google.colab import files; files.upload()\n",
"# CRYPTO_KEYS_PATH = \"crypto_keys.json\"\n",
"# KEY_FILE = \"crypto_keys.json\"\n",
"\n",
"benchmark_owner = login_do(encryption=True, crypto_keys_path=CRYPTO_KEYS_PATH)\n",
"print(f\" Benchmark owner : {benchmark_owner.email}\")"
"benchmark_owner = login_do(key_file=KEY_FILE)\n",
"print(f\" Benchmark owner : {benchmark_owner.email}\")\n",
"\n",
"# First login: back up the encryption key. It lives only on this runtime's filesystem.\n",
"if KEY_FILE is None:\n",
" from google.colab import files\n",
" files.download(str(benchmark_owner.syftbox_folder / benchmark_owner.email / \"private\" / \"crypto_keys.json\"))"
]
},
{
Expand All @@ -119,7 +124,7 @@
"source": [
"# # Optionally, to start from a clean SyftBox\n",
"# benchmark_owner.delete_syftbox()\n",
"# benchmark_owner._rds.peer_manager.write_own_version()"
"# benchmark_owner._rds.peer_manager.write_own_peer_info()"
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,18 @@
"metadata": {},
"outputs": [],
"source": [
"CRYPTO_KEYS_PATH = None\n",
"# NOTE: Re-storing Private Key from local_file, if you have it, uncomment the following line and upload the file.\n",
"KEY_FILE = None\n",
"# To restore the encryption key of an earlier session, uncomment the next two lines and upload crypto_keys.json.\n",
"# from google.colab import files; files.upload()\n",
"# CRYPTO_KEYS_PATH = \"crypto_keys.json\"\n",
"# KEY_FILE = \"crypto_keys.json\"\n",
"\n",
"model_owner = login_do(encryption=True, crypto_keys_path=CRYPTO_KEYS_PATH)\n",
"print(f\" Model owner : {model_owner.email}\")"
"model_owner = login_do(key_file=KEY_FILE)\n",
"print(f\" Model owner : {model_owner.email}\")\n",
"\n",
"# First login: back up the encryption key. It lives only on this runtime's filesystem.\n",
"if KEY_FILE is None:\n",
" from google.colab import files\n",
" files.download(str(model_owner.syftbox_folder / model_owner.email / \"private\" / \"crypto_keys.json\"))"
]
},
{
Expand All @@ -117,7 +122,7 @@
"source": [
"# # Optionally, to start from a clean SyftBox\n",
"# model_owner.delete_syftbox()\n",
"# model_owner._rds.peer_manager.write_own_version()"
"# model_owner._rds.peer_manager.write_own_peer_info()"
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
"source": [
"# # Optionally to clear state\n",
"# model_owner._manager.delete_syftbox()\n",
"# model_owner._manager.peer_manager.write_own_version()"
"# model_owner._manager.peer_manager.write_own_peer_info()"
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"source": [
"# # Optionally to clear state\n",
"# benchmark_owner._manager.delete_syftbox()\n",
"# benchmark_owner._manager.peer_manager.write_own_version()"
"# benchmark_owner._manager.peer_manager.write_own_peer_info()"
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
"source": [
"# # Optionally to clear state\n",
"# researcher._manager.delete_syftbox()\n",
"# researcher._manager.peer_manager.write_own_version()"
"# researcher._manager.peer_manager.write_own_peer_info()"
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"\n",
"```python\n",
"client._manager.delete_syftbox()\n",
"client._manager.peer_manager.write_own_version()\n",
"client._manager.peer_manager.write_own_peer_info()\n",
"```"
]
},
Expand Down Expand Up @@ -68,7 +68,7 @@
"\n",
"client = login_do(sync=False, load_peers=False)\n",
"client._rds.delete_syftbox()\n",
"client._rds.peer_manager.write_own_version()"
"client._rds.peer_manager.write_own_peer_info()"
]
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,16 @@
"metadata": {},
"outputs": [],
"source": [
"CRYPTO_KEYS_PATH = None\n",
"# NOTE: Re-storing Private Key from local_file, if you have it, uncomment the following line and upload the file.\n",
"KEY_FILE = None\n",
"# To restore the encryption key of an earlier session, uncomment the next two lines and upload crypto_keys.json.\n",
"# from google.colab import files; files.upload()\n",
"# CRYPTO_KEYS_PATH = \"crypto_keys.json\"\n",
"# KEY_FILE = \"crypto_keys.json\"\n",
"\n",
"model_owner = login_do(encryption=True, crypto_keys_path=CRYPTO_KEYS_PATH)\n",
"model_owner = login_do(key_file=KEY_FILE)\n",
"print(f\" Model owner : {model_owner.email}\")\n",
"\n",
"# First login: back up the encryption key — it lives only on this runtime's filesystem.\n",
"if CRYPTO_KEYS_PATH is None:\n",
"# First login: back up the encryption key. It lives only on this runtime's filesystem.\n",
"if KEY_FILE is None:\n",
" from google.colab import files\n",
" files.download(str(model_owner.syftbox_folder / model_owner.email / \"private\" / \"crypto_keys.json\"))"
]
Expand All @@ -179,7 +179,7 @@
"source": [
"# # Optionally to clear state\n",
"# model_owner._manager.delete_syftbox()\n",
"# model_owner._manager.peer_manager.write_own_version()"
"# model_owner._manager.peer_manager.write_own_peer_info()"
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,16 @@
"metadata": {},
"outputs": [],
"source": [
"CRYPTO_KEYS_PATH = None\n",
"# NOTE: Re-storing Private Key from local_file, if you have it, uncomment the following line and upload the file.\n",
"KEY_FILE = None\n",
"# To restore the encryption key of an earlier session, uncomment the next two lines and upload crypto_keys.json.\n",
"# from google.colab import files; files.upload()\n",
"# CRYPTO_KEYS_PATH = \"crypto_keys.json\"\n",
"# KEY_FILE = \"crypto_keys.json\"\n",
"\n",
"benchmark_owner = login_do(encryption=True, crypto_keys_path=CRYPTO_KEYS_PATH)\n",
"benchmark_owner = login_do(key_file=KEY_FILE)\n",
"print(f\" Benchmark owner : {benchmark_owner.email}\")\n",
"\n",
"# First login: back up the encryption key — it lives only on this runtime's filesystem.\n",
"if CRYPTO_KEYS_PATH is None:\n",
"# First login: back up the encryption key. It lives only on this runtime's filesystem.\n",
"if KEY_FILE is None:\n",
" from google.colab import files\n",
" files.download(str(benchmark_owner.syftbox_folder / benchmark_owner.email / \"private\" / \"crypto_keys.json\"))"
]
Expand All @@ -123,7 +123,7 @@
"source": [
"# # Optionally to clear state\n",
"# benchmark_owner._manager.delete_syftbox()\n",
"# benchmark_owner._manager.peer_manager.write_own_version()"
"# benchmark_owner._manager.peer_manager.write_own_peer_info()"
]
},
{
Expand Down
4 changes: 2 additions & 2 deletions notebooks/enclave/gemma/colab/0. clear-state-gemma.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"\n",
"```python\n",
"client._manager.delete_syftbox()\n",
"client._manager.peer_manager.write_own_version()\n",
"client._manager.peer_manager.write_own_peer_info()\n",
"```"
]
},
Expand Down Expand Up @@ -68,7 +68,7 @@
"\n",
"client = login_do(sync=False, load_peers=False)\n",
"client._manager.delete_syftbox()\n",
"client._manager.peer_manager.write_own_version()"
"client._manager.peer_manager.write_own_peer_info()"
]
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,16 @@
"metadata": {},
"outputs": [],
"source": [
"CRYPTO_KEYS_PATH = None\n",
"# NOTE: Re-storing Private Key from local_file, if you have it, uncomment the following line and upload the file.\n",
"KEY_FILE = None\n",
"# To restore the encryption key of an earlier session, uncomment the next two lines and upload crypto_keys.json.\n",
"# from google.colab import files; files.upload()\n",
"# CRYPTO_KEYS_PATH = \"crypto_keys.json\"\n",
"# KEY_FILE = \"crypto_keys.json\"\n",
"\n",
"model_owner = login_do(encryption=True, crypto_keys_path=CRYPTO_KEYS_PATH)\n",
"model_owner = login_do(key_file=KEY_FILE)\n",
"print(f\" Model owner : {model_owner.email}\")\n",
"\n",
"# First login: back up the encryption key — it lives only on this runtime's filesystem.\n",
"if CRYPTO_KEYS_PATH is None:\n",
"# First login: back up the encryption key. It lives only on this runtime's filesystem.\n",
"if KEY_FILE is None:\n",
" from google.colab import files\n",
" files.download(str(model_owner.syftbox_folder / model_owner.email / \"private\" / \"crypto_keys.json\"))"
]
Expand All @@ -131,7 +131,7 @@
"source": [
"# # Optionally to clear state\n",
"# model_owner._manager.delete_syftbox()\n",
"# model_owner._manager.peer_manager.write_own_version()"
"# model_owner._manager.peer_manager.write_own_peer_info()"
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,16 @@
"metadata": {},
"outputs": [],
"source": [
"CRYPTO_KEYS_PATH = None\n",
"# NOTE: Re-storing Private Key from local_file, if you have it, uncomment the following line and upload the file.\n",
"KEY_FILE = None\n",
"# To restore the encryption key of an earlier session, uncomment the next two lines and upload crypto_keys.json.\n",
"# from google.colab import files; files.upload()\n",
"# CRYPTO_KEYS_PATH = \"crypto_keys.json\"\n",
"# KEY_FILE = \"crypto_keys.json\"\n",
"\n",
"benchmark_owner = login_do(encryption=True, crypto_keys_path=CRYPTO_KEYS_PATH)\n",
"benchmark_owner = login_do(key_file=KEY_FILE)\n",
"print(f\" Benchmark owner : {benchmark_owner.email}\")\n",
"\n",
"# First login: back up the encryption key — it lives only on this runtime's filesystem.\n",
"if CRYPTO_KEYS_PATH is None:\n",
"# First login: back up the encryption key. It lives only on this runtime's filesystem.\n",
"if KEY_FILE is None:\n",
" from google.colab import files\n",
" files.download(str(benchmark_owner.syftbox_folder / benchmark_owner.email / \"private\" / \"crypto_keys.json\"))"
]
Expand All @@ -112,7 +112,7 @@
"source": [
"# # Optionally to clear state\n",
"# benchmark_owner._manager.delete_syftbox()\n",
"# benchmark_owner._manager.peer_manager.write_own_version()"
"# benchmark_owner._manager.peer_manager.write_own_peer_info()"
]
},
{
Expand Down
6 changes: 3 additions & 3 deletions notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,9 @@
"researcher._manager.delete_syftbox()\n",
"\n",
"# Write Own Version\n",
"model_owner._manager.peer_manager.write_own_version()\n",
"benchmark_owner._manager.peer_manager.write_own_version()\n",
"researcher._manager.peer_manager.write_own_version()"
"model_owner._manager.peer_manager.write_own_peer_info()\n",
"benchmark_owner._manager.peer_manager.write_own_peer_info()\n",
"researcher._manager.peer_manager.write_own_peer_info()"
]
},
{
Expand Down
6 changes: 3 additions & 3 deletions notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,9 @@
"metadata": {},
"outputs": [],
"source": [
"# model_owner._manager.peer_manager.write_own_version()\n",
"# benchmark_owner._manager.peer_manager.write_own_version()\n",
"# researcher._manager.peer_manager.write_own_version()"
"# model_owner._manager.peer_manager.write_own_peer_info()\n",
"# benchmark_owner._manager.peer_manager.write_own_peer_info()\n",
"# researcher._manager.peer_manager.write_own_peer_info()"
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"source": [
"# Optionally to clear state\n",
"model_owner._manager.delete_syftbox()\n",
"model_owner._manager.peer_manager.write_own_version()"
"model_owner._manager.peer_manager.write_own_peer_info()"
]
},
{
Expand Down
Loading
Loading