firekeep-client 1.3.0__tar.gz → 1.4.2__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 (151) hide show
  1. {firekeep_client-1.3.0/firekeep_client.egg-info → firekeep_client-1.4.2}/PKG-INFO +13 -9
  2. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/README.md +12 -8
  3. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/__init__.py +1 -1
  4. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/adapters/base.py +33 -0
  5. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/gateway.py +75 -4
  6. {firekeep_client-1.3.0 → firekeep_client-1.4.2/firekeep_client.egg-info}/PKG-INFO +13 -9
  7. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client.egg-info/SOURCES.txt +1 -0
  8. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/pyproject.toml +1 -1
  9. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_gateway.py +2 -0
  10. firekeep_client-1.4.2/tests/test_gateway_toolset.py +153 -0
  11. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_package.py +1 -1
  12. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/LICENSE +0 -0
  13. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/NOTICE +0 -0
  14. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/adapters/__init__.py +0 -0
  15. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/adapters/claude.py +0 -0
  16. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/adapters/claude_desktop.py +0 -0
  17. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/adapters/codex.py +0 -0
  18. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/adapters/generic.py +0 -0
  19. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/adapters/kiro.py +0 -0
  20. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/adapters/opencode.py +0 -0
  21. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/autoupdate.py +0 -0
  22. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/backups.py +0 -0
  23. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/cli.py +0 -0
  24. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/config_write.py +0 -0
  25. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/connect.py +0 -0
  26. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/contract/__init__.py +0 -0
  27. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/contract/matrix.py +0 -0
  28. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/decision/__init__.py +0 -0
  29. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/decision/board.py +0 -0
  30. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/decision/server.py +0 -0
  31. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/dexes.py +0 -0
  32. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/docdexsync.py +0 -0
  33. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooklog.py +0 -0
  34. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/__init__.py +0 -0
  35. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/__main__.py +0 -0
  36. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/_git.py +0 -0
  37. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/_mcp.py +0 -0
  38. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/destructive.py +0 -0
  39. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/post_tool.py +0 -0
  40. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/pre_tool.py +0 -0
  41. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/precompact.py +0 -0
  42. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/prompt.py +0 -0
  43. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/runbooks.py +0 -0
  44. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/session_end.py +0 -0
  45. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/session_start.py +0 -0
  46. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/hooks/stop.py +0 -0
  47. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/join.py +0 -0
  48. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/joincode.py +0 -0
  49. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/maildexsync.py +0 -0
  50. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/migrate.py +0 -0
  51. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/nightshift.py +0 -0
  52. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/pathenv.py +0 -0
  53. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/promptrecall.py +0 -0
  54. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/resolver.py +0 -0
  55. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/serverinit.py +0 -0
  56. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/shim.py +0 -0
  57. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/sidecar.py +0 -0
  58. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/signing.py +0 -0
  59. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/state.py +0 -0
  60. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/stdio.py +0 -0
  61. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/symdexindex.py +0 -0
  62. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/transport.py +0 -0
  63. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/updater.py +0 -0
  64. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/wizard.py +0 -0
  65. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client/worktree_snapshot.py +0 -0
  66. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client.egg-info/dependency_links.txt +0 -0
  67. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client.egg-info/entry_points.txt +0 -0
  68. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client.egg-info/requires.txt +0 -0
  69. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/firekeep_client.egg-info/top_level.txt +0 -0
  70. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/setup.cfg +0 -0
  71. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_attribution_headers.py +0 -0
  72. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_autoupdate.py +0 -0
  73. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_bash_layer_retired.py +0 -0
  74. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_bootstrap_docdex.py +0 -0
  75. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_bootstrap_maildex.py +0 -0
  76. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_bootstrap_ps1.py +0 -0
  77. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_bootstrap_reinstall.py +0 -0
  78. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_bootstrap_sh.py +0 -0
  79. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_bootstrap_symdex.py +0 -0
  80. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_bootstrap_venv_provisioning.py +0 -0
  81. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_ci_publishes_symdex.py +0 -0
  82. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli.py +0 -0
  83. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_backup.py +0 -0
  84. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_dex.py +0 -0
  85. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_docdex.py +0 -0
  86. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_doctor.py +0 -0
  87. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_doctor_generic.py +0 -0
  88. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_entry.py +0 -0
  89. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_generic_install.py +0 -0
  90. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_install.py +0 -0
  91. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_maildex.py +0 -0
  92. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_onboarding.py +0 -0
  93. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_personal.py +0 -0
  94. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_uninstall.py +0 -0
  95. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_uninstall_generic.py +0 -0
  96. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_cli_update.py +0 -0
  97. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_config_migration.py +0 -0
  98. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_connect.py +0 -0
  99. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_decision_board_html.py +0 -0
  100. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_decision_bypass.py +0 -0
  101. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_decision_registration.py +0 -0
  102. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_decision_server.py +0 -0
  103. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_dexes.py +0 -0
  104. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_docdexsync.py +0 -0
  105. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_docs_reference_client_kit.py +0 -0
  106. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_doctor_credential_expiry.py +0 -0
  107. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_doctor_embeddings.py +0 -0
  108. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_doctor_no_server.py +0 -0
  109. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_e2e_bootstrap.py +0 -0
  110. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_generic_config.py +0 -0
  111. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_hooklog.py +0 -0
  112. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_import_boundary.py +0 -0
  113. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_init_self_enroll.py +0 -0
  114. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_init_stale_bundle.py +0 -0
  115. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_join.py +0 -0
  116. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_join_config_write.py +0 -0
  117. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_joincode.py +0 -0
  118. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_kit_smoke.py +0 -0
  119. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_maildexsync.py +0 -0
  120. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_make_release.py +0 -0
  121. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_memory_instructions.py +0 -0
  122. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_migration_entrypoints.py +0 -0
  123. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_nightshift.py +0 -0
  124. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_pathenv.py +0 -0
  125. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_promptrecall.py +0 -0
  126. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_resolver.py +0 -0
  127. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_resolver_bypass.py +0 -0
  128. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_resolver_single_connection.py +0 -0
  129. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_runbook_match_parity.py +0 -0
  130. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_runbook_state.py +0 -0
  131. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_scratch_ttl.py +0 -0
  132. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_serverinit.py +0 -0
  133. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_shim_bridge.py +0 -0
  134. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_shim_bypass.py +0 -0
  135. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_shim_fail_loud.py +0 -0
  136. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_shim_identity.py +0 -0
  137. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_shim_recovery.py +0 -0
  138. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_shim_skeleton.py +0 -0
  139. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_sidecar.py +0 -0
  140. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_sidecar_robustness.py +0 -0
  141. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_signing.py +0 -0
  142. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_state.py +0 -0
  143. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_stdio_encoding.py +0 -0
  144. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_symdexindex.py +0 -0
  145. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_transport.py +0 -0
  146. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_transport_sse.py +0 -0
  147. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_twin_removed.py +0 -0
  148. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_updater.py +0 -0
  149. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_wizard.py +0 -0
  150. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/tests/test_wizard_generic.py +0 -0
  151. {firekeep_client-1.3.0 → firekeep_client-1.4.2}/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.3.0
