gumloop 0.4.9__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. {gumloop-0.4.9 → gumloop-0.5.0}/.github/workflows/ci.yml +5 -3
  2. {gumloop-0.4.9 → gumloop-0.5.0}/PKG-INFO +41 -1
  3. gumloop-0.5.0/README.md +131 -0
  4. {gumloop-0.4.9 → gumloop-0.5.0}/pyproject.toml +1 -0
  5. gumloop-0.5.0/scripts/import-logins.sh +93 -0
  6. {gumloop-0.4.9 → gumloop-0.5.0}/scripts/install.sh +1 -1
  7. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/_client.py +14 -1
  8. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/_version.py +1 -1
  9. gumloop-0.5.0/src/gumloop/browser_logins/__init__.py +19 -0
  10. gumloop-0.5.0/src/gumloop/browser_logins/chromium_cookies.py +205 -0
  11. gumloop-0.5.0/src/gumloop/browser_logins/discovery.py +172 -0
  12. gumloop-0.5.0/src/gumloop/browser_logins/extract.py +105 -0
  13. gumloop-0.5.0/src/gumloop/browser_logins/filter.py +85 -0
  14. gumloop-0.5.0/src/gumloop/browser_logins/firefox_cookies.py +43 -0
  15. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/agents.py +117 -1
  16. gumloop-0.5.0/src/gumloop/cli/commands/browser.py +310 -0
  17. gumloop-0.5.0/src/gumloop/cli/commands/evaluations.py +481 -0
  18. gumloop-0.5.0/src/gumloop/cli/commands/models.py +165 -0
  19. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/main.py +6 -0
  20. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/plugin_assets/gumloop/skills/gumloop-cli/SKILL.md +1 -0
  21. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/plugin_assets/gumloop/skills/gumloop-cli/references/commands.md +11 -0
  22. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/resources/__init__.py +12 -0
  23. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/resources/agents.py +101 -2
  24. gumloop-0.5.0/src/gumloop/resources/browser_profiles.py +66 -0
  25. gumloop-0.5.0/src/gumloop/resources/evaluations.py +274 -0
  26. gumloop-0.5.0/src/gumloop/resources/organizations.py +21 -0
  27. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/types.py +260 -1
  28. gumloop-0.5.0/tests/cli/test_browser_logins.py +361 -0
  29. gumloop-0.5.0/tests/cli/test_evaluations.py +203 -0
  30. gumloop-0.5.0/tests/cli/test_models.py +111 -0
  31. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_agents.py +26 -1
  32. gumloop-0.5.0/tests/sdk/test_evaluations.py +205 -0
  33. gumloop-0.5.0/tests/sdk/test_models.py +76 -0
  34. gumloop-0.5.0/tests/sdk/test_organizations.py +34 -0
  35. {gumloop-0.4.9 → gumloop-0.5.0}/uv.lock +121 -79
  36. gumloop-0.4.9/README.md +0 -92
  37. {gumloop-0.4.9 → gumloop-0.5.0}/.env.example +0 -0
  38. {gumloop-0.4.9 → gumloop-0.5.0}/.github/workflows/release.yml +0 -0
  39. {gumloop-0.4.9 → gumloop-0.5.0}/.gitignore +0 -0
  40. {gumloop-0.4.9 → gumloop-0.5.0}/.pre-commit-config.yaml +0 -0
  41. {gumloop-0.4.9 → gumloop-0.5.0}/.python-version +0 -0
  42. {gumloop-0.4.9 → gumloop-0.5.0}/LICENSE +0 -0
  43. {gumloop-0.4.9 → gumloop-0.5.0}/scripts/check +0 -0
  44. {gumloop-0.4.9 → gumloop-0.5.0}/scripts/fmt +0 -0
  45. {gumloop-0.4.9 → gumloop-0.5.0}/scripts/sdk_examples.py +0 -0
  46. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/__init__.py +0 -0
  47. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/_auth.py +0 -0
  48. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/_gumcp_transport.py +0 -0
  49. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/_http.py +0 -0
  50. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/__init__.py +0 -0
  51. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/__init__.py +0 -0
  52. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/_args_input.py +0 -0
  53. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/_downloads.py +0 -0
  54. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/_sync_output.py +0 -0
  55. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/artifacts.py +0 -0
  56. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/auth.py +0 -0
  57. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/brain.py +0 -0
  58. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/chat.py +0 -0
  59. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/mcp.py +0 -0
  60. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/plugin.py +0 -0
  61. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/sessions.py +0 -0
  62. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/skills.py +0 -0
  63. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/sync.py +0 -0
  64. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/commands/update.py +0 -0
  65. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/console.py +0 -0
  66. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/context.py +0 -0
  67. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/credentials.py +0 -0
  68. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/errors.py +0 -0
  69. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/oauth.py +0 -0
  70. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/cli/plugin_assets/gumloop/plugin.json +0 -0
  71. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/errors.py +0 -0
  72. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/oauth.py +0 -0
  73. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/py.typed +0 -0
  74. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/resources/artifacts.py +0 -0
  75. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/resources/brain.py +0 -0
  76. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/resources/chat.py +0 -0
  77. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/resources/mcp.py +0 -0
  78. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/resources/sessions.py +0 -0
  79. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/resources/skills.py +0 -0
  80. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/resources/sync.py +0 -0
  81. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/resources/teams.py +0 -0
  82. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/spec/__init__.py +0 -0
  83. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/spec/_compat.py +0 -0
  84. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/spec/_extensions.py +0 -0
  85. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/__init__.py +0 -0
  86. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/archive.py +0 -0
  87. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/bundle.py +0 -0
  88. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/bundle_validation.py +0 -0
  89. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/errors.py +0 -0
  90. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/hashing.py +0 -0
  91. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/installed.py +0 -0
  92. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/local_state.py +0 -0
  93. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/lock.py +0 -0
  94. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/markers.py +0 -0
  95. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/reconcile.py +0 -0
  96. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/reporting.py +0 -0
  97. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/results.py +0 -0
  98. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/run.py +0 -0
  99. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/scheduler.py +0 -0
  100. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/target_files.py +0 -0
  101. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/targets.py +0 -0
  102. {gumloop-0.4.9 → gumloop-0.5.0}/src/gumloop/sync/wire.py +0 -0
  103. {gumloop-0.4.9 → gumloop-0.5.0}/tests/__init__.py +0 -0
  104. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/__init__.py +0 -0
  105. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/conftest.py +0 -0
  106. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/sync_scenario_helpers.py +0 -0
  107. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/sync_test_fakes.py +0 -0
  108. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_agents.py +0 -0
  109. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_args_input.py +0 -0
  110. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_artifacts.py +0 -0
  111. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_brain.py +0 -0
  112. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_chat.py +0 -0
  113. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_context.py +0 -0
  114. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_credentials.py +0 -0
  115. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_home_isolation.py +0 -0
  116. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_login.py +0 -0
  117. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_main.py +0 -0
  118. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_mcp.py +0 -0
  119. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_plugin.py +0 -0
  120. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_sessions.py +0 -0
  121. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_skills.py +0 -0
  122. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_sync_bundle.py +0 -0
  123. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_sync_command.py +0 -0
  124. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_sync_command_safety.py +0 -0
  125. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_sync_contract_fixtures.py +0 -0
  126. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_sync_enrollment.py +0 -0
  127. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_sync_fixture_corpus.py +0 -0
  128. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_sync_hashing.py +0 -0
  129. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_sync_installed.py +0 -0
  130. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_sync_reconciliation_safety.py +0 -0
  131. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_sync_scheduler_macos.py +0 -0
  132. {gumloop-0.4.9 → gumloop-0.5.0}/tests/cli/test_sync_targets.py +0 -0
  133. {gumloop-0.4.9 → gumloop-0.5.0}/tests/conftest.py +0 -0
  134. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/README.md +0 -0
  135. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/generated-bundle-transfer-limit.json +0 -0
  136. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-absolute-path.zip +0 -0
  137. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-bytes-per-file-limit.zip +0 -0
  138. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-device.zip +0 -0
  139. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-duplicate-entry.zip +0 -0
  140. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-fifo.zip +0 -0
  141. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-files-per-skill-limit.zip +0 -0
  142. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-nul-name.zip +0 -0
  143. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-parent-traversal.zip +0 -0
  144. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-reserved-marker.zip +0 -0
  145. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-socket.zip +0 -0
  146. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-symlink.zip +0 -0
  147. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-total-uncompressed-limit.zip +0 -0
  148. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/invalid-windows-drive-prefix.zip +0 -0
  149. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/valid-single-skill-stored.zip +0 -0
  150. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/archives/valid-single-skill.zip +0 -0
  151. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/bundles/newer-than-plan.zip +0 -0
  152. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/bundles/normal.zip +0 -0
  153. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/catalog.json +0 -0
  154. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/corpus.sha256 +0 -0
  155. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/hashes/content-v1.json +0 -0
  156. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/hashes/manifest-v1.json +0 -0
  157. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/manifests/invalid-duplicate-identity.json +0 -0
  158. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/manifests/invalid-duplicate-install-name.json +0 -0
  159. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/manifests/invalid-manifest-hash.json +0 -0
  160. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/manifests/newer-than-plan.json +0 -0
  161. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/manifests/normal-bundle.json +0 -0
  162. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/responses/below-pro.json +0 -0
  163. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/responses/cli-upgrade-required.json +0 -0
  164. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/responses/lost-membership.json +0 -0
  165. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/responses/normal-empty-plan.json +0 -0
  166. {gumloop-0.4.9 → gumloop-0.5.0}/tests/fixtures/skill_sync/v1/responses/normal-plan.json +0 -0
  167. {gumloop-0.4.9 → gumloop-0.5.0}/tests/integration/conftest.py +0 -0
  168. {gumloop-0.4.9 → gumloop-0.5.0}/tests/integration/test_live.py +0 -0
  169. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/__init__.py +0 -0
  170. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/conftest.py +0 -0
  171. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/helpers.py +0 -0
  172. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_artifacts.py +0 -0
  173. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_brain.py +0 -0
  174. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_chat.py +0 -0
  175. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_client.py +0 -0
  176. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_errors.py +0 -0
  177. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_gumcp_transport.py +0 -0
  178. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_mcp.py +0 -0
  179. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_oauth.py +0 -0
  180. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_sessions.py +0 -0
  181. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_skills.py +0 -0
  182. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_sync.py +0 -0
  183. {gumloop-0.4.9 → gumloop-0.5.0}/tests/sdk/test_teams.py +0 -0
  184. {gumloop-0.4.9 → gumloop-0.5.0}/tests/skill_sync_fixtures.py +0 -0
  185. {gumloop-0.4.9 → gumloop-0.5.0}/tests/test_client.py +0 -0
