firekeep-client 1.7.2__tar.gz → 1.8.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 (181) hide show
  1. {firekeep_client-1.7.2/firekeep_client.egg-info → firekeep_client-1.8.0}/PKG-INFO +1 -1
  2. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/__init__.py +1 -1
  3. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/adapters/codex.py +3 -0
  4. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/childenv.py +1 -1
  5. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/cli.py +65 -2
  6. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/gateway.py +9 -0
  7. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/promptrecall.py +13 -1
  8. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/shim.py +23 -7
  9. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/state.py +85 -0
  10. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/updater.py +2 -2
  11. {firekeep_client-1.7.2 → firekeep_client-1.8.0/firekeep_client.egg-info}/PKG-INFO +1 -1
  12. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client.egg-info/SOURCES.txt +2 -1
  13. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/pyproject.toml +1 -1
  14. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_install.py +40 -2
  15. firekeep_client-1.8.0/tests/test_cli_whoami.py +101 -0
  16. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_e2e_bootstrap.py +2 -2
  17. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_gateway_child_env.py +6 -0
  18. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_gateway_toolset.py +41 -0
  19. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_package.py +1 -1
  20. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_promptrecall.py +24 -0
  21. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_shim_identity.py +124 -0
  22. firekeep_client-1.8.0/tests/test_state_session_report.py +106 -0
  23. firekeep_client-1.7.2/tests/test_ci_publishes_symdex.py +0 -30
  24. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/LICENSE +0 -0
  25. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/NOTICE +0 -0
  26. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/README.md +0 -0
  27. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/adapters/__init__.py +0 -0
  28. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/adapters/base.py +0 -0
  29. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/adapters/claude.py +0 -0
  30. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/adapters/claude_desktop.py +0 -0
  31. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/adapters/generic.py +0 -0
  32. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/adapters/kiro.py +0 -0
  33. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/adapters/opencode.py +0 -0
  34. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/adapters/pi.py +0 -0
  35. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/autoupdate.py +0 -0
  36. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/background.py +0 -0
  37. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/backups.py +0 -0
  38. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/config_write.py +0 -0
  39. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/connect.py +0 -0
  40. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/contract/__init__.py +0 -0
  41. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/contract/matrix.py +0 -0
  42. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/decision/__init__.py +0 -0
  43. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/decision/board.py +0 -0
  44. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/decision/server.py +0 -0
  45. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/dexes.py +0 -0
  46. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/docdexsync.py +0 -0
  47. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooklog.py +0 -0
  48. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/__init__.py +0 -0
  49. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/__main__.py +0 -0
  50. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/_git.py +0 -0
  51. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/_mcp.py +0 -0
  52. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/destructive.py +0 -0
  53. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/post_tool.py +0 -0
  54. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/pre_tool.py +0 -0
  55. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/precompact.py +0 -0
  56. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/prompt.py +0 -0
  57. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/runbooks.py +0 -0
  58. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/session_end.py +0 -0
  59. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/session_start.py +0 -0
  60. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/hooks/stop.py +0 -0
  61. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/jobobject.py +0 -0
  62. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/join.py +0 -0
  63. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/joincode.py +0 -0
  64. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/maildexsync.py +0 -0
  65. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/migrate.py +0 -0
  66. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/nightshift.py +0 -0
  67. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/nightshiftdrain.py +0 -0
  68. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/pathenv.py +0 -0
  69. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/report.py +0 -0
  70. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/resolver.py +0 -0
  71. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/serverinit.py +0 -0
  72. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/serverupdate.py +0 -0
  73. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/sidecar.py +0 -0
  74. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/signing.py +0 -0
  75. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/stdio.py +0 -0
  76. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/symdexindex.py +0 -0
  77. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/transport.py +0 -0
  78. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/wizard.py +0 -0
  79. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client/worktree_snapshot.py +0 -0
  80. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client.egg-info/dependency_links.txt +0 -0
  81. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client.egg-info/entry_points.txt +0 -0
  82. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client.egg-info/requires.txt +0 -0
  83. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/firekeep_client.egg-info/top_level.txt +0 -0
  84. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/setup.cfg +0 -0
  85. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_attribution_headers.py +0 -0
  86. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_autoupdate.py +0 -0
  87. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_background.py +0 -0
  88. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_bash_layer_retired.py +0 -0
  89. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_bootstrap_consent_pty.py +0 -0
  90. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_bootstrap_docdex.py +0 -0
  91. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_bootstrap_maildex.py +0 -0
  92. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_bootstrap_ps1.py +0 -0
  93. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_bootstrap_reinstall.py +0 -0
  94. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_bootstrap_sh.py +0 -0
  95. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_bootstrap_symdex.py +0 -0
  96. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_bootstrap_venv_provisioning.py +0 -0
  97. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli.py +0 -0
  98. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_backup.py +0 -0
  99. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_dex.py +0 -0
  100. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_docdex.py +0 -0
  101. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_doctor.py +0 -0
  102. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_doctor_generic.py +0 -0
  103. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_doctor_report.py +0 -0
  104. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_entry.py +0 -0
  105. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_generic_install.py +0 -0
  106. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_hands.py +0 -0
  107. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_maildex.py +0 -0
  108. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_onboarding.py +0 -0
  109. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_personal.py +0 -0
  110. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_uninstall.py +0 -0
  111. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_uninstall_generic.py +0 -0
  112. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_cli_update.py +0 -0
  113. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_config_migration.py +0 -0
  114. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_connect.py +0 -0
  115. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_decision_board_html.py +0 -0
  116. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_decision_bypass.py +0 -0
  117. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_decision_poll_ceiling.py +0 -0
  118. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_decision_registration.py +0 -0
  119. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_decision_server.py +0 -0
  120. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_dexes.py +0 -0
  121. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_docdexsync.py +0 -0
  122. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_docs_reference_client_kit.py +0 -0
  123. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_doctor_credential_expiry.py +0 -0
  124. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_doctor_embeddings.py +0 -0
  125. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_doctor_no_server.py +0 -0
  126. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_gateway.py +0 -0
  127. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_gateway_schema_slimming.py +0 -0
  128. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_generic_config.py +0 -0
  129. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_hooklog.py +0 -0
  130. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_import_boundary.py +0 -0
  131. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_init_self_enroll.py +0 -0
  132. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_init_stale_bundle.py +0 -0
  133. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_install_skill_contract.py +0 -0
  134. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_instruction_budget.py +0 -0
  135. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_jobobject.py +0 -0
  136. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_join.py +0 -0
  137. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_join_config_write.py +0 -0
  138. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_joincode.py +0 -0
  139. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_kit_smoke.py +0 -0
  140. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_maildexsync.py +0 -0
  141. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_make_release.py +0 -0
  142. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_memory_instructions.py +0 -0
  143. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_migration_entrypoints.py +0 -0
  144. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_nightshift.py +0 -0
  145. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_nightshiftdrain.py +0 -0
  146. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_pathenv.py +0 -0
  147. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_report_bootstrap_enums.py +0 -0
  148. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_report_capture.py +0 -0
  149. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_report_consent.py +0 -0
  150. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_report_consent_surfaces.py +0 -0
  151. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_report_flush_points.py +0 -0
  152. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_report_integration.py +0 -0
  153. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_report_schema.py +0 -0
  154. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_report_spool.py +0 -0
  155. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_resolver.py +0 -0
  156. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_resolver_bypass.py +0 -0
  157. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_resolver_single_connection.py +0 -0
  158. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_runbook_match_parity.py +0 -0
  159. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_runbook_state.py +0 -0
  160. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_scratch_ttl.py +0 -0
  161. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_serverinit.py +0 -0
  162. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_serverupdate.py +0 -0
  163. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_shim_bridge.py +0 -0
  164. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_shim_bypass.py +0 -0
  165. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_shim_eof_exit.py +0 -0
  166. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_shim_fail_loud.py +0 -0
  167. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_shim_recovery.py +0 -0
  168. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_shim_skeleton.py +0 -0
  169. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_sidecar.py +0 -0
  170. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_sidecar_robustness.py +0 -0
  171. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_signing.py +0 -0
  172. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_state.py +0 -0
  173. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_stdio_encoding.py +0 -0
  174. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_symdexindex.py +0 -0
  175. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_transport.py +0 -0
  176. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_transport_sse.py +0 -0
  177. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_twin_removed.py +0 -0
  178. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_updater.py +0 -0
  179. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_wizard.py +0 -0
  180. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_wizard_generic.py +0 -0
  181. {firekeep_client-1.7.2 → firekeep_client-1.8.0}/tests/test_worktree_snapshot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firekeep-client
