firekeep-client 1.8.2__tar.gz → 1.8.3__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.
- {firekeep_client-1.8.2/firekeep_client.egg-info → firekeep_client-1.8.3}/PKG-INFO +1 -1
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/__init__.py +1 -1
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/join.py +83 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3/firekeep_client.egg-info}/PKG-INFO +1 -1
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/pyproject.toml +1 -1
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_join.py +103 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_package.py +1 -1
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/LICENSE +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/NOTICE +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/README.md +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/adapters/__init__.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/adapters/base.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/adapters/claude.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/adapters/claude_desktop.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/adapters/codex.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/adapters/generic.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/adapters/kiro.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/adapters/opencode.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/adapters/pi.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/autoupdate.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/background.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/backups.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/childenv.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/cli.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/config_write.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/connect.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/contract/__init__.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/contract/matrix.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/decision/__init__.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/decision/board.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/decision/server.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/dexes.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/docdexsync.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/gateway.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooklog.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/__init__.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/__main__.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/_git.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/_mcp.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/destructive.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/post_tool.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/pre_tool.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/precompact.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/prompt.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/runbooks.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/session_end.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/session_start.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/hooks/stop.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/jobobject.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/joincode.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/maildexsync.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/migrate.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/nightshift.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/nightshiftdrain.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/pathenv.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/promptrecall.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/report.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/resolver.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/serverinit.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/serverupdate.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/shim.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/sidecar.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/signing.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/state.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/stdio.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/symdexindex.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/transport.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/updater.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/wizard.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client/worktree_snapshot.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client.egg-info/SOURCES.txt +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client.egg-info/dependency_links.txt +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client.egg-info/entry_points.txt +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client.egg-info/requires.txt +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client.egg-info/top_level.txt +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/setup.cfg +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_attribution_headers.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_autoupdate.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_background.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_bash_layer_retired.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_bootstrap_consent_pty.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_bootstrap_docdex.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_bootstrap_maildex.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_bootstrap_ps1.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_bootstrap_reinstall.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_bootstrap_sh.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_bootstrap_symdex.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_bootstrap_venv_provisioning.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_backup.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_dex.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_docdex.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_doctor.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_doctor_generic.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_doctor_report.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_entry.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_generic_install.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_hands.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_install.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_maildex.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_onboarding.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_personal.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_uninstall.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_uninstall_generic.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_update.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_cli_whoami.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_config_migration.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_connect.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_decision_board_html.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_decision_bypass.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_decision_poll_ceiling.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_decision_registration.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_decision_server.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_dexes.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_docdexsync.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_docs_reference_client_kit.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_doctor_credential_expiry.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_doctor_embeddings.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_doctor_no_server.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_e2e_bootstrap.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_gateway.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_gateway_child_env.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_gateway_schema_slimming.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_gateway_toolset.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_generic_config.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_hooklog.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_import_boundary.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_init_self_enroll.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_init_stale_bundle.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_install_skill_contract.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_instruction_budget.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_jobobject.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_join_config_write.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_joincode.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_kit_smoke.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_maildexsync.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_make_release.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_memory_instructions.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_migration_entrypoints.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_nightshift.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_nightshiftdrain.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_pathenv.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_promptrecall.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_report_bootstrap_enums.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_report_capture.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_report_consent.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_report_consent_surfaces.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_report_flush_points.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_report_integration.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_report_milestones.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_report_schema.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_report_spool.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_resolver.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_resolver_bypass.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_resolver_single_connection.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_runbook_match_parity.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_runbook_state.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_scratch_ttl.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_serverinit.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_serverupdate.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_shim_bridge.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_shim_bypass.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_shim_eof_exit.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_shim_fail_loud.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_shim_identity.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_shim_recovery.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_shim_skeleton.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_sidecar.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_sidecar_robustness.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_signing.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_state.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_state_session_report.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_stdio_encoding.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_symdexindex.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_transport.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_transport_sse.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_twin_removed.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_updater.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_wizard.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_wizard_generic.py +0 -0
- {firekeep_client-1.8.2 → firekeep_client-1.8.3}/tests/test_worktree_snapshot.py +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
|
+
import configparser
|
|
6
7
|
import hashlib
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
@@ -91,6 +92,19 @@ def _new_pending(code: JoinCode) -> dict:
|
|
|
91
92
|
def _prepare_pending(code: JoinCode, *, resume: bool, config_path: Path) -> dict:
|
|
92
93
|
path = pending_path(config_path)
|
|
93
94
|
sweep_pending(config_path)
|
|
95
|
+
if not resume and path.exists():
|
|
96
|
+
# Re-running the SAME code after a crash or a lost response is the
|
|
97
|
+
# documented shape of "it failed, run it again" -- and reusing the
|
|
98
|
+
# pending secret is exactly the safe thing to do: the server answers
|
|
99
|
+
# `replay` if that secret was already registered, and spends nothing
|
|
100
|
+
# twice. Only a pending enrollment for a DIFFERENT code still refuses.
|
|
101
|
+
try:
|
|
102
|
+
existing = json.loads(path.read_text(encoding="utf-8"))
|
|
103
|
+
except (OSError, json.JSONDecodeError):
|
|
104
|
+
existing = {}
|
|
105
|
+
if existing.get("ticket_id") == code.tid:
|
|
106
|
+
print("resuming the pending enrollment for this join code")
|
|
107
|
+
resume = True
|
|
94
108
|
if resume:
|
|
95
109
|
try:
|
|
96
110
|
pending = json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -230,6 +244,10 @@ def _server_values(
|
|
|
230
244
|
expires = response.get("credential_expires_at")
|
|
231
245
|
if expires:
|
|
232
246
|
server["credential_expires_at"] = str(expires)
|
|
247
|
+
# Which code produced this credential. Lets a re-run of the same install
|
|
248
|
+
# command recognise "already joined" locally instead of spending a second
|
|
249
|
+
# attempt on a single-use ticket and failing with "already redeemed".
|
|
250
|
+
server["join_ticket"] = code.tid
|
|
233
251
|
if response["kind"] == "ports":
|
|
234
252
|
server["host"] = "127.0.0.1" if code.transport == "tunnel" else str(response["host"])
|
|
235
253
|
else:
|
|
@@ -239,6 +257,46 @@ def _server_values(
|
|
|
239
257
|
return server
|
|
240
258
|
|
|
241
259
|
|
|
260
|
+
_USED_CREDENTIAL = re.compile(r"issuing credential ([0-9a-f]{16})")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _joined_credential(config_path: Path) -> tuple[str, str]:
|
|
264
|
+
"""(credential_id, join_ticket) of the connection this machine already holds.
|
|
265
|
+
|
|
266
|
+
Empty strings when there is no usable connection (no api_key). A RAW read,
|
|
267
|
+
never resolver.load_config(): that migrates and rewrites the file, and a
|
|
268
|
+
join that ends up refused must leave the config exactly as it found it."""
|
|
269
|
+
cfg = configparser.ConfigParser(interpolation=None)
|
|
270
|
+
try:
|
|
271
|
+
cfg.read_string(config_path.read_text(encoding="utf-8"))
|
|
272
|
+
except (OSError, UnicodeDecodeError, configparser.Error):
|
|
273
|
+
return "", ""
|
|
274
|
+
if not cfg.get("server", "api_key", fallback="").strip():
|
|
275
|
+
return "", ""
|
|
276
|
+
return (
|
|
277
|
+
cfg.get("server", "credential_id", fallback="").strip(),
|
|
278
|
+
cfg.get("server", "join_ticket", fallback="").strip(),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _already_joined(config_path: Path, credential_id: str) -> int:
|
|
283
|
+
"""Report an install re-run against a code this machine already redeemed."""
|
|
284
|
+
try:
|
|
285
|
+
pending_path(config_path).unlink()
|
|
286
|
+
except OSError:
|
|
287
|
+
pass
|
|
288
|
+
print(
|
|
289
|
+
f"this machine already joined with this code (credential {credential_id}); "
|
|
290
|
+
"nothing to do."
|
|
291
|
+
)
|
|
292
|
+
from firekeep_client.cli import run_doctor
|
|
293
|
+
|
|
294
|
+
rows = run_doctor()
|
|
295
|
+
for name, status, detail in rows:
|
|
296
|
+
print(f"[{status.upper()}] {name}: {detail}")
|
|
297
|
+
return 1 if any(status == "fail" for _, status, _ in rows) else 0
|
|
298
|
+
|
|
299
|
+
|
|
242
300
|
def join(
|
|
243
301
|
pasted_code: str,
|
|
244
302
|
*,
|
|
@@ -260,6 +318,14 @@ def join(
|
|
|
260
318
|
raise JoinError(str(exc), exit_code=2) from exc
|
|
261
319
|
|
|
262
320
|
config_path = resolver._config_path()
|
|
321
|
+
joined_credential, joined_ticket = _joined_credential(config_path)
|
|
322
|
+
if (
|
|
323
|
+
code.purpose == "device"
|
|
324
|
+
and not force
|
|
325
|
+
and joined_credential
|
|
326
|
+
and joined_ticket == code.tid
|
|
327
|
+
):
|
|
328
|
+
return _already_joined(config_path, joined_credential)
|
|
263
329
|
pending = (
|
|
264
330
|
_prepare_pending(code, resume=resume, config_path=config_path)
|
|
265
331
|
if code.purpose == "device"
|
|
@@ -333,6 +399,23 @@ def join(
|
|
|
333
399
|
pending = _new_pending(code)
|
|
334
400
|
_write_private_json(pending_path(config_path), pending)
|
|
335
401
|
continue
|
|
402
|
+
# A config written before `join_ticket` existed cannot be matched
|
|
403
|
+
# locally; the server's "already redeemed" names the credential the
|
|
404
|
+
# code issued, and if that is the one this machine holds, the
|
|
405
|
+
# earlier run succeeded -- this is a re-run, not a stolen code.
|
|
406
|
+
used = _USED_CREDENTIAL.search(str(exc)) if exc.status == 409 else None
|
|
407
|
+
if used and joined_credential and used.group(1) == joined_credential:
|
|
408
|
+
return _already_joined(config_path, joined_credential)
|
|
409
|
+
# A JSON 4xx is a definitive refusal: the enrollment script registers
|
|
410
|
+
# the credential only on success, so the pending secret was never
|
|
411
|
+
# stored and keeping it would only block the next join for 24h.
|
|
412
|
+
# Transport failures and 5xx keep it -- the response may have been
|
|
413
|
+
# lost after the server registered it, which is what --resume is for.
|
|
414
|
+
if exc.status is not None and 400 <= exc.status < 500 and exc.response_is_json:
|
|
415
|
+
try:
|
|
416
|
+
pending_path(config_path).unlink()
|
|
417
|
+
except OSError:
|
|
418
|
+
pass
|
|
336
419
|
if exc.status == 404 and not exc.response_is_json:
|
|
337
420
|
raise JoinError(
|
|
338
421
|
"this server predates client enrollment (no POST /enroll). On "
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "firekeep-client"
|
|
7
|
-
version = "1.8.
|
|
7
|
+
version = "1.8.3"
|
|
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"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
5
6
|
import stat
|
|
@@ -189,6 +190,108 @@ def test_enrollment_error_exit_codes(
|
|
|
189
190
|
assert "predates client enrollment" in str(error.value)
|
|
190
191
|
|
|
191
192
|
|
|
193
|
+
def _throw(exc):
|
|
194
|
+
def raiser(*a, **k):
|
|
195
|
+
raise exc
|
|
196
|
+
return raiser
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_rerun_of_the_same_code_is_a_local_no_op(home, monkeypatch, capsys):
|
|
200
|
+
"""The install one-liner re-run on a machine it already joined must succeed
|
|
201
|
+
without spending a second attempt on the single-use ticket."""
|
|
202
|
+
monkeypatch.setattr(J, "get_json", lambda *a, **k: {"ok": True})
|
|
203
|
+
monkeypatch.setattr(J, "post_json", lambda *a, **k: response())
|
|
204
|
+
code = encode(payload())
|
|
205
|
+
assert J.join(code) == 0
|
|
206
|
+
assert "join_ticket" in home.read_text(encoding="utf-8")
|
|
207
|
+
before = home.read_text(encoding="utf-8")
|
|
208
|
+
|
|
209
|
+
posts = []
|
|
210
|
+
monkeypatch.setattr(J, "get_json", lambda *a, **k: posts.append(a))
|
|
211
|
+
monkeypatch.setattr(J, "post_json", lambda *a, **k: posts.append(a))
|
|
212
|
+
capsys.readouterr()
|
|
213
|
+
assert J.join(code) == 0
|
|
214
|
+
assert posts == []
|
|
215
|
+
assert "already joined with this code" in capsys.readouterr().out
|
|
216
|
+
assert home.read_text(encoding="utf-8") == before
|
|
217
|
+
assert not J.pending_path(home).exists()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_rerun_against_a_legacy_config_recognises_its_own_credential(home, monkeypatch, capsys):
|
|
221
|
+
"""A config written before join_ticket existed: the server's 'already
|
|
222
|
+
redeemed' names the credential, and it is the one this machine holds."""
|
|
223
|
+
home.parent.mkdir(parents=True)
|
|
224
|
+
home.write_text(
|
|
225
|
+
"[identity]\nagent_id = bob\n\n[server]\nkind = ports\nhost = firekeep.example\n"
|
|
226
|
+
f"api_key = nxs_{'a' * 64}\ncredential_id = {'1' * 16}\n",
|
|
227
|
+
encoding="utf-8",
|
|
228
|
+
)
|
|
229
|
+
monkeypatch.setattr(J, "get_json", lambda *a, **k: {"ok": True})
|
|
230
|
+
monkeypatch.setattr(J, "post_json", _throw(TransportError(
|
|
231
|
+
'409 {"detail":"this join code was already redeemed at 2026-11-09T00:09:30 by a '
|
|
232
|
+
f'different device, issuing credential {"1" * 16}. Join codes are single-use."}}',
|
|
233
|
+
status=409, response_is_json=True,
|
|
234
|
+
)))
|
|
235
|
+
assert J.join(encode(payload())) == 0
|
|
236
|
+
assert "already joined with this code" in capsys.readouterr().out
|
|
237
|
+
assert not J.pending_path(home).exists()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_code_redeemed_by_another_credential_still_fails_and_cleans_up(home, monkeypatch):
|
|
241
|
+
home.parent.mkdir(parents=True)
|
|
242
|
+
home.write_text(
|
|
243
|
+
f"[server]\nkind = ports\napi_key = nxs_{'a' * 64}\ncredential_id = {'9' * 16}\n",
|
|
244
|
+
encoding="utf-8",
|
|
245
|
+
)
|
|
246
|
+
monkeypatch.setattr(J, "get_json", lambda *a, **k: {"ok": True})
|
|
247
|
+
monkeypatch.setattr(J, "post_json", _throw(TransportError(
|
|
248
|
+
f'409 {{"detail":"already redeemed ... issuing credential {"1" * 16}."}}',
|
|
249
|
+
status=409, response_is_json=True,
|
|
250
|
+
)))
|
|
251
|
+
with pytest.raises(J.JoinError, match="already redeemed") as error:
|
|
252
|
+
J.join(encode(payload()))
|
|
253
|
+
assert error.value.exit_code == 3
|
|
254
|
+
# Refused outright: the secret was never registered, so nothing to resume.
|
|
255
|
+
assert not J.pending_path(home).exists()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_transport_failure_keeps_pending_and_a_plain_rerun_resumes_it(home, monkeypatch, capsys):
|
|
259
|
+
"""A lost response may mean the server DID register the secret, so the
|
|
260
|
+
pending file stays -- and re-running the same command (no --resume) reuses
|
|
261
|
+
it instead of refusing with 'a pending enrollment already exists'."""
|
|
262
|
+
monkeypatch.setattr(J, "get_json", lambda *a, **k: {"ok": True})
|
|
263
|
+
hashes = []
|
|
264
|
+
|
|
265
|
+
def lost(url, body, **kwargs):
|
|
266
|
+
hashes.append(body["credential_hash"])
|
|
267
|
+
raise TransportError("response lost")
|
|
268
|
+
|
|
269
|
+
monkeypatch.setattr(J, "post_json", lost)
|
|
270
|
+
with pytest.raises(J.JoinError):
|
|
271
|
+
J.join(encode(payload()))
|
|
272
|
+
assert J.pending_path(home).exists()
|
|
273
|
+
|
|
274
|
+
def replay(url, body, **kwargs):
|
|
275
|
+
hashes.append(body["credential_hash"])
|
|
276
|
+
return {**response(), "replay": True}
|
|
277
|
+
|
|
278
|
+
monkeypatch.setattr(J, "post_json", replay)
|
|
279
|
+
assert J.join(encode(payload())) == 0
|
|
280
|
+
assert hashes[0] == hashes[1]
|
|
281
|
+
assert "resuming the pending enrollment" in capsys.readouterr().out
|
|
282
|
+
assert not J.pending_path(home).exists()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_pending_for_a_different_code_still_refuses(home, monkeypatch):
|
|
286
|
+
monkeypatch.setattr(J, "get_json", lambda *a, **k: {"ok": True})
|
|
287
|
+
monkeypatch.setattr(J, "post_json", _throw(TransportError("response lost")))
|
|
288
|
+
with pytest.raises(J.JoinError):
|
|
289
|
+
J.join(encode(payload()))
|
|
290
|
+
other = encode(payload(q=base64.urlsafe_b64encode(bytes(range(1, 33))).decode().rstrip("=")))
|
|
291
|
+
with pytest.raises(J.JoinError, match="pending enrollment already exists"):
|
|
292
|
+
J.join(other)
|
|
293
|
+
|
|
294
|
+
|
|
192
295
|
def test_tunnel_setup_failure_is_actionable_exit_7(home, monkeypatch):
|
|
193
296
|
from firekeep_client import connect
|
|
194
297
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{firekeep_client-1.8.2 → firekeep_client-1.8.3}/firekeep_client.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|