@@ -25,10 +25,12 @@ jobs:
25
25
  - run: uv run --frozen ruff format --check .
26
26
  - run: uv run --frozen ruff check .
27
27
  - run: uv lock --check
28
- - name: Check install.sh syntax
28
+ - name: Check installer script syntax
29
29
  run: |
30
- sh -n scripts/install.sh
31
- bash -n scripts/install.sh
30
+ for script in scripts/install.sh scripts/import-logins.sh; do
31
+ sh -n "$script"
32
+ bash -n "$script"
33
+ done
32
34
 
33
35
  typecheck:
34
36
  runs-on: ubuntu-latest
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gumloop
3
- Version: 0.4.9
3
+ Version: 0.5.0
4
4
  Summary: Python client for the Gumloop API
5
5
  Project-URL: Homepage, https://github.com/gumloop/gumloop-py
6
6
  Project-URL: Bug Tracker, https://github.com/gumloop/gumloop-py/issues
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Python: >=3.10
15
15
  Requires-Dist: click>=8.0
16
+ Requires-Dist: cryptography>=42.0.0
16
17
  Requires-Dist: httpx-sse>=0.4.0
17
18
  Requires-Dist: httpx>=0.24.1
18
19
  Requires-Dist: keyring>=24
@@ -41,6 +42,26 @@ The installer is fully self-contained under `~/.gumloop` — it ships its own Py
41
42
  gumloop update