3
- Version: 1.7.2
3
+ Version: 1.8.0
4
4
  Summary: Client for Firekeep, the self-hosted operating layer for connected AI agents.
5
5
  License-Expression: LicenseRef-Firekeep-BUSL-1.1
6
6
  Project-URL: Homepage, https://firekeep.ai
@@ -9,4 +9,4 @@ versions without a verdict (`_check_versions`) and judges staleness against the
9
9
  release manifest instead (`_check_client_version`).
10
10
  """
11
11
 
12
- __version__ = "1.7.2"
12
+ __version__ = "1.8.0"
@@ -46,6 +46,9 @@ def _toml_block(venv_bin: Path) -> str:
46
46
  # TOML literal (single-quoted) string: no escape processing -> Windows backslashes safe.
47
47
  lines.append(f"command = '{cmd}'")
48
48
  lines.append(f"args = [{arglist}]")
49
+ # Codex clears the env of stdio MCP servers; forward what Firekeep
50
+ # Studio sets per run (spec 2026-10-11 §7.2).
51
+ lines.append('env_vars = ["FIREKEEP_SESSION_REPORT_FILE"]')
49
52
  lines.append("")
50
53
  return "\n".join(lines).rstrip()
51
54
 
@@ -65,7 +65,7 @@ _KIT = frozenset({
65
65
  "FIREKEEP_SHIM_DEBUG", "FIREKEEP_NO_JOB_OBJECT",
66
66
  "FIREKEEP_REPORT_CONSENT", "FIREKEEP_REPORT_DIR",
67
67
  "FIREKEEP_FAILURE_REPORT", "FIREKEEP_NO_FAILURE_REPORT",
68
- "FIREKEEP_SESSION_ID", "NEXUS_AGENT_ID",
68
+ "FIREKEEP_SESSION_ID", "FIREKEEP_SESSION_REPORT_FILE", "NEXUS_AGENT_ID",
69
69
  # hook-side knobs the client modules a child imports may consult
70
70
  "FIREKEEP_AGENT_GOAL", "FIREKEEP_BRIEFING", "FIREKEEP_NO_AUTO_UPDATE",
71
71
  "FIREKEEP_NO_AUTO_SYNC", "FIREKEEP_NO_AUTO_INDEX", "FIREKEEP_NO_AUTO_NIGHTSHIFT",
@@ -87,6 +87,44 @@ def cmd_version(args) -> int:
87
87
  return 0
88
88
 
89
89
 
90
+ # --- whoami: identity facts for Studio 0.5 -----------------------------------
91
+
92
+ def cmd_whoami(args) -> int:
93
+ """Identity facts a local client (Firekeep Studio) needs — never the API key.
94
+
95
+ Studio must not read ~/.firekeep/config itself because the file holds the
96
+ key, so this is the one place it learns the agent id (spec 2026-10-11 §7.1).
97
+ """
98
+ cfg = None
99
+ try:
100
+ cfg = resolver.load_config(_config_path())
101
+ except resolver.ConfigMigrationConflict as exc:
102
+ print(f"firekeep: {exc}", file=sys.stderr)
103
+ return 3
104
+ except resolver.ConfigError:
105
+ cfg = None
106
+ agent: str | None = None
107
+ if cfg is not None:
108
+ try:
109
+ agent = resolver.agent_id(cfg)
110
+ except resolver.ConfigError:
111
+ agent = None
112
+ if agent == wizard.PLACEHOLDER_AGENT_ID:
113
+ agent = None # the installer skeleton value is "unconfigured"
114
+ facts = {
115
+ "agent_id": agent,
116
+ "kit_version": __version__,
117
+ "personal_mode": bool(resolver.is_bypassed()),
118
+ "server_configured": bool(cfg is not None and wizard._already_connected(cfg)),
119
+ }
120
+ if getattr(args, "json", False):
121
+ print(json.dumps(facts))
122
+ else:
123
+ for key, value in facts.items():
124
+ print(f"{key}: {value}")
125
+ return 0 if agent else 1
126
+
127
+
90
128
  # --- install: venv, pip, ~/.firekeep bootstrap, adapter render -----------------
91
129
 
92
130
  # `configured = false` is the sentinel that makes "never set up" a REPRESENTABLE
@@ -277,7 +315,18 @@ def _venv_has_pip(venv: Path) -> bool:
277
315
  return True
278
316
 
279
317
 
280
- def _create_venv(venv: Path) -> None:
318
+ def _is_live_venv(home: Path, venv: Path) -> bool:
319
+ """True when the `current` link resolves to `venv` — the install every hook runs."""
320
+ current = home / CURRENT_LINK_NAME
321
+ if not os.path.lexists(current):
322
+ return False
323
+ try:
324
+ return os.path.normcase(os.path.realpath(current)) == os.path.normcase(os.path.realpath(venv))
325
+ except OSError:
326
+ return False
327
+
328
+
329
+ def _create_venv(venv: Path, home: Path) -> None:
281
330
  if _venv_bin(venv).exists():
282
331
  if _venv_has_pip(venv):
283
332
  return
@@ -285,6 +334,16 @@ def _create_venv(venv: Path) -> None:
285
334
  # bootstrap that died between `uv venv` and `uv pip install` leaves exactly this.
286
335
  # Short-circuiting here used to crash the checkout install later in _pip_install;
287
336
  # rebuild in place instead (--clear reprovisions, pip included via ensurepip).
337
+ #
338
+ # Never the LIVE venv, though: a healthy bootstrap venv is pip-less too, and a
339
+ # checkout whose version matches the installed one lands here. On 2026-10-12 the
340
+ # --clear died on executables held by running sessions, leaving Scripts/ without
341
+ # a stdlib, and every hook on the machine failed until the venv was rebuilt.
342
+ if _is_live_venv(home, venv):
343
+ raise RuntimeError(
344
+ f"{venv} is the live install (`current` points at it) and has no pip; "
345
+ "rebuilding it would break every running session. Bump the checkout's "
346
+ "version, or run `firekeep update` to repair the installed release.")
288
347
  _run([sys.executable, "-m", "venv", "--clear", str(venv)])
289
348
  return
290
349
  _run([sys.executable, "-m", "venv", str(venv)])
@@ -555,7 +614,7 @@ def cmd_install(args) -> int:
555
614
  # Checkout install: provision a fresh versioned venv at its FINAL path
556
615
  # (never moved — venvs aren't relocatable) and flip `current` to it below.
557
616
  venv = home / VENVS_DIR_NAME / _kit_version(kit)
558
- _create_venv(venv)
617
+ _create_venv(venv, home)
559
618
  else:
560
619
  venv = Path(sys.executable).resolve().parent.parent
561
620
  python = _venv_python(venv)
@@ -3147,6 +3206,10 @@ def _build_parser() -> argparse.ArgumentParser:
3147
3206
  ver = sub.add_parser("version", help="print client version + skew status")
3148
3207
  ver.set_defaults(func=cmd_version)
3149
3208
 
3209
+ who = sub.add_parser("whoami", help="print this machine's Firekeep identity (never the key)")
3210
+ who.add_argument("--json", action="store_true", help="one line of JSON for programs")
3211
+ who.set_defaults(func=cmd_whoami)
3212
+
3150
3213
  inst = sub.add_parser("install", help="install/refresh the client kit")
3151
3214
  # An unset runtime prepares every shipped adapter. Explicit --runtime (or the
3152
3215
  # bootstrap's FIREKEEP_RUNTIME) remains available for a targeted re-render.
@@ -66,6 +66,15 @@ TOOLSET_PRESETS: dict[str, frozenset[str]] = {
66
66
  "ctx_abandon_session", "ctx_list_sessions", "ctx_resume_session",
67
67
  "ctx_get_shadow",
68
68
  }),
69
+ # Firekeep Studio's Keep panel and per-turn receipts (Studio 0.5, spec
70
+ # 2026-10-11 §7.1). Reads plus knowledge feedback only: Studio never
71
+ # starts, updates or completes a Keep session — the agent owns those.
72
+ "studio": frozenset({
73
+ "memory_recall", "memory_feedback",
74
+ "skill_list", "skill_recall",
75
+ "relay_who_is_online", "relay_read",
76
+ "ctx_list_sessions", "replay_timeline", "corpus_sources",
77
+ }),
69
78
  }
70
79
 
71
80
  # A preset that narrows the tool surface also narrows the handshake text: the
@@ -286,6 +286,15 @@ def remember(agent: str, ids: list[str]) -> None:
286
286
  hooklog.log_failure(_HOOK, f"proactive recall dedupe write failed: {e}")
287
287
 
288
288
 
289
+ def _keep_session(cfg) -> str | None:
290
+ """The agent's current Keep session, so the pushed recall is attributed to
291
+ it in replay (Studio 0.5 receipts, spec §7.2). Best-effort: None on any error."""
292
+ try:
293
+ return state.current_session_id(resolver.agent_id(cfg))
294
+ except Exception: # noqa: BLE001
295
+ return None
296
+
297
+
289
298
  def recall(cfg, prompt: str) -> list:
290
299
  """POST the prompt to cortex `/memory/recall` and return its `sources`.