3
+ Version: 1.4.2
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
@@ -32,7 +32,8 @@ operating layer for connected AI agents**. It carries durable knowledge,
32
32
  working context, procedures, coordination, and replayable evidence across
33
33
  sessions, models, machines, and teammates. Agent-facing MCP tools are exposed
34
34
  through one local `firekeep` gateway. Shipped adapters configure Claude Code,
35
- Codex, Kiro, and OpenCode; other MCP clients can connect through the generic
35
+ Claude Desktop (auto-detected when the app's config dir exists), Codex, Kiro,
36
+ and OpenCode; other MCP clients can connect through the generic
36
37
  configuration path, without hook-driven lifecycle automation.
37
38
 
38
39
  mcp-name: io.github.kapella-hub/firekeep
@@ -44,13 +45,16 @@ aggregates your team server's services (memory, sessions, environment,
44
45
  coordination) plus the Decision Board and whichever **dexes** — the domain
45
46
  indexes the Keep understands — you have registered, the lifecycle hook cores,
46
47
  the runtime adapters, and the `firekeep` CLI (`install`, `join`, `doctor`,
47
- `update`, `dex`, `docdex`).
48
-
49
- Two dexes ship today: `firekeep-symdex` (code intelligence) and
50
- `firekeep-docdex` (folders of documents indexed into the Keep's corpus). The
51
- managed installer always installs both wheels; `firekeep dex list|add|remove`
52
- decides which of them actually run. Existing installs keep symdex across an
53
- update; a fresh install opts in with `firekeep dex add symdex`.
48
+ `update`, `dex`, `docdex`, `maildex`).
49
+
50
+ Three dexes ship today: `firekeep-symdex` (code intelligence),
51
+ `firekeep-docdex` (folders of documents indexed into the Keep's corpus), and
52
+ `firekeep-maildex` (email over read-only IMAP, always member-private,
53
+ registered with `firekeep maildex add`). The managed installer always installs
54
+ all three wheels; `firekeep dex list|add|remove` decides which of them
55
+ actually run. Symdex and docdex are registered by default — since client 1.2.0
56
+ an absent registry is seeded with both (default-on), `firekeep dex remove` is
57
+ the off-switch, and removals stick across updates.
54
58
 
55
59
  **Firekeep is self-hosted — this package needs a server to talk to.** A person
56
60
  or team runs its own server, with per-key authentication. Current server images
@@ -5,7 +5,8 @@ operating layer for connected AI agents**. It carries durable knowledge,
5
5
  working context, procedures, coordination, and replayable evidence across
6
6
  sessions, models, machines, and teammates. Agent-facing MCP tools are exposed
7
7
  through one local `firekeep` gateway. Shipped adapters configure Claude Code,
8
- Codex, Kiro, and OpenCode; other MCP clients can connect through the generic
8
+ Claude Desktop (auto-detected when the app's config dir exists), Codex, Kiro,
9
+ and OpenCode; other MCP clients can connect through the generic
9
10
  configuration path, without hook-driven lifecycle automation.
10
11
 
11
12
  mcp-name: io.github.kapella-hub/firekeep
@@ -17,13 +18,16 @@ aggregates your team server's services (memory, sessions, environment,
17
18
  coordination) plus the Decision Board and whichever **dexes** — the domain
18
19
  indexes the Keep understands — you have registered, the lifecycle hook cores,
19
20
  the runtime adapters, and the `firekeep` CLI (`install`, `join`, `doctor`,
20
- `update`, `dex`, `docdex`).
21
-
22
- Two dexes ship today: `firekeep-symdex` (code intelligence) and
23
- `firekeep-docdex` (folders of documents indexed into the Keep's corpus). The
24
- managed installer always installs both wheels; `firekeep dex list|add|remove`
25
- decides which of them actually run. Existing installs keep symdex across an
26
- update; a fresh install opts in with `firekeep dex add symdex`.
21
+ `update`, `dex`, `docdex`, `maildex`).
22
+
23
+ Three dexes ship today: `firekeep-symdex` (code intelligence),
24
+ `firekeep-docdex` (folders of documents indexed into the Keep's corpus), and
25
+ `firekeep-maildex` (email over read-only IMAP, always member-private,
26
+ registered with `firekeep maildex add`). The managed installer always installs
27
+ all three wheels; `firekeep dex list|add|remove` decides which of them
28
+ actually run. Symdex and docdex are registered by default — since client 1.2.0
29
+ an absent registry is seeded with both (default-on), `firekeep dex remove` is
30
+ the off-switch, and removals stick across updates.
27
31
 
28
32
  **Firekeep is self-hosted — this package needs a server to talk to.** A person
29
33
  or team runs its own server, with per-key authentication. Current server images
@@ -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.3.0"
12
+ __version__ = "1.4.2"
@@ -516,6 +516,36 @@ instead of asking inline. If it returns pending, keep calling decision_board_che
516
516
  until the human answers.
517
517
  """
518
518
 
519
+ # The handshake text for the `chat` toolset (gateway.py, FIREKEEP_TOOLSET=chat —
520
+ # the ChatGPT tunnel surface). NOT derived from MCP_SERVER_INSTRUCTIONS by
521
+ # replace(): that text instructs agents to call vault_retrieve, vault_store,
522
+ # skill_create, action_before and decision_board — none of which the chat preset
523
+ # serves, and an instruction to call a tool that errors is worse than no
524
+ # instruction. This variant may only name tools the preset carries;
525
+ # test_gateway_toolset pins that mechanically (every tool-shaped name mentioned
526
+ # here must be in CHAT_TOOLSET).
527
+ CHAT_INSTRUCTIONS = """\
528
+ Firekeep — persistent team memory, reachable from this chat.
529
+
530
+ Recall BEFORE answering, and treat not knowing as the trigger: if the user names
531
+ a host, IP, path, service or convention you cannot name from the current
532
+ conversation ("my VPS", "our server"), or uses history words ("again", "still",
533
+ "last time", "how did we"), call memory_recall(task=<their request>) first.
534
+ Never claim you don't know about the user's own systems before calling it once.
535
+ For operational or repeated-failure tasks, also check skill_recall(task).
536
+
537
+ Working on something across turns? ctx_start_session(goal) opens a session — its
538
+ response includes prior art: what the team already built or decided near your
539
+ goal. ctx_update as you go, ctx_complete_session(outcome) when done.
540
+
541
+ memory_learn the moment something worth keeping is decided or fixed (include
542
+ what failed first). Never store secrets — no passwords, tokens or keys in
543
+ memory.
544
+
545
+ When recalled knowledge shaped what you DID, call memory_feedback(memory_ids,
546
+ useful) with a one-line comment — only for knowledge you acted on.
547
+ """
548
+
519
549
 
520
550
  def _hash12(text: str) -> str:
521
551
  """sha256(text utf-8), first 12 hex chars — the contract's hash shape."""
@@ -529,6 +559,9 @@ def _hash12(text: str) -> str:
529
559
  # covers the handshake text served fresh from the running wheel every session.
530
560
  RENDERED_INSTRUCTIONS_HASH = _hash12(FIREKEEP_INSTRUCTIONS)
531
561
  GATEWAY_INSTRUCTIONS_HASH = _hash12(GATEWAY_INSTRUCTIONS)
562
+ # Same contract for the chat-toolset handshake: a session's recorded
563
+ # serverInfo.version names exactly which instruction text it received.
564
+ CHAT_INSTRUCTIONS_HASH = _hash12(CHAT_INSTRUCTIONS)
532
565
  # Same definition for the hook-free text. Doctor compares a rendered block
533
566
  # against the hash for ITS runtime — a generic block checked against the four's
534
567
  # hash would read "edited" forever.
@@ -22,7 +22,12 @@ from pathlib import Path
22
22
  from typing import Any
23
23
 
24
24
  from firekeep_client import dexes
25
- from firekeep_client.adapters.base import GATEWAY_INSTRUCTIONS, GATEWAY_INSTRUCTIONS_HASH
25
+ from firekeep_client.adapters.base import (
26
+ CHAT_INSTRUCTIONS,
27
+ CHAT_INSTRUCTIONS_HASH,
28
+ GATEWAY_INSTRUCTIONS,
29
+ GATEWAY_INSTRUCTIONS_HASH,
30
+ )
26
31
  from firekeep_client.stdio import force_utf8_stdio, pin_import_paths
27
32
 
28
33
 
@@ -43,6 +48,56 @@ STATUS_TOOL = {
43
48
  }
44
49
 
45
50
 
51
+ # Named toolsets (FIREKEEP_TOOLSET). A toolset narrows the gateway to a curated
52
+ # surface for hosts where the full ~90 tools are wrong — the ChatGPT tunnel is
53
+ # the founding case: a consumer chat surface, prompt-injection-rich, that still
54
+ # deserves recall, sessions (prior art rides ctx_start_session) and the one
55
+ # write that makes chat valuable (memory_learn — the poisoning risk is carried
56
+ # by replay attribution, runtime: chatgpt, not prevented). Excluded outright:
57
+ # vault, corpus ingest, relay, backup, dex/code tools.
58
+ TOOLSET_PRESETS: dict[str, frozenset[str]] = {
59
+ "chat": frozenset({
60
+ "memory_recall", "memory_learn", "memory_feedback",
61
+ "skill_recall", "skill_list",
62
+ "ctx_start_session", "ctx_update", "ctx_complete_session",
63
+ "ctx_abandon_session", "ctx_list_sessions", "ctx_resume_session",
64
+ "ctx_get_shadow",
65
+ }),
66
+ }
67
+
68
+ # A preset that narrows the tool surface also narrows the handshake text: the
69
+ # default GATEWAY_INSTRUCTIONS instructs agents to call vault_retrieve and
70
+ # decision_board, which the chat preset does not serve. An explicit
71
+ # FIREKEEP_TOOLS_ALLOW keeps the default text — the operator overrode the
72
+ # preset and owns the mismatch.
73
+ _PRESET_INSTRUCTIONS: dict[str, tuple[str, str]] = {
74
+ "chat": (CHAT_INSTRUCTIONS, CHAT_INSTRUCTIONS_HASH),
75
+ }
76
+
77
+
78
+ def _active_toolset() -> tuple[str | None, frozenset[str] | None]:
79
+ """(label, allowlist) from the environment.
80
+
81
+ FIREKEEP_TOOLS_ALLOW (explicit comma-list) wins over FIREKEEP_TOOLSET.
82
+ An UNKNOWN preset refuses to start rather than falling back to the full
83
+ surface: this gateway may sit behind a tunnel reachable from a consumer
84
+ chat host, and a typo must fail closed, not open ~90 tools."""
85
+ allow = os.environ.get("FIREKEEP_TOOLS_ALLOW", "").strip()
86
+ if allow:
87
+ names = frozenset(n.strip() for n in allow.split(",") if n.strip())
88
+ return "allowlist", names
89
+ preset = os.environ.get("FIREKEEP_TOOLSET", "").strip()
90
+ if not preset:
91
+ return None, None
92
+ if preset not in TOOLSET_PRESETS:
93
+ raise SystemExit(
94
+ f"firekeep gateway: unknown FIREKEEP_TOOLSET {preset!r} "
95
+ f"(valid: {', '.join(sorted(TOOLSET_PRESETS))}); refusing to start "
96
+ "rather than serve the full tool surface"
97
+ )
98
+ return preset, TOOLSET_PRESETS[preset]
99
+
100
+
46
101
  def _console_script(name: str) -> str:
47
102
  suffix = ".exe" if os.name == "nt" else ""
48
103
  filename = f"{name}{suffix}"
@@ -210,11 +265,15 @@ class Gateway:
210
265
  ]
211
266
  self.protocol_version = "2025-03-26"
212
267
  self.routes: dict[str, Backend] = {}
268
+ # Fail-closed at construction: an unknown preset never serves a request.
269
+ self.toolset_label, self.toolset = _active_toolset()
270
+ self.tools_filtered = 0
213
271
 
214
272
  def discover(self) -> list[dict[str, Any]]:
215
273
  with ThreadPoolExecutor(max_workers=len(self.backends)) as pool:
216
274
  list(pool.map(lambda backend: backend.discover(self.protocol_version), self.backends))
217
275
  self.routes = {}
276
+ self.tools_filtered = 0
218
277
  tools = [STATUS_TOOL]
219
278
  for backend in self.backends:
220
279
  for tool in backend.tools:
@@ -222,6 +281,12 @@ class Gateway:
222
281
  if name in self.routes or name == STATUS_TOOL["name"]:
223
282
  backend.state = f"degraded: duplicate tool name {name}"
224
283
  continue
284
+ # Toolset filter at the ROUTING layer: an excluded tool never
285
+ # enters self.routes, so it is invisible in tools/list AND a
286
+ # tools/call for it returns -32601 — enforcement, not decoration.
287
+ if self.toolset is not None and name not in self.toolset:
288
+ self.tools_filtered += 1
289
+ continue
225
290
  self.routes[name] = backend
226
291
  tools.append(tool)
227
292
  return tools
@@ -231,6 +296,9 @@ class Gateway:
231
296
  "backends": {backend.name: backend.state for backend in self.backends},
232
297
  "tool_count": len(self.routes),
233
298
  "plan_filtering": False,
299
+ # Disclosure: a narrowed surface must say so. null when unfiltered.
300
+ "toolset": self.toolset_label,
301
+ "tools_filtered": self.tools_filtered,
234
302
  }