42
43
  ```
43
44
 
45
+ ### Import your browser sign-ins into a browser profile
46
+
47
+ Gumloop agents that use the Browser ability keep their sign-ins in a **browser profile**. Import the sites you are already signed into locally (Chrome, Brave, Edge, Chromium, Arc, Firefox on macOS or Linux) without an extension. By default every site in the local browser profile you pick comes across; you see the sites and cookie counts and confirm before anything is sent:
48
+
49
+ ```bash
50
+ gumloop browser import-logins # every site, into your personal default profile
51
+ gumloop browser import-logins --browser brave --exclude-domain doubleclick.net
52
+ gumloop browser import-logins --include-domain github.com --include-domain linear.app
53
+ gumloop browser import-logins --url https://mail.google.com --team <team_id> --into 'Ops inbox' # one site
54
+ gumloop browser profiles list
55
+ ```
56
+
57
+ Or, without installing anything first (set `GUMLOOP_LOGIN_URL` to import a single site, `GUMLOOP_EXCLUDE_DOMAINS` / `GUMLOOP_INCLUDE_DOMAINS` to narrow a whole-profile import):
58
+
59
+ ```bash
60
+ curl -fsSL https://gumloop.com/cli/import-logins.sh | sh
61
+ ```
62
+
63
+ Cookies only: local storage and IndexedDB stay on your machine, so sites that keep the session there ask the agent to sign in once, after which the agent's browser keeps it. Imported cookies are encrypted with the profile's own key before storage and are never shown back in the UI or API; sign-ins the agent picks up while running are saved back to the same profile. Rename, remove sites from, or delete profiles on the Secrets page in Gumloop.
64
+
44
65
  ## SDK
45
66
 
46
67
  To use the client as a library in your own Python project:
@@ -115,3 +136,22 @@ async def main() -> None:
115
136
 
116
137
  asyncio.run(main())
117
138
  ```