291
300
 
@@ -309,12 +318,15 @@ def recall(cfg, prompt: str) -> list:
309
318
  memory filed under `infrastructure` and friends — 146 of them on the live store,
310
319
  which is the defect `docs/guides/cortex-design-decisions.md` records.
311
320
 
321
+ It carries the agent's current Keep session (`X-Session-Id`) so the recall shows
322
+ in that session's replay timeline.
323
+
312
324
  Returns [] for a DEGRADED response (vector search down, results graph-only).
313
325
  Same call as Bridge makes: the floor below is calibrated on cosine, and a
314
326
  graph-only set is a different scale, so pushing it would be pushing noise the
315
327
  threshold cannot judge.
316
328
  """
317
- ep = resolver.resolve("cortex", cfg=cfg)
329
+ ep = resolver.resolve("cortex", cfg=cfg, session_id=_keep_session(cfg))
318
330
  body = {
319
331
  "task": prompt[:MAX_TASK_CHARS],
320
332
  "top_k": TOP_K,
@@ -159,8 +159,9 @@ class _StashSessionAuth(httpx.Auth):
159
159
  # Tap-wired: what THIS process observed, or nothing.
160
160
  sid = self._tap._session_id
161
161
  else:
162
- stash = state.read_session_stash(self._agent)
163
- sid = (stash or {}).get("session_id")
162
+ # Fresh stash first; under Studio, the report file survives
163
+ # the stash clear a resumed runtime's session_start performs.
164
+ sid = state.current_session_id(self._agent)
164
165
  if sid:
165
166
  request.headers["X-Session-Id"] = sid
166
167
  except Exception:
@@ -515,11 +516,15 @@ class _BridgeSessionTap:
515
516
  return item
516
517
  name = params.get("name")
517
518
  rid = getattr(root, "id", None)
518
- if rid is not None and (
519
- name in self._CAPTURE_TOOLS
520
- or name in self._END_TOOLS
521
- ):
519
+ if rid is not None and name in self._CAPTURE_TOOLS:
522
520
  self._pending[rid] = name
521
+ elif rid is not None and name in self._END_TOOLS:
522
+ # Remember WHICH session the call targets: an explicit id from
523
+ # the agent wins, else this process's captured id.
524
+ raw_args = params.get("arguments")
525
+ explicit = raw_args.get("session_id") if isinstance(raw_args, dict) else None
526
+ target = explicit if isinstance(explicit, str) and explicit else self._session_id
527
+ self._pending[rid] = (name, target)
523
528
  if name in self._INJECT_TOOLS:
524
529
  args = params.get("arguments")
525
530
  if isinstance(args, dict):
@@ -546,8 +551,16 @@ class _BridgeSessionTap:
546
551
  rid = getattr(root, "id", None)
547
552
  if rid is None or rid not in self._pending:
548
553
  return item
549
- name = self._pending.pop(rid)
554
+ entry = self._pending.pop(rid)
555
+ target = None
556
+ if isinstance(entry, tuple):
557
+ name, target = entry
558
+ else:
559
+ name = entry
550
560
  if name in self._END_TOOLS:
561
+ # Tombstone the session the call targeted before clearing.
562
+ if target:
563
+ state.end_session_report(target)
551
564
  self._session_id = None
552
565
  state.clear_session_stash(self._agent)
553
566
  return item
@@ -557,6 +570,9 @@ class _BridgeSessionTap:
557
570
  # Keep writing the agent-keyed disk stash: hook cores in other
558
571
  # processes still read it, and it is the restart fallback.
559
572
  state.write_session_stash(self._agent, session_id=sid)
573
+ # Studio 0.5: tell the launching Studio session which Keep
574
+ # session this agent is in (no-op unless the env var is set).
575
+ state.append_session_report(sid)
560
576
  except Exception:
561
577
  pass
562
578
  return item
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  import json
8
8
  import os
9
+ import re
9
10
  import subprocess
10
11
  import sys
11
12
  import time
@@ -448,6 +449,90 @@ def clear_session_stash(agent_id: str) -> None:
448
449
  pass
449
450
 
450
451
 
452
+ # Firekeep Studio 0.5 (spec 2026-10-11 §7.2): Studio launches each agent with
453
+ # this variable naming a per-Studio-session file. The bridge tap appends every
454
+ # Keep session id it captures, so Studio can build per-turn receipts from the
455
+ # sessions the AGENT started — Studio never starts one itself.
456
+ SESSION_REPORT_ENV = "FIREKEEP_SESSION_REPORT_FILE"
457
+ # Ids travel in an HTTP header (X-Session-Id): no whitespace, no newline, ever.
458
+ _SESSION_ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}")
459
+
460
+
461
+ def _report_path() -> str | None:
462
+ raw = os.environ.get(SESSION_REPORT_ENV, "").strip()
463
+ return raw or None
464
+
465
+
466
+ def append_session_report(session_id: str) -> None:
467
+ """Append a captured Keep session id to $FIREKEEP_SESSION_REPORT_FILE.
468
+ No-op when the variable is unset or the id is not header-safe. Never raises."""
469
+ try:
470
+ path = _report_path()
471
+ if not path or not isinstance(session_id, str) or not _SESSION_ID_RE.fullmatch(session_id):
472
+ return
473
+ with open(path, "a", encoding="utf-8", newline="\n") as fh:
474
+ fh.write(session_id + "\n")
475
+ except Exception:
476
+ pass
477
+
478
+
479
+ def end_session_report(session_id: str) -> None:
480
+ """Mark a session as ended in $FIREKEEP_SESSION_REPORT_FILE by appending
481
+ '#ended <id>'. No-op when the variable is unset or the id is not header-safe.
482
+ Never raises."""
483
+ try:
484
+ path = _report_path()
485
+ if not path or not isinstance(session_id, str) or not _SESSION_ID_RE.fullmatch(session_id):
486
+ return
487
+ with open(path, "a", encoding="utf-8", newline="\n") as fh:
488
+ fh.write(f"#ended {session_id}\n")
489
+ except Exception:
490
+ pass
491
+
492
+
493
+ def reported_session_id() -> str | None:
494
+ """The last active (not ended) header-safe id in $FIREKEEP_SESSION_REPORT_FILE.
495
+ A line '#ended <id>' marks an id as ended; scanning in order, the last active
496
+ id is returned, or None. A file whose mtime is older than the session stash
497
+ TTL is ignored (a stale file must not attribute a new day's work to a dead
498
+ session). Handles non-UTF-8 bytes gracefully."""
499
+ try:
500
+ path = _report_path()
501
+ if not path:
502
+ return None
503
+ if time.time() - os.path.getmtime(path) > _session_stash_ttl_seconds():
504
+ return None
505
+ last = None
506
+ with open(path, encoding="utf-8", errors="replace") as fh:
507
+ for line in fh:
508
+ line = line.strip()
509
+ if line.startswith("#ended "):
510
+ # Extract the id after "#ended "
511
+ ended_id = line[7:] # len("#ended ") == 7
512
+ # Deliberately fail-closed: a tombstone clears only the
513
+ # most recent id, so "K1, K2, #ended K2" yields None even
514
+ # if K1 is still live. Tracking a set of ended ids would
515
+ # revive sessions that ended without writing a tombstone.
516
+ if ended_id == last:
517
+ last = None
518
+ elif _SESSION_ID_RE.fullmatch(line):
519
+ last = line
520
+ return last
521
+ except Exception:
522
+ return None
523
+
524
+
525
+ def current_session_id(agent_id: str) -> str | None:
526
+ """The Keep session this agent is working in: the fresh stash, else the
527
+ Studio report file (which survives the stash clear a resumed runtime's
528
+ session_start performs). Never raises."""
529
+ try:
530
+ stash = read_session_stash(agent_id) or {}
531
+ return stash.get("session_id") or reported_session_id()
532
+ except Exception:
533
+ return None
534
+
535
+
451
536
  # --- runbook bundle store (Enforced Runbooks Phase B) ------------------------
452
537
  #
453
538
  # Spec: docs/superpowers/specs/2026-08-15-enforced-runbooks-design.md ("Bundle").
@@ -1,7 +1,7 @@
1
1
  """Release-manifest fetch, version compare, and verified download for `firekeep update`.
2
2
 
3
3
  Stdlib-only (import boundary). Optional non-stdlib import: truststore (guarded) — OS-trust
4
- for RELEASE-HOST fetches only. The GitLab host is NEVER hardcoded: the bootstrap knows the
4
+ for RELEASE-HOST fetches only. The release host is NEVER hardcoded: the bootstrap knows the
5
5
  URL it was fetched from and records it as [dist] base_url in ~/.firekeep/config, which is the
6
6
  only place this module learns it from.
7
7
  """
@@ -18,7 +18,7 @@ from firekeep_client.transport import TransportError, get_json
18
18
 
19
19
 
20
20
  def dist_ssl_context() -> "ssl.SSLContext | None":
21
- """OS-trust SSL context for release-host fetches ONLY (GitHub Pages / GitLab sit
21
+ """OS-trust SSL context for release-host fetches ONLY (a release host can sit
22
22
  behind corporate TLS interception; the managed CPython's default bundle lacks the
23
23
  interception CA). Scoped on purpose — truststore.inject_into_ssl() would replace
24
24
  ssl.SSLContext process-wide and widen office ca_path pinning to 'pin OR OS store'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firekeep-client
3
- Version: 1.7.2
3
+ Version: 1.8.0
4
4
  Summary: Client for Firekeep, the self-hosted operating layer for connected AI agents.
5
5
  License-Expression: LicenseRef-Firekeep-BUSL-1.1
6
6
  Project-URL: Homepage, https://firekeep.ai
@@ -82,7 +82,6 @@ tests/test_bootstrap_reinstall.py
82
82
  tests/test_bootstrap_sh.py
83
83
  tests/test_bootstrap_symdex.py
84
84
  tests/test_bootstrap_venv_provisioning.py
85
- tests/test_ci_publishes_symdex.py
86
85
  tests/test_cli.py
87
86
  tests/test_cli_backup.py
88
87
  tests/test_cli_dex.py
@@ -100,6 +99,7 @@ tests/test_cli_personal.py
100
99
  tests/test_cli_uninstall.py
101
100
  tests/test_cli_uninstall_generic.py
102
101
  tests/test_cli_update.py
102
+ tests/test_cli_whoami.py
103
103
  tests/test_config_migration.py
104
104
  tests/test_connect.py
105
105
  tests/test_decision_board_html.py
@@ -166,6 +166,7 @@ tests/test_sidecar.py
166
166
  tests/test_sidecar_robustness.py
167
167
  tests/test_signing.py
168
168
  tests/test_state.py
169
+ tests/test_state_session_report.py
169
170
  tests/test_stdio_encoding.py
170
171
  tests/test_symdexindex.py
171
172
  tests/test_transport.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "firekeep-client"
7
- version = "1.7.2"
7
+ version = "1.8.0"
8
8
  description = "Client for Firekeep, the self-hosted operating layer for connected AI agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -596,10 +596,48 @@ def test_create_venv_recreates_a_pipless_venv(tmp_path, monkeypatch):
596
596
  calls = []
597
597
  monkeypatch.setattr(cli, "_run", lambda cmd, **kw: calls.append(list(cmd)))
598
598
  monkeypatch.setattr(cli, "_venv_has_pip", lambda v: False)
599
- cli._create_venv(venv)
599
+ cli._create_venv(venv, tmp_path)
600
600
  assert calls and "--clear" in calls[0], f"expected a --clear rebuild, got {calls}"
601
601
 
602
602
 
603
+ def test_create_venv_never_clears_the_live_venv(tmp_path, monkeypatch):
604
+ """Live incident 2026-10-12: a checkout `install` run while the checkout's version
605
+ equalled the installed one targeted venvs/<V> — the venv `current` points at. The
606
+ bootstrap's uv venv ships no pip, so the pip-less branch `--clear`ed it; executables
607
+ held by running sessions stopped the clear halfway, leaving Scripts/ with no stdlib
608
+ or pyvenv.cfg, and every hook on the machine failed. The live venv is never cleared."""
609
+ venv = tmp_path / "venvs" / "1.7.2"
610
+ bindir = venv / ("Scripts" if cli.os.name == "nt" else "bin")
611
+ bindir.mkdir(parents=True)
612
+ cli._point_current(tmp_path, venv)
613
+ calls = []
614
+ monkeypatch.setattr(cli, "_run", lambda cmd, **kw: calls.append(list(cmd)))
615
+ monkeypatch.setattr(cli, "_venv_has_pip", lambda v: False) # exactly a uv venv
616
+ with pytest.raises(RuntimeError, match="current"):
617
+ cli._create_venv(venv, tmp_path)
618
+ assert calls == [], f"the live venv must not be rebuilt, got {calls}"
619
+ assert bindir.is_dir()
620
+
621
+
622
+ def test_checkout_install_of_the_installed_version_leaves_the_live_venv(install_env, monkeypatch, tmp_path, capsys):
623
+ """The incident path end to end: the checkout reads the version already installed
624
+ (1.2.3 here), `current` points at venvs/1.2.3, and that bootstrap venv has no pip."""
625
+ home, runs, _ = install_env
626
+ live = home / "venvs" / "1.2.3"
627
+ (live / ("Scripts" if os.name == "nt" else "bin")).mkdir(parents=True)
628
+ cli._point_current(home, live)
629
+ monkeypatch.setattr(cli, "_venv_has_pip", lambda v: False)
630
+ pips = []
631
+ monkeypatch.setattr(cli, "_pip_install", lambda py, *pkgs, **k: pips.append(pkgs))
632
+ _checkout(tmp_path, monkeypatch)
633
+
634
+ rc = cli.main(["install", "--runtime", "claude"])
635
+ assert rc == 1
636
+ assert "install failed at 'create venv'" in capsys.readouterr().err
637
+ assert not [r for r in runs if "venv" in r], f"live venv rebuilt: {runs}"
638
+ assert pips == []
639
+
640
+
603
641
  def test_create_venv_skips_a_healthy_venv(tmp_path, monkeypatch):
604
642
  venv = tmp_path / "venv"
605
643
  bindir = venv / ("Scripts" if cli.os.name == "nt" else "bin")
@@ -607,7 +645,7 @@ def test_create_venv_skips_a_healthy_venv(tmp_path, monkeypatch):
607
645
  calls = []
608
646
  monkeypatch.setattr(cli, "_run", lambda cmd, **kw: calls.append(list(cmd)))
609
647
  monkeypatch.setattr(cli, "_venv_has_pip", lambda v: True)
610
- cli._create_venv(venv)
648
+ cli._create_venv(venv, tmp_path)
611
649
  assert calls == []
612
650
 
613
651
 
@@ -0,0 +1,101 @@
1
+ """`firekeep whoami` — identity facts for local clients (Firekeep Studio 0.5,
2
+ spec §7.1). Studio must never read ~/.firekeep/config itself: it holds the key."""
3
+ import json
4
+ import textwrap
5
+
6
+ import pytest
7
+
8
+ from firekeep_client import __version__, cli
9
+
10
+ SECRET = "fk_live_do_not_print_me"
11
+
12
+ CONFIG = textwrap.dedent(f"""\
13
+ [identity]
14
+ agent_id = tester
15
+
16
+ [server]
17
+ kind = ports
18
+ scheme = http
19
+ host = 10.0.0.5
20
+ verify_tls = false
21
+ api_key = {SECRET}
22
+ """)
23
+
24
+
25
+ @pytest.fixture
26
+ def config_file(tmp_path, monkeypatch):
27
+ cfg = tmp_path / ".firekeep" / "config"
28
+ cfg.parent.mkdir(parents=True)
29
+ cfg.write_text(CONFIG, encoding="utf-8")
30
+ monkeypatch.setenv("FIREKEEP_CONFIG", str(cfg))
31
+ monkeypatch.setenv("FIREKEEP_CACHE_DIR", str(tmp_path / "cache"))
32
+ monkeypatch.delenv("FIREKEEP_AGENT_ID", raising=False)
33
+ monkeypatch.setattr("firekeep_client.state._private", lambda _p: None)
34
+ monkeypatch.setattr("firekeep_client.resolver.is_bypassed", lambda *a, **k: False)
35
+ return cfg
36
+
37
+
38
+ def test_whoami_json_reports_identity(config_file, capsys):
39
+ assert cli.main(["whoami", "--json"]) == 0
40
+ out = capsys.readouterr().out
41
+ assert json.loads(out) == {
42
+ "agent_id": "tester",
43
+ "kit_version": __version__,
44
+ "personal_mode": False,
45
+ "server_configured": True,
46
+ }
47
+
48
+
49
+ def test_whoami_never_prints_the_api_key(config_file, capsys):
50
+ cli.main(["whoami", "--json"])
51
+ cli.main(["whoami"])
52
+ captured = capsys.readouterr()
53
+ assert SECRET not in captured.out
54
+ assert SECRET not in captured.err
55
+
56
+
57
+ def test_whoami_honours_agent_id_env(config_file, monkeypatch, capsys):
58
+ monkeypatch.setenv("FIREKEEP_AGENT_ID", "tester-studio-panel")
59
+ cli.main(["whoami", "--json"])
60
+ assert json.loads(capsys.readouterr().out)["agent_id"] == "tester-studio-panel"
61
+
62
+
63
+ def test_whoami_reports_personal_mode(config_file, monkeypatch, capsys):
64
+ monkeypatch.setattr("firekeep_client.resolver.is_bypassed", lambda *a, **k: True)
65
+ cli.main(["whoami", "--json"])
66
+ assert json.loads(capsys.readouterr().out)["personal_mode"] is True
67
+
68
+
69
+ def test_whoami_reports_bypass_env_as_personal_mode(config_file, monkeypatch, capsys):
70
+ monkeypatch.undo() # drop is_bypassed patch; exercise the real predicate
71
+ monkeypatch.setenv("FIREKEEP_CONFIG", str(config_file))
72
+ monkeypatch.setenv("FIREKEEP_CACHE_DIR", str(config_file.parent / "cache"))
73
+ monkeypatch.setenv("FIREKEEP_BYPASS", "1")
74
+ cli.main(["whoami", "--json"])
75
+ assert json.loads(capsys.readouterr().out)["personal_mode"] is True
76
+
77
+
78
+ def test_whoami_placeholder_agent_id_is_unconfigured(config_file, capsys):
79
+ config_file.write_text(
80
+ config_file.read_text(encoding="utf-8").replace("agent_id = tester", "agent_id = CHANGEME"),
81
+ encoding="utf-8",
82
+ )
83
+ assert cli.main(["whoami", "--json"]) == 1
84
+ assert json.loads(capsys.readouterr().out)["agent_id"] is None
85
+
86
+
87
+ def test_whoami_without_config_is_null_not_a_crash(tmp_path, monkeypatch, capsys):
88
+ monkeypatch.setenv("FIREKEEP_CONFIG", str(tmp_path / "missing" / "config"))
89
+ monkeypatch.delenv("FIREKEEP_AGENT_ID", raising=False)
90
+ monkeypatch.setattr("firekeep_client.resolver.is_bypassed", lambda *a, **k: False)
91
+ assert cli.main(["whoami", "--json"]) == 1
92
+ data = json.loads(capsys.readouterr().out)
93
+ assert data["agent_id"] is None
94
+ assert data["server_configured"] is False
95
+
96
+
97
+ def test_whoami_plain_text_is_one_fact_per_line(config_file, capsys):
98
+ assert cli.main(["whoami"]) == 0
99
+ out = capsys.readouterr().out.splitlines()
100
+ assert out[0] == "agent_id: tester"
101
+ assert f"kit_version: {__version__}" in out
@@ -83,8 +83,8 @@ VERSION = tomllib.loads((CLIENT / "pyproject.toml").read_text())["project"]["ver
83
83
  @pytest.fixture
84
84
  def release(tmp_path):
85
85
  """Build a REAL release: real wheel, real mirrored uv, real make_release.py — then fan it
86
- out into the REAL version-addressed layout CI now publishes, exactly as the upload() loop
87
- in .gitlab-ci.yml does: BASE/latest/{install.sh,install.ps1,latest.json} (the stable
86
+ out into the REAL version-addressed layout release.yml publishes:
87
+ BASE/latest/{install.sh,install.ps1,latest.json} (the stable
88
88
  entry point) + BASE/<version>/{SHA256SUMS,uv-<target>,wheel} (every version keeps its own
89
89
  directory). Serving flat (the old shape) would silently resurrect C1/C3 — a tag-pinned
90
90
  base whose latest.json points at the release it shipped inside, forever."""
@@ -345,3 +345,9 @@ def test_every_env_name_a_child_reads_is_classified():
345
345
  "child may see it, CHILD_EXTRAS[<child>] if it is that child's own secret, "
346
346
  "NOT_FORWARDED if no gateway child needs it"
347
347
  )
348
+
349
+
350
+ def test_session_report_file_reaches_backend_children():
351
+ from firekeep_client import childenv
352
+
353
+ assert "FIREKEEP_SESSION_REPORT_FILE" in childenv.ALLOWED_NAMES
@@ -151,3 +151,44 @@ def test_chat_preset_is_exactly_the_decided_surface():
151
151
  "ctx_abandon_session", "ctx_list_sessions", "ctx_resume_session",
152
152
  "ctx_get_shadow",
153
153
  }
154
+
155
+
156
+ STUDIO_SURFACE = frozenset({
157
+ "memory_recall", "memory_feedback",
158
+ "skill_list", "skill_recall",
159
+ "relay_who_is_online", "relay_read",
160
+ "ctx_list_sessions", "replay_timeline", "corpus_sources",
161
+ })
162
+
163
+
164
+ def test_studio_preset_is_exactly_the_decided_surface():
165
+ from firekeep_client.gateway import TOOLSET_PRESETS
166
+
167
+ assert TOOLSET_PRESETS["studio"] == STUDIO_SURFACE
168
+
169
+
170
+ def test_studio_preset_cannot_write_sessions_or_memories():
171
+ """Studio reads the Keep and thumbs knowledge; it never owns a session
172
+ (spec §7.2) and never learns on the user's behalf."""
173
+ from firekeep_client.gateway import TOOLSET_PRESETS
174
+
175
+ forbidden = {"ctx_start_session", "ctx_resume_session", "ctx_update",
176
+ "ctx_complete_session", "ctx_abandon_session", "memory_learn",
177
+ "vault_retrieve", "skill_create"}
178
+ assert not (TOOLSET_PRESETS["studio"] & forbidden)
179
+
180
+
181
+ def test_studio_preset_resolves_from_env(monkeypatch):
182
+ monkeypatch.delenv("FIREKEEP_TOOLS_ALLOW", raising=False)
183
+ monkeypatch.setenv("FIREKEEP_TOOLSET", "studio")
184
+ label, names = _active_toolset()
185
+ assert label == "studio"
186
+ assert names == STUDIO_SURFACE
187
+
188
+
189
+ def test_unknown_preset_message_lists_studio(monkeypatch):
190
+ monkeypatch.delenv("FIREKEEP_TOOLS_ALLOW", raising=False)
191
+ monkeypatch.setenv("FIREKEEP_TOOLSET", "studoi")
192
+ with pytest.raises(SystemExit) as exc:
193
+ _active_toolset()
194
+ assert "chat, studio" in str(exc.value)
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  def test_package_imports_and_exposes_frozen_version():
8
8
  import firekeep_client
9
9
 
10
- assert firekeep_client.__version__ == "1.7.2"
10
+ assert firekeep_client.__version__ == "1.8.0"
11
11
 
12
12
 
13
13
  def test_the_version_pins_agree():