235
303
 
236
304
  def handle(self, message: dict[str, Any]) -> dict[str, Any] | None:
@@ -241,6 +309,9 @@ class Gateway:
241
309
  return None
242
310
  if method == "initialize":
243
311
  self.protocol_version = params.get("protocolVersion") or self.protocol_version
312
+ instructions, instructions_hash = _PRESET_INSTRUCTIONS.get(
313
+ self.toolset_label or "", (GATEWAY_INSTRUCTIONS, GATEWAY_INSTRUCTIONS_HASH)
314
+ )
244
315
  return {
245
316
  "jsonrpc": "2.0",
246
317
  "id": request_id,
@@ -250,9 +321,9 @@ class Gateway:
250
321
  # version = the handshake text's content hash, so a session's
251
322
  # recorded serverInfo names exactly which instruction text it
252
323
  # received (round-2 measurement contract) — the hardcoded "1"
253
- # said nothing.
254
- "serverInfo": {"name": "firekeep", "version": GATEWAY_INSTRUCTIONS_HASH},
255
- "instructions": GATEWAY_INSTRUCTIONS,
324
+ # said nothing. A preset that swaps the text swaps the hash.
325
+ "serverInfo": {"name": "firekeep", "version": instructions_hash},
326
+ "instructions": instructions,
256
327
  },
257
328
  }
258
329
  if method == "ping":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firekeep-client
3
- Version: 1.3.0
3
+ Version: 1.4.2
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
@@ -32,7 +32,8 @@ operating layer for connected AI agents**. It carries durable knowledge,
32
32
  working context, procedures, coordination, and replayable evidence across
33
33
  sessions, models, machines, and teammates. Agent-facing MCP tools are exposed
34
34
  through one local `firekeep` gateway. Shipped adapters configure Claude Code,
35
- Codex, Kiro, and OpenCode; other MCP clients can connect through the generic
35
+ Claude Desktop (auto-detected when the app's config dir exists), Codex, Kiro,
36
+ and OpenCode; other MCP clients can connect through the generic
36
37
  configuration path, without hook-driven lifecycle automation.
37
38
 
38
39
  mcp-name: io.github.kapella-hub/firekeep
@@ -44,13 +45,16 @@ aggregates your team server's services (memory, sessions, environment,
44
45
  coordination) plus the Decision Board and whichever **dexes** — the domain
45
46
  indexes the Keep understands — you have registered, the lifecycle hook cores,
46
47
  the runtime adapters, and the `firekeep` CLI (`install`, `join`, `doctor`,
47
- `update`, `dex`, `docdex`).
48
-
49
- Two dexes ship today: `firekeep-symdex` (code intelligence) and
50
- `firekeep-docdex` (folders of documents indexed into the Keep's corpus). The
51
- managed installer always installs both wheels; `firekeep dex list|add|remove`
52
- decides which of them actually run. Existing installs keep symdex across an
53
- update; a fresh install opts in with `firekeep dex add symdex`.
48
+ `update`, `dex`, `docdex`, `maildex`).
49
+
50
+ Three dexes ship today: `firekeep-symdex` (code intelligence),
51
+ `firekeep-docdex` (folders of documents indexed into the Keep's corpus), and
52
+ `firekeep-maildex` (email over read-only IMAP, always member-private,
53
+ registered with `firekeep maildex add`). The managed installer always installs
54
+ all three wheels; `firekeep dex list|add|remove` decides which of them
55
+ actually run. Symdex and docdex are registered by default — since client 1.2.0
56
+ an absent registry is seeded with both (default-on), `firekeep dex remove` is
57
+ the off-switch, and removals stick across updates.
54
58
 
55
59
  **Firekeep is self-hosted — this package needs a server to talk to.** A person
56
60
  or team runs its own server, with per-key authentication. Current server images
@@ -103,6 +103,7 @@ tests/test_doctor_embeddings.py
103
103
  tests/test_doctor_no_server.py
104
104
  tests/test_e2e_bootstrap.py
105
105
  tests/test_gateway.py
106
+ tests/test_gateway_toolset.py
106
107
  tests/test_generic_config.py
107
108
  tests/test_hooklog.py
108
109
  tests/test_import_boundary.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "firekeep-client"
7
- version = "1.3.0"
7
+ version = "1.4.2"
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"
@@ -74,6 +74,8 @@ def test_gateway_advertises_reachable_tools_and_diagnostic_status_only():
74
74
  },
75
75
  "tool_count": 1,
76
76
  "plan_filtering": False,
77
+ "toolset": None,
78
+ "tools_filtered": 0,
77
79
  }
78
80
 
79
81
 
@@ -0,0 +1,153 @@
1
+ """FIREKEEP_TOOLSET / FIREKEEP_TOOLS_ALLOW — the gateway's curated surfaces.
2
+
3
+ The invariants:
4
+ - filtering happens at the ROUTING layer: an excluded tool is invisible in
5
+ tools/list AND uncallable (-32601), never decoratively hidden;
6
+ - an unknown preset fails CLOSED (refuses to start) — this gateway can sit
7
+ behind a tunnel reachable from a consumer chat host, and a typo must not
8
+ open the full ~90-tool surface;
9
+ - unset env is byte-identical to today (pinned by test_gateway.py's exact
10
+ status assertion plus the unfiltered case here);
11
+ - the narrowing is disclosed via firekeep_gateway_status;
12
+ - a preset that narrows the tools narrows the handshake text with it, and
13
+ the chat instructions may only name tools the chat preset serves.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import re
18
+
19
+ import pytest
20
+
21
+ from firekeep_client.adapters.base import (
22
+ CHAT_INSTRUCTIONS,
23
+ CHAT_INSTRUCTIONS_HASH,
24
+ GATEWAY_INSTRUCTIONS_HASH,
25
+ )
26
+ from firekeep_client.gateway import STATUS_TOOL, TOOLSET_PRESETS, Gateway, _active_toolset
27
+
28
+
29
+ class FakeBackend:
30
+ def __init__(self, name, tools):
31
+ self.name = name
32
+ self.tools = tools
33
+ self.state = "not checked"
34
+
35
+ def discover(self, protocol_version):
36
+ self.state = f"ready ({len(self.tools)} tools)"
37
+
38
+ def request(self, method, params, **kwargs):
39
+ return {"jsonrpc": "2.0", "id": "up", "result": {"content": []}}
40
+
41
+ def close(self):
42
+ pass
43
+
44
+
45
+ def _tool(name):
46
+ return {"name": name, "inputSchema": {}}
47
+
48
+
49
+ def _gateway(monkeypatch, **env):
50
+ for key in ("FIREKEEP_TOOLSET", "FIREKEEP_TOOLS_ALLOW"):
51
+ monkeypatch.delenv(key, raising=False)
52
+ for key, value in env.items():
53
+ monkeypatch.setenv(key, value)
54
+ gateway = Gateway()
55
+ gateway.backends = [
56
+ FakeBackend("cortex", [_tool("memory_recall"), _tool("memory_learn"),
57
+ _tool("vault_retrieve"), _tool("corpus_ingest")]),
58
+ FakeBackend("bridge", [_tool("ctx_start_session"), _tool("ctx_update")]),
59
+ FakeBackend("relay", [_tool("relay_broadcast")]),
60
+ ]
61
+ return gateway
62
+
63
+
64
+ def test_chat_preset_filters_list_and_routes(monkeypatch):
65
+ gateway = _gateway(monkeypatch, FIREKEEP_TOOLSET="chat")
66
+ reply = gateway.handle({"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
67
+ names = {tool["name"] for tool in reply["result"]["tools"]}
68
+ assert names == {STATUS_TOOL["name"], "memory_recall", "memory_learn",
69
+ "ctx_start_session", "ctx_update"}
70
+ # Routing-layer enforcement: the excluded tool is UNCALLABLE, and the
71
+ # handler's re-discover retry must not resurrect it.
72
+ call = gateway.handle({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
73
+ "params": {"name": "vault_retrieve", "arguments": {}}})
74
+ assert call["error"]["code"] == -32601
75
+
76
+
77
+ def test_allowlist_overrides_preset(monkeypatch):
78
+ gateway = _gateway(monkeypatch, FIREKEEP_TOOLSET="chat",
79
+ FIREKEEP_TOOLS_ALLOW="vault_retrieve, relay_broadcast")
80
+ reply = gateway.handle({"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
81
+ names = {tool["name"] for tool in reply["result"]["tools"]}
82
+ assert names == {STATUS_TOOL["name"], "vault_retrieve", "relay_broadcast"}
83
+
84
+
85
+ def test_unset_env_serves_everything(monkeypatch):
86
+ gateway = _gateway(monkeypatch)
87
+ reply = gateway.handle({"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
88
+ assert len(reply["result"]["tools"]) == 8 # 7 fakes + status tool
89
+ assert gateway.status()["toolset"] is None
90
+ assert gateway.status()["tools_filtered"] == 0
91
+
92
+
93
+ def test_unknown_preset_fails_closed(monkeypatch):
94
+ monkeypatch.delenv("FIREKEEP_TOOLS_ALLOW", raising=False)
95
+ monkeypatch.setenv("FIREKEEP_TOOLSET", "chta")
96
+ with pytest.raises(SystemExit) as exc:
97
+ _active_toolset()
98
+ assert "chta" in str(exc.value)
99
+ assert "refusing" in str(exc.value)
100
+
101
+
102
+ def test_status_discloses_narrowing(monkeypatch):
103
+ gateway = _gateway(monkeypatch, FIREKEEP_TOOLSET="chat")
104
+ gateway.discover()
105
+ status = gateway.status()
106
+ assert status["toolset"] == "chat"
107
+ assert status["tools_filtered"] == 3 # vault_retrieve, corpus_ingest, relay_broadcast
108
+ assert status["tool_count"] == 4
109
+
110
+
111
+ def test_status_tool_is_never_filtered(monkeypatch):
112
+ gateway = _gateway(monkeypatch, FIREKEEP_TOOLS_ALLOW="memory_recall")
113
+ reply = gateway.handle({"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
114
+ assert STATUS_TOOL["name"] in {tool["name"] for tool in reply["result"]["tools"]}
115
+
116
+
117
+ def test_chat_preset_swaps_handshake_text_and_hash(monkeypatch):
118
+ gateway = _gateway(monkeypatch, FIREKEEP_TOOLSET="chat")
119
+ reply = gateway.handle({"jsonrpc": "2.0", "id": 1, "method": "initialize",
120
+ "params": {"protocolVersion": "2025-03-26"}})
121
+ result = reply["result"]
122
+ assert result["instructions"] == CHAT_INSTRUCTIONS
123
+ assert result["serverInfo"]["version"] == CHAT_INSTRUCTIONS_HASH
124
+ assert "decision_board" not in result["instructions"]
125
+ assert "vault" not in result["instructions"]
126
+
127
+
128
+ def test_allowlist_keeps_default_handshake(monkeypatch):
129
+ # The operator overrode the preset; the default text stays and they own
130
+ # any tool/instruction mismatch.
131
+ gateway = _gateway(monkeypatch, FIREKEEP_TOOLS_ALLOW="memory_recall")
132
+ reply = gateway.handle({"jsonrpc": "2.0", "id": 1, "method": "initialize",
133
+ "params": {"protocolVersion": "2025-03-26"}})
134
+ assert reply["result"]["serverInfo"]["version"] == GATEWAY_INSTRUCTIONS_HASH
135
+
136
+
137
+ def test_chat_instructions_only_name_preset_tools():
138
+ """Mechanical honesty: every tool-shaped name the chat handshake mentions
139
+ must be a tool the chat preset actually serves — an instruction to call a
140
+ tool that errors is worse than no instruction."""
141
+ mentioned = set(re.findall(r"\b[a-z]+_[a-z_]+\b", CHAT_INSTRUCTIONS))
142
+ non_tools = {"memory_ids"} # a parameter name, not a tool
143
+ assert mentioned - non_tools <= TOOLSET_PRESETS["chat"]
144
+
145
+
146
+ def test_chat_preset_is_exactly_the_decided_surface():
147
+ assert TOOLSET_PRESETS["chat"] == {
148
+ "memory_recall", "memory_learn", "memory_feedback",
149
+ "skill_recall", "skill_list",
150
+ "ctx_start_session", "ctx_update", "ctx_complete_session",
151
+ "ctx_abandon_session", "ctx_list_sessions", "ctx_resume_session",
152
+ "ctx_get_shadow",
153
+ }
@@ -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.3.0"
10
+ assert firekeep_client.__version__ == "1.4.2"
11
11
 
12
12
 
13
13
  def test_frozen_module_layout_is_present():
File without changes
File without changes