139
+
140
+ ## Route a task to the right model
141
+
142
+ Ask Gumloop Chew which of your candidate models should handle a task. Decision only — nothing runs.
143
+
144
+ ```python
145
+ from gumloop import Gumloop
146
+
147
+ client = Gumloop(api_key="your_api_key", user_id="your_user_id")
148
+
149
+ decision = client.models.route(
150
+ input="Summarize this email thread and draft a reply",
151
+ models=["gpt-5.6-luna", "x-ai/grok-4.6", "claude-opus-5"],
152
+ )
153
+
154
+ print(decision.route.model, decision.route.lane, decision.route.fallback_models)
155
+ ```
156
+
157
+ Omit `models` to route across the full Chew catalog. Each call bills one small classifier completion.
@@ -0,0 +1,131 @@
1
+ # Gumloop Python Client
2
+
3
+ A Python client for the Gumloop API that makes it easy to run and monitor Gumloop flows, plus the `gumloop` CLI.
4
+
5
+ ## CLI
6
+
7
+ Install on macOS or Linux:
8
+
9
+ ```bash
10
+ curl -fsSL https://gumloop.com/cli/install.sh | sh
11
+ ```
12
+
13
+ The installer is fully self-contained under `~/.gumloop` — it ships its own Python, never touches your system Python, and needs no sudo. Run `gumloop --help` to get started and update any time with:
14
+
15
+ ```bash
16
+ gumloop update
17
+ ```
18
+
19
+ ### Import your browser sign-ins into a browser profile
20
+
21
+ Gumloop agents that use the Browser ability keep their sign-ins in a **browser profile**. Import the sites you are already signed into locally (Chrome, Brave, Edge, Chromium, Arc, Firefox on macOS or Linux) without an extension. By default every site in the local browser profile you pick comes across; you see the sites and cookie counts and confirm before anything is sent:
22
+
23
+ ```bash
24
+ gumloop browser import-logins # every site, into your personal default profile
25
+ gumloop browser import-logins --browser brave --exclude-domain doubleclick.net
26
+ gumloop browser import-logins --include-domain github.com --include-domain linear.app
27
+ gumloop browser import-logins --url https://mail.google.com --team <team_id> --into 'Ops inbox' # one site
28
+ gumloop browser profiles list
29
+ ```
30
+
31
+ Or, without installing anything first (set `GUMLOOP_LOGIN_URL` to import a single site, `GUMLOOP_EXCLUDE_DOMAINS` / `GUMLOOP_INCLUDE_DOMAINS` to narrow a whole-profile import):
32
+
33
+ ```bash
34
+ curl -fsSL https://gumloop.com/cli/import-logins.sh | sh
35
+ ```
36
+
37
+ Cookies only: local storage and IndexedDB stay on your machine, so sites that keep the session there ask the agent to sign in once, after which the agent's browser keeps it. Imported cookies are encrypted with the profile's own key before storage and are never shown back in the UI or API; sign-ins the agent picks up while running are saved back to the same profile. Rename, remove sites from, or delete profiles on the Secrets page in Gumloop.
38
+
39
+ ## SDK
40
+
41
+ To use the client as a library in your own Python project:
42
+
43
+ ```bash
44
+ uv add gumloop
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```python
50
+ from gumloop import GumloopClient
51
+
52
+ # Initialize the client
53
+ client = GumloopClient(
54
+ api_key="your_api_key",
55
+ user_id="your_user_id"
56
+ )
57
+
58
+ # Run a flow and wait for outputs
59
+ output = client.run_flow(
60
+ flow_id="your_flow_id",
61
+ inputs={
62
+ "recipient": "example@email.com",
63
+ "subject": "Hello",
64
+ "body": "World"
65
+ }
66
+ )
67
+
68
+ print(output)
69
+ ```
70
+
71
+ ## Authenticate with a team API key
72
+
73
+ Team (workspace) API keys are scoped to a single team. Pass the team's ID and
74
+ the acting member's user ID — every request is validated against that team and
75
+ only reaches resources the team owns.
76
+
77
+ ```python
78
+ from gumloop import Gumloop
79
+
80
+ client = Gumloop(
81
+ api_key="your_team_api_key",
82
+ user_id="your_user_id", # must be a member of the team
83
+ team_id="your_team_id",
84
+ )
85
+
86
+ agents = client.agents.list() # scoped to the team
87
+ ```
88
+
89
+ `team_id` can also be provided via the `GUMLOOP_TEAM_ID` environment variable.
90
+
91
+ ## Chat with an agent (streaming)
92
+
93
+ ```python
94
+ import asyncio
95
+
96
+ from gumloop import AsyncGumloop
97
+
98
+
99
+ async def main() -> None:
100
+ async with AsyncGumloop(access_token="your_access_token") as client:
101
+ agents = await client.agents.list()
102
+ agent = agents.agents[0]
103
+
104
+ async for event in client.sessions.stream(
105
+ agent.id,
106
+ input="Hello, what can you do?",
107
+ ):
108
+ print(event)
109
+
110
+
111
+ asyncio.run(main())
112
+ ```
113
+
114
+ ## Route a task to the right model
115
+
116
+ Ask Gumloop Chew which of your candidate models should handle a task. Decision only — nothing runs.
117
+
118
+ ```python
119
+ from gumloop import Gumloop
120
+
121
+ client = Gumloop(api_key="your_api_key", user_id="your_user_id")
122
+
123
+ decision = client.models.route(
124
+ input="Summarize this email thread and draft a reply",
125
+ models=["gpt-5.6-luna", "x-ai/grok-4.6", "claude-opus-5"],
126
+ )
127
+
128
+ print(decision.route.model, decision.route.lane, decision.route.fallback_models)
129
+ ```
130
+
131
+ Omit `models` to route across the full Chew catalog. Each call bills one small classifier completion.
@@ -19,6 +19,7 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3.13",
20
20
  ]
21
21
  dependencies = [
22
+ "cryptography>=42.0.0",
22
23
  "httpx>=0.24.1",
23
24
  "httpx-sse>=0.4.0",
24
25
  "keyring>=24",
@@ -0,0 +1,93 @@
1
+ #!/bin/sh
2
+ # Bring this machine's browser sign-ins into your Gumloop browser profile (every site, cookies only):
3
+ # curl -fsSL https://gumloop.com/cli/import-logins.sh | sh
4
+ # One site only:
5
+ # GUMLOOP_LOGIN_URL='https://app.example.com' sh -c 'curl -fsSL https://gumloop.com/cli/import-logins.sh | sh'
6
+
7
+ set -eu
8
+
9
+ INSTALL_URL="${GUMLOOP_INSTALL_URL:-https://gumloop.com/cli/install.sh}"
10
+ GUMLOOP_HOME="${GUMLOOP_INSTALL_DIR:-$HOME/.gumloop}"
11
+ SHIM_PATH="$HOME/.local/bin/gumloop"
12
+
13
+ if [ -t 1 ]; then
14
+ BOLD="$(printf '\033[1m')"
15
+ RED="$(printf '\033[31m')"
16
+ RESET="$(printf '\033[0m')"
17
+ else
18
+ BOLD="" RED="" RESET=""
19
+ fi
20
+
21
+ step() { printf '%s==>%s %s\n' "${BOLD}" "${RESET}" "$1"; }
22
+ fail() { printf '%serror:%s %s\n' "${RED}" "${RESET}" "$1" >&2; exit 1; }
23
+
24
+ # Prompts read /dev/tty, not stdin: under `curl | sh` stdin is the script.
25
+ has_tty() { ( : < /dev/tty ) 2>/dev/null; }
26
+
27
+ case "$(uname -s)" in
28
+ Darwin|Linux) ;;
29
+ *) fail "the Gumloop CLI runs on macOS and Linux. On Windows, use the Gumloop Chrome extension from the Secrets page instead." ;;
30
+ esac
31
+
32
+ find_cli() {
33
+ if [ -x "$SHIM_PATH" ]; then
34
+ printf '%s' "$SHIM_PATH"
35
+ elif [ -x "$GUMLOOP_HOME/venv/bin/gumloop" ]; then
36
+ printf '%s' "$GUMLOOP_HOME/venv/bin/gumloop"
37
+ elif command -v gumloop >/dev/null 2>&1; then
38
+ command -v gumloop
39
+ fi
40
+ }
41
+
42
+ CLI="$(find_cli || true)"
43
+ if [ -z "$CLI" ]; then
44
+ step "Installing the Gumloop CLI"
45
+ if command -v curl >/dev/null 2>&1; then
46
+ curl -fsSL "$INSTALL_URL" | GUMLOOP_SKIP_LOGIN=1 sh
47
+ elif command -v wget >/dev/null 2>&1; then
48
+ wget -qO- "$INSTALL_URL" | GUMLOOP_SKIP_LOGIN=1 sh
49
+ else
50
+ fail "curl or wget is required"
51
+ fi
52
+ CLI="$(find_cli || true)"
53
+ [ -n "$CLI" ] || fail "the Gumloop CLI did not install; see the messages above."
54
+ fi
55
+
56
+ if ! "$CLI" browser --help >/dev/null 2>&1; then
57
+ step "Updating the Gumloop CLI (this version cannot import sites)"
58
+ "$CLI" update >/dev/null 2>&1 || fail "could not update the Gumloop CLI; run '$CLI update' and retry."
59
+ fi
60
+
61
+ url="${GUMLOOP_LOGIN_URL:-}"
62
+
63
+ if [ -z "${GUMLOOP_API_KEY:-}" ] && [ -z "${GUMLOOP_ACCESS_TOKEN:-}" ]; then
64
+ if ! "$CLI" browser profiles list --json >/dev/null 2>&1; then
65
+ step "Signing in to Gumloop"
66
+ has_tty || fail "not signed in and no terminal to sign in on. Set GUMLOOP_API_KEY and GUMLOOP_USER_ID, or run 'gumloop login' first."
67
+ "$CLI" login < /dev/tty
68
+ fi
69
+ fi
70
+
71
+ set -- browser import-logins
72
+ if [ -n "$url" ]; then
73
+ set -- "$@" --url "$url" --yes
74
+ else
75
+ # A whole profile is previewed and confirmed on the terminal; headless runs have already opted in.
76
+ has_tty || set -- "$@" --yes
77
+ for domain in $(printf '%s' "${GUMLOOP_INCLUDE_DOMAINS:-}" | tr ',' ' '); do set -- "$@" --include-domain "$domain"; done
78
+ for domain in $(printf '%s' "${GUMLOOP_EXCLUDE_DOMAINS:-}" | tr ',' ' '); do set -- "$@" --exclude-domain "$domain"; done
79
+ fi
80
+ [ -n "${GUMLOOP_BROWSER_PROFILE_ID:-}" ] && set -- "$@" --into "$GUMLOOP_BROWSER_PROFILE_ID"
81
+ [ -n "${GUMLOOP_TEAM_ID:-}" ] && set -- "$@" --team "$GUMLOOP_TEAM_ID"
82
+ [ -n "${GUMLOOP_BROWSER:-}" ] && set -- "$@" --browser "$GUMLOOP_BROWSER"
83
+
84
+ if [ -n "$url" ]; then
85
+ step "Importing your sign-in for ${url}"
86
+ else
87
+ step "Importing your browser's sign-ins"
88
+ fi
89
+ if has_tty; then
90
+ "$CLI" "$@" < /dev/tty
91
+ else
92
+ "$CLI" "$@"
93
+ fi
@@ -279,7 +279,7 @@ case "$login_state" in
279
279
  printf 'or install a keychain (e.g. apt install gnome-keyring libsecret-1-0) and run `gumloop login`.\n\n'
280
280
  ;;
281
281
  *)
282
- if has_tty && confirm "Log in to Gumloop now?"; then
282
+ if [ -z "${GUMLOOP_SKIP_LOGIN:-}" ] && has_tty && confirm "Log in to Gumloop now?"; then
283
283
  "$SHIM_PATH" login < /dev/tty || true
284
284
  fi
285
285
  ;;
@@ -18,15 +18,21 @@ from gumloop.resources import Artifacts
18
18
  from gumloop.resources import AsyncAgents
19
19
  from gumloop.resources import AsyncArtifacts
20
20
  from gumloop.resources import AsyncBrain
21
+ from gumloop.resources import AsyncBrowserProfiles
21
22
  from gumloop.resources import AsyncChat
23
+ from gumloop.resources import AsyncEvaluations
22
24
  from gumloop.resources import AsyncMCP
23
25
  from gumloop.resources import AsyncModels
26
+ from gumloop.resources import AsyncOrganizations
24
27
  from gumloop.resources import AsyncSessions
25
28
  from gumloop.resources import AsyncSkills
26
29
  from gumloop.resources import AsyncTeams
27
30
  from gumloop.resources import Brain
31
+ from gumloop.resources import BrowserProfiles
28
32
  from gumloop.resources import Chat
33
+ from gumloop.resources import Evaluations
29
34
  from gumloop.resources import Models
35
+ from gumloop.resources import Organizations
30
36
  from gumloop.resources import Sessions
31
37
  from gumloop.resources import Skills
32
38
  from gumloop.resources import Sync
@@ -46,7 +52,8 @@ def _derive_stream_base_url(base_url: str) -> str:
46
52
  class Gumloop:
47
53
  """Sync Gumloop SDK client. Composes an :class:`HttpClient` with the
48
54
  resource classes (``agents``, ``sessions``, ``mcp``, ``teams``,
49
- ``skills``, ``artifacts``, ``brain``, ``models``). The :attr:`oauth`
55
+ ``organizations``, ``evaluations``, ``skills``, ``artifacts``, ``brain``,
56
+ ``models``). The :attr:`oauth`
50
57
  attribute holds OAuth helpers — kept off the transport because it
51
58
  bootstraps the bearer token rather than consuming one."""
52
59
 
@@ -92,10 +99,13 @@ class Gumloop:
92
99
  self.models = Models(self._http)
93
100
  self.mcp = MCP(self._http)
94
101
  self.teams = Teams(self._http)
102
+ self.organizations = Organizations(self._http)
103
+ self.evaluations = Evaluations(self._http)
95
104
  self.skills = Skills(self._http)
96
105
  self.sync = Sync(self._http)
97
106
  self.artifacts = Artifacts(self._http)
98
107
  self.brain = Brain(self._http)
108
+ self.browser_profiles = BrowserProfiles(self._http)
99
109
  self.oauth = OAuth(base_url=self.base_url, timeout=self.timeout)
100
110
 
101
111
  def close(self) -> None:
@@ -151,9 +161,12 @@ class AsyncGumloop:
151
161
  self.models = AsyncModels(self._http)
152
162
  self.mcp = AsyncMCP(self._http)
153
163
  self.teams = AsyncTeams(self._http)
164
+ self.organizations = AsyncOrganizations(self._http)
165
+ self.evaluations = AsyncEvaluations(self._http)
154
166
  self.skills = AsyncSkills(self._http)
155
167
  self.artifacts = AsyncArtifacts(self._http)
156
168
  self.brain = AsyncBrain(self._http)
169
+ self.browser_profiles = AsyncBrowserProfiles(self._http)
157
170
  self.oauth = OAuth(base_url=self.base_url, timeout=self.timeout)
158
171
 
159
172
  async def __aenter__(self) -> AsyncGumloop:
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.4.9"
3
+ __version__ = "0.5.0"
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from gumloop.browser_logins.discovery import BrowserKind
4
+ from gumloop.browser_logins.discovery import LocalProfile
5
+ from gumloop.browser_logins.discovery import discover_profiles
6
+ from gumloop.browser_logins.extract import ExtractResult
7
+ from gumloop.browser_logins.extract import extract_profile_cookies
8
+ from gumloop.browser_logins.extract import extract_site_cookies
9
+ from gumloop.browser_logins.filter import site_of_url
10
+
11
+ __all__ = [
12
+ "BrowserKind",
13
+ "ExtractResult",
14
+ "LocalProfile",
15
+ "discover_profiles",
16
+ "extract_profile_cookies",
17
+ "extract_site_cookies",
18
+ "site_of_url",
19
+ ]
@@ -0,0 +1,205 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import shutil
5
+ import sqlite3
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ from collections.abc import Callable
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from cryptography.hazmat.primitives import hashes
15
+ from cryptography.hazmat.primitives.ciphers import Cipher
16
+ from cryptography.hazmat.primitives.ciphers import algorithms
17
+ from cryptography.hazmat.primitives.ciphers import modes
18
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
19
+
20
+ from gumloop.browser_logins.discovery import BrowserKind
21
+
22
+ _SALT = b"saltysalt"
23
+ _IV = b" " * 16
24
+ _KEY_LENGTH = 16
25
+ _MAC_ITERATIONS = 1003
26
+ _LINUX_ITERATIONS = 1
27
+ _LINUX_DEFAULT_PASSWORD = b"peanuts"
28
+ _WEBKIT_EPOCH_OFFSET_US = 11_644_473_600 * 1_000_000
29
+ _SAME_SITE = {0: "None", 1: "Lax", 2: "Strict"}
30
+
31
+ _QUERY = (
32
+ "SELECT host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, "
33
+ "samesite, has_expires, top_frame_site_key FROM cookies"
34
+ )
35
+
36
+
37
+ class KeychainAccessError(RuntimeError):
38
+ """The OS refused to hand over the browser's cookie encryption key."""
39
+
40
+
41
+ @dataclass
42
+ class ChromiumReadResult:
43
+ cookies: list[dict[str, Any]]
44
+ undecryptable: int = 0
45
+
46
+
47
+ def macos_safe_storage_password(
48
+ service: str, runner: Callable[..., subprocess.CompletedProcess] = subprocess.run
49
+ ) -> bytes:
50
+ completed = runner(
51
+ ["security", "find-generic-password", "-w", "-s", service],
52
+ capture_output=True,
53
+ text=True,
54
+ check=False,
55
+ )
56
+ if completed.returncode != 0 or not completed.stdout.strip():
57
+ raise KeychainAccessError(
58
+ f"macOS Keychain did not release the '{service}' key (was the prompt denied?). "
59
+ "Allow access when macOS asks, or use the Gumloop Chrome extension instead."
60
+ )
61
+
62
+ return completed.stdout.strip().encode("utf-8")
63
+
64
+
65
+ def linux_safe_storage_password(service: str) -> bytes:
66
+ try:
67
+ import secretstorage # type: ignore[import-not-found] # noqa: PLC0415 - optional, desktop-only dependency
68
+
69
+ connection = secretstorage.dbus_init()
70
+ collection = secretstorage.get_default_collection(connection)
71
+ for item in collection.get_all_items():
72
+ if item.get_label() == service:
73
+ secret = item.get_secret()
74
+ if secret:
75
+ return bytes(secret)
76
+ except Exception: # noqa: BLE001 - no bus, no keyring, no secretstorage: fall back to the default key
77
+ pass
78
+
79
+ return _LINUX_DEFAULT_PASSWORD
80
+
81
+
82
+ def derive_key(password: bytes, *, iterations: int) -> bytes:
83
+ kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=_KEY_LENGTH, salt=_SALT, iterations=iterations)
84
+ return kdf.derive(password)
85
+
86
+
87
+ def resolve_key(browser: BrowserKind, *, platform: str | None = None) -> bytes:
88
+ platform = platform or sys.platform
89
+ if platform == "darwin":
90
+ return derive_key(macos_safe_storage_password(browser.safe_storage_service), iterations=_MAC_ITERATIONS)
91
+
92
+ return derive_key(linux_safe_storage_password(browser.safe_storage_service), iterations=_LINUX_ITERATIONS)
93
+
94
+
95
+ def decrypt_value(encrypted: bytes, key: bytes, host_key: str) -> str | None:
96
+ """Chrome 130+ prefixes the plaintext with SHA-256(host_key)."""
97
+ if len(encrypted) < 3:
98
+ return None
99
+
100
+ prefix, body = encrypted[:3], encrypted[3:]
101
+ if prefix not in (b"v10", b"v11"):
102
+ return None
103
+
104
+ if len(body) % 16 != 0 or not body:
105
+ return None
106
+
107
+ decryptor = Cipher(algorithms.AES(key), modes.CBC(_IV)).decryptor()
108
+ padded = decryptor.update(body) + decryptor.finalize()
109
+ pad = padded[-1]
110
+ if pad < 1 or pad > 16 or padded[-pad:] != bytes([pad]) * pad:
111
+ return None
112
+
113
+ plain = padded[:-pad]
114
+ host_hash = hashlib.sha256(host_key.encode("utf-8")).digest()
115
+ if plain[:32] == host_hash:
116
+ plain = plain[32:]
117
+
118
+ try:
119
+ return plain.decode("utf-8")
120
+ except UnicodeDecodeError:
121
+ return None
122
+
123
+
124
+ def _webkit_to_unix(expires_utc: int) -> float | None:
125
+ if not expires_utc:
126
+ return None
127
+
128
+ return (expires_utc - _WEBKIT_EPOCH_OFFSET_US) / 1_000_000
129
+
130
+
131
+ def copy_database(db_path: Path) -> tuple[Path, Path]:
132
+ """Copy the database (and its journal/WAL) so a running browser's lock does not matter."""
133
+ temp_dir = Path(tempfile.mkdtemp(prefix="gumloop-cookies-"))
134
+ copied = temp_dir / db_path.name
135
+ shutil.copy2(db_path, copied)
136
+ for suffix in ("-journal", "-wal", "-shm"):
137
+ sidecar = db_path.with_name(db_path.name + suffix)
138
+ if sidecar.exists():
139
+ shutil.copy2(sidecar, temp_dir / sidecar.name)
140
+
141
+ return temp_dir, copied
142
+
143
+
144
+ def read_cookies(db_path: Path, key: bytes | None, *, keep: Callable[[str], bool] | None = None) -> ChromiumReadResult:
145
+ """Cookies as CDP ``CookieParam``-shaped dicts; ``keep(host_key)`` filters before any value is decrypted."""
146
+ temp_dir, copied = copy_database(db_path)
147
+ try:
148
+ connection = sqlite3.connect(f"file:{copied}?mode=ro", uri=True)
149
+ try:
150
+ rows = connection.execute(_QUERY).fetchall()
151
+ finally:
152
+ connection.close()
153
+ finally:
154
+ shutil.rmtree(temp_dir, ignore_errors=True)
155
+
156
+ cookies: list[dict[str, Any]] = []
157
+ undecryptable = 0
158
+ for (
159
+ host_key,
160
+ name,
161
+ value,
162
+ encrypted_value,
163
+ path,
164
+ expires_utc,
165
+ is_secure,
166
+ is_httponly,
167
+ samesite,
168
+ has_expires,
169
+ top_frame,
170
+ ) in rows:
171
+ if keep is not None and not keep(host_key):
172
+ continue
173
+
174
+ plain = value or ""
175
+ if not plain and encrypted_value:
176
+ decrypted = decrypt_value(bytes(encrypted_value), key, host_key) if key is not None else None
177
+ if decrypted is None:
178
+ undecryptable += 1
179
+ continue
180
+
181
+ plain = decrypted
182
+
183
+ cookie: dict[str, Any] = {
184
+ "name": name,
185
+ "value": plain,
186
+ "domain": host_key,
187
+ "path": path or "/",
188
+ "secure": bool(is_secure),
189
+ "httpOnly": bool(is_httponly),
190
+ }
191
+ same_site = _SAME_SITE.get(samesite)
192
+ if same_site:
193
+ cookie["sameSite"] = same_site
194
+
195
+ if has_expires:
196
+ expires = _webkit_to_unix(expires_utc)
197
+ if expires:
198
+ cookie["expires"] = expires
199
+
200
+ if top_frame:
201
+ cookie["partitionKey"] = {"topLevelSite": top_frame}
202
+
203
+ cookies.append(cookie)
204
+
205
+ return ChromiumReadResult(cookies=cookies, undecryptable=undecryptable)