claude-multiacc 2.0.58 → 2.0.60

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. package/README.md +125 -4
  2. package/bin/claude +389 -94
  3. package/bin/claude-accounts +276 -42
  4. package/bin/codex +69 -5
  5. package/bin/codex-accounts +33 -4
  6. package/docs/ACCOUNT_OPERATIONS.md +62 -2
  7. package/docs/AUTORESUME-HANDOFF.md +28 -3
  8. package/docs/AUTORESUME.md +275 -18
  9. package/docs/CODEX.md +1 -0
  10. package/docs/INSTALLATION.md +7 -1
  11. package/docs/VERIFICATION.md +4 -0
  12. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  13. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  14. package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
  15. package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
  16. package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
  17. package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
  18. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  19. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  20. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  21. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  22. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  23. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  24. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  25. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  26. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  27. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  28. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  29. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  30. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  31. package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
  32. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  33. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  34. package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
  35. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  36. package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
  37. package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
  38. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  39. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  40. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  41. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  42. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  43. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  44. package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
  45. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  46. package/lib/audit.py +88 -22
  47. package/lib/autoresume.py +1197 -82
  48. package/lib/autoresume_cross.py +3 -1
  49. package/lib/autoresume_handoff.py +81 -9
  50. package/lib/autoresume_probe.py +2 -2
  51. package/lib/claude_client_error.py +9 -4
  52. package/lib/codex_trust.py +263 -0
  53. package/lib/common.sh +146 -3
  54. package/lib/credential.py +5 -2
  55. package/lib/install_actions.sh +19 -0
  56. package/lib/keychain.py +180 -36
  57. package/lib/mcp_oauth.py +1748 -0
  58. package/lib/mcp_registry.py +230 -8
  59. package/lib/model_catalog.json +404 -0
  60. package/lib/model_catalog.py +201 -0
  61. package/lib/model_match.py +104 -0
  62. package/lib/report.py +8 -5
  63. package/lib/session_settings.py +123 -0
  64. package/package.json +2 -2
  65. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
  66. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
  67. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
  68. package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
  69. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  70. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  93. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  94. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  95. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
  96. package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
  97. package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
  98. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
  99. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  100. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  101. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
  102. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  103. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  104. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  105. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  106. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  107. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  108. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  109. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  110. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  111. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  112. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  113. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  114. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  115. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  116. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  117. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  118. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  119. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  120. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  121. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
  122. package/tests/fake_codex_tui.py +25 -0
  123. package/tests/fake_security.sh +24 -2
  124. package/tests/fixtures/cross_provider_cli.py +109 -0
  125. package/tests/run-tests.sh +1171 -23
  126. package/tests/test_autoresume.py +215 -8
  127. package/tests/test_autoresume_catalog.py +226 -0
  128. package/tests/test_autoresume_catalog_cli.py +185 -0
  129. package/tests/test_autoresume_codex_records.py +16 -0
  130. package/tests/test_autoresume_codex_trust.py +247 -0
  131. package/tests/test_autoresume_cross_guards.py +28 -0
  132. package/tests/test_autoresume_cross_tmux.py +27 -2
  133. package/tests/test_autoresume_cross_tokens.py +32 -2
  134. package/tests/test_autoresume_handoff_argv.py +120 -16
  135. package/tests/test_autoresume_handoff_origins.py +14 -0
  136. package/tests/test_autoresume_memory.py +1141 -0
  137. package/tests/test_autoresume_memory_tmux.py +94 -0
  138. package/tests/test_autoresume_probe.py +2 -1
  139. package/tests/test_autoresume_relaunch_settings.py +317 -0
  140. package/tests/test_autoresume_settings.py +135 -0
  141. package/tests/test_autoresume_sidechain.py +1518 -0
  142. package/tests/test_autoresume_sidechain_tmux.py +79 -0
  143. package/tests/test_autoresume_trust_tmux.py +63 -0
  144. package/tests/test_claude_client_error.py +10 -7
  145. package/tests/test_claude_org_block.py +1 -0
  146. package/tests/test_claude_pool_prepare.py +4 -1
  147. package/tests/test_claude_reset.py +3 -0
  148. package/tests/test_keychain_nologin.py +340 -0
  149. package/tests/test_mcp_oauth.py +1414 -0
  150. package/tests/test_mcp_registry.py +234 -3
  151. package/tests/test_packaged_commands.py +41 -0
@@ -33,8 +33,12 @@ Reconcile rules, the same for both providers:
33
33
  any more" needs to hold fleet-wide; a name in `retiredUser` (a learned stock
34
34
  `claude mcp remove -s user`, or `mcp remove --user-only`) leaves project
35
35
  entries alone, exactly as the client itself did;
36
- * anything else the account holds is left alone (an account may carry its own
37
- extras);
36
+ * a server an account holds that no section names, live or retired, is ADOPTED
37
+ into the registry before the reconcile (`adopt`), and so reaches every account:
38
+ some route around the shims' mirror put it there (2026-10-22: the operator's
39
+ `alias claude='claude --dangerously-skip-permissions'` hid every typed
40
+ `claude mcp add` from it, and motionsites sat in 2 of 11 accounts). Kill switch
41
+ `<PROVIDER>_MULTIACC_MCP_ADOPT=0`;
38
42
  * project-scoped entries (`projects` in the registry) exist for claude only:
39
43
  Codex 0.156 reads no per-project config at all.
40
44
 
@@ -73,6 +77,12 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
73
77
  REGISTRY_FILE = "mcp-servers.json"
74
78
  OVERLAY_FILE = "mcp-servers.local.json"
75
79
  STAMP_FILE = ".mcp-applied"
80
+ #: Every name the pool itself ever wrote into the account (from any section): adoption
81
+ #: never takes one of those back, so a server dropped from a section without a tombstone
82
+ #: (README's recovery for a stray synced entry) or one whose overlay owner vanished is a
83
+ #: stale copy, not a new server — app-robot's machine-specific appinspire-mcp must never
84
+ #: reach the synced registry that way.
85
+ MANAGED_FILE = ".mcp-managed.json"
76
86
  LOCK_DIR = os.path.join(".locks", "mcp")
77
87
  PROVIDERS = ("claude", "codex")
78
88
  CONFIG_FILE = {"claude": ".claude.json", "codex": "config.toml"}
@@ -168,13 +178,14 @@ def _log(root, message):
168
178
  class _Lock:
169
179
  """mkdir lock, the same shape as lib/common.sh mutate_lock (30 s stale)."""
170
180
 
171
- def __init__(self, root):
181
+ def __init__(self, root, wait=60):
172
182
  self.path = os.path.join(root, LOCK_DIR)
173
183
  self.held = False
184
+ self.wait = wait
174
185
 
175
186
  def __enter__(self):
176
187
  os.makedirs(os.path.dirname(self.path), exist_ok=True)
177
- deadline = time.time() + 60
188
+ deadline = time.time() + self.wait
178
189
  while True:
179
190
  try:
180
191
  os.mkdir(self.path)
@@ -864,18 +875,215 @@ def snapshot_codex(account_dir):
864
875
 
865
876
  # ---------------------------------------------------------------- reconcile
866
877
 
878
+ def load_managed(account_dir):
879
+ """{"user": set(names), "projects": {path: set(names)}} — the account's ledger."""
880
+ doc = _read_json(os.path.join(account_dir, MANAGED_FILE), {})
881
+ doc = doc if isinstance(doc, dict) else {}
882
+ user = doc.get("user") if isinstance(doc.get("user"), list) else []
883
+ projects = doc.get("projects") if isinstance(doc.get("projects"), dict) else {}
884
+ return {"user": {str(n) for n in user},
885
+ "projects": {str(p): {str(n) for n in names} for p, names in projects.items()
886
+ if isinstance(names, list)}}
887
+
888
+
889
+ def record_managed(provider, account_dir, eff):
890
+ """Add what this reconcile wrote to the ledger (it only ever grows)."""
891
+ managed = load_managed(account_dir)
892
+ user = managed["user"] | set(eff.servers)
893
+ projects = {path: set(names) for path, names in managed["projects"].items()}
894
+ if provider == "claude":
895
+ for path, spec in eff.projects.items():
896
+ names = set(spec.get("mcpServers") or {})
897
+ if names:
898
+ projects[path] = projects.get(path, set()) | names
899
+ if user == managed["user"] and projects == managed["projects"]:
900
+ return
901
+ try:
902
+ _atomic_write(os.path.join(account_dir, MANAGED_FILE), json.dumps(
903
+ {"user": sorted(user), "projects": {p: sorted(n) for p, n in sorted(projects.items())}},
904
+ indent=2) + "\n")
905
+ except OSError:
906
+ pass
907
+
908
+
867
909
  def apply_account(root, provider, account_dir):
868
910
  eff = effective(load_registry(root), load_overlay(root))
869
911
  if provider == "claude":
870
912
  state = apply_claude(account_dir, eff)
871
913
  else:
872
914
  state = apply_codex(account_dir, eff)
915
+ record_managed(provider, account_dir, eff)
873
916
  write_stamp(root, provider, account_dir)
874
917
  return state
875
918
 
876
919
 
920
+ def _adopt_enabled(provider):
921
+ return os.environ.get(f"{provider.upper()}_MULTIACC_MCP_ADOPT", "1") != "0"
922
+
923
+
924
+ def _unknown(eff, snap, managed=None):
925
+ """What one account's snapshot holds that no section names, live or retired:
926
+ [(scope, path, name, block)] (path None for user scope). A project's `.mcp.json`
927
+ approvals are NOT adopted: one account's click would override another's rejection
928
+ (`disabledMcpjsonServers`) everywhere — only a mirrored `claude mcp` command carries one."""
929
+ managed = managed or {"user": set(), "projects": {}}
930
+ found = []
931
+ for name, block in (snap.get("user") or {}).items():
932
+ if not isinstance(block, dict):
933
+ continue
934
+ if name in eff.servers or name in eff.retired or name in eff.retired_user \
935
+ or name in managed["user"]:
936
+ continue
937
+ found.append(("user", None, name, block))
938
+ if snap.get("provider") != "claude":
939
+ return found
940
+ for path, entry in (snap.get("projects") or {}).items():
941
+ spec = eff.projects.get(path) or {}
942
+ known = set(spec.get("mcpServers") or {}) | set(spec.get("retired") or []) \
943
+ | eff.retired | managed["projects"].get(path, set())
944
+ for name, block in (entry.get("mcpServers") or {}).items():
945
+ if isinstance(block, dict) and name not in known:
946
+ found.append(("project", path, name, block))
947
+ return found
948
+
949
+
950
+ def _still_unknown(eff, scope, path, name):
951
+ if scope == "user":
952
+ return not (name in eff.servers or name in eff.retired or name in eff.retired_user)
953
+ spec = eff.projects.get(path) or {}
954
+ return not (name in (spec.get("mcpServers") or {}) or name in (spec.get("retired") or [])
955
+ or name in eff.retired)
956
+
957
+
958
+ def _config_mtime(provider, account_dir):
959
+ try:
960
+ return os.stat(os.path.join(account_dir, CONFIG_FILE[provider])).st_mtime
961
+ except OSError:
962
+ return 0.0
963
+
964
+
965
+ def _codex_faithful(name, table):
966
+ """Whether the registry's codex translation gives this table back as it is (bar the
967
+ startup timeout default it adds to a stdio server): only then is a codex table adopted,
968
+ because the reconcile then rewrites the user's own table in that translation."""
969
+ try:
970
+ again, _notes = codex_block(name, codex_to_claude(name, table))
971
+ except RegistryError:
972
+ return False
973
+ if again is None:
974
+ return False
975
+ again = dict(again)
976
+ if "startup_timeout_sec" not in table and again.get("startup_timeout_sec") == 60:
977
+ again.pop("startup_timeout_sec")
978
+ if not table.get("args") and again.get("args") == []:
979
+ again.pop("args", None)
980
+ return again == {k: v for k, v in table.items()}
981
+
982
+
983
+ def _carries_values(block):
984
+ """A block whose env or headers hold literal values — a token, an API key — stays on
985
+ this machine: the synced registry is a plain file pushed to every peer."""
986
+ for key in ("env", "headers"):
987
+ values = block.get(key)
988
+ if isinstance(values, dict) and any(str(v) and not str(v).startswith("${")
989
+ for v in values.values()):
990
+ return True
991
+ return False
992
+
993
+
994
+ def adopt(root, provider, account_dirs, dry=False, lock_wait=60):
995
+ """Take every server these accounts hold that no section names into the registry
996
+ (on a replica: this machine's overlay, like any learned change). The mirror
997
+ (`learn`) sees only a `mcp add` the shim recognised; anything else — a global
998
+ option in front of `mcp` (the operator's aliases), the real binary run directly, a
999
+ hand edit, another tool's installer — left the server in the one account it ran
1000
+ under, for ever. Several accounts holding different definitions of one name: the
1001
+ one most of them hold wins, then the newest config file. Returns the change list
1002
+ (dry: the names it would adopt, nothing written)."""
1003
+ if not account_dirs or not _adopt_enabled(provider):
1004
+ return []
1005
+ eff = effective(load_registry(root), load_overlay(root))
1006
+ variants = {} # (scope, path, name) -> {canonical json: [count, newest mtime, block]}
1007
+ # Every account votes, whichever one this run reconciles: a single-account apply (the
1008
+ # shim's, before exec) must choose what `apply --all` would, never its own stale copy.
1009
+ voters = list(dict.fromkeys(list(account_dirs) + manifest_account_dirs(root)))
1010
+ for account_dir in voters:
1011
+ try:
1012
+ snap = snapshot(provider, account_dir)
1013
+ except RegistryError:
1014
+ continue # apply reports an unreadable config itself
1015
+ mtime = _config_mtime(provider, account_dir)
1016
+ for scope, path, name, block in _unknown(eff, snap, load_managed(account_dir)):
1017
+ slot = variants.setdefault((scope, path, name), {})
1018
+ key = json.dumps(block, sort_keys=True)
1019
+ count = slot.setdefault(key, [0, 0.0, block])
1020
+ count[0] += 1
1021
+ count[1] = max(count[1], mtime)
1022
+ if not variants or dry:
1023
+ return sorted(name for _scope, _path, name in variants) # dry: what would be adopted
1024
+ changes = []
1025
+ synced_changed = local_changed = False
1026
+ with _Lock(root, wait=lock_wait):
1027
+ target = _Target(root)
1028
+ local_overlay = target.overlay if target.replica else load_overlay(root)
1029
+ eff = effective(load_registry(root), load_overlay(root)) # what holds under the lock
1030
+ for (scope, path, name), slot in sorted(variants.items(), key=lambda kv: (
1031
+ kv[0][0], kv[0][1] or "", kv[0][2])):
1032
+ if not _still_unknown(eff, scope, path, name):
1033
+ continue # named meanwhile (a concurrent add, another machine's push)
1034
+ count, _mtime, block = max(slot.values(), key=lambda v: (v[0], v[1]))
1035
+ if provider == "codex" and not _codex_faithful(name, block):
1036
+ _warn(f'not adopted: "{name}" — the registry cannot express its codex table '
1037
+ f"as it is; register it with: codex-accounts mcp add")
1038
+ continue
1039
+ try:
1040
+ learned = block if provider == "claude" else codex_to_claude(name, block)
1041
+ validate_block(name, learned)
1042
+ except RegistryError as exc:
1043
+ _warn(f"not adopted: {exc}")
1044
+ continue
1045
+ where = f" (project {path})" if path else ""
1046
+ if _carries_values(learned) and not target.replica:
1047
+ local = local_overlay.setdefault("owners", {}).setdefault(
1048
+ "local", _normalize_section({}))
1049
+ _upsert(local, name, learned, scope, path)
1050
+ local_changed = True
1051
+ where += " (this machine only: it carries env/header values)"
1052
+ else:
1053
+ _upsert(target.section, name, learned, scope, path)
1054
+ synced_changed = True
1055
+ if len(slot) > 1:
1056
+ changes.append(f'"{name}"{where} (the definition {count} of '
1057
+ f'{sum(v[0] for v in slot.values())} accounts hold)')
1058
+ else:
1059
+ changes.append(f'"{name}"{where}')
1060
+ if synced_changed:
1061
+ target.save()
1062
+ if local_changed:
1063
+ save_overlay(root, local_overlay)
1064
+ if changes:
1065
+ _log(root, f"adopt {', '.join(changes)} into {target.where}")
1066
+ return changes
1067
+
1068
+
877
1069
  def apply_many(root, provider, account_dirs, quiet=False, fail_open=False):
878
- """Reconcile each dir; returns the number of accounts that could not be."""
1070
+ """Adopt what these accounts hold that no section names, then reconcile each dir;
1071
+ returns the number of accounts that could not be."""
1072
+ try:
1073
+ # a launch must not wait long on a lock left by a killed holder: the next launch
1074
+ # or the limits pass adopts instead
1075
+ adopted = adopt(root, provider, account_dirs, lock_wait=2 if fail_open else 60)
1076
+ except Exception as exc: # noqa: BLE001 — adoption must never stop a reconcile
1077
+ adopted = []
1078
+ _warn(f"adopt: {type(exc).__name__}: {exc}")
1079
+ if adopted and not quiet:
1080
+ print(f"adopted {', '.join(adopted)}: every {provider} account gets "
1081
+ f"{'it' if len(adopted) == 1 else 'them'}")
1082
+ if adopted and len(account_dirs) < len(manifest_account_dirs(root)):
1083
+ # One account told the registry something new: the others need it as well (the
1084
+ # shims would bring it to each on its next launch; an idle one would wait).
1085
+ # (a dir being seeded is not in the manifest yet: it stays in the list)
1086
+ account_dirs = list(dict.fromkeys(list(account_dirs) + manifest_account_dirs(root)))
879
1087
  failures = 0
880
1088
  for account_dir in account_dirs:
881
1089
  label = os.path.basename(account_dir.rstrip("/"))
@@ -1194,7 +1402,8 @@ def cmd_apply(args, leftovers):
1194
1402
  if args.all or not args.account_dir:
1195
1403
  rc = _apply_all(args, quiet=args.quiet)
1196
1404
  return 0 if args.fail_open else rc
1197
- failures = apply_many(args.root, args.provider, args.account_dir, quiet=args.quiet)
1405
+ failures = apply_many(args.root, args.provider, args.account_dir, quiet=args.quiet,
1406
+ fail_open=args.fail_open)
1198
1407
  return 0 if args.fail_open or not failures else 1
1199
1408
 
1200
1409
 
@@ -1251,8 +1460,12 @@ def cmd_import_local(args):
1251
1460
  # it: an owner that stops shipping a server means it, and every account
1252
1461
  # would otherwise keep the stale copy for ever.
1253
1462
  dropped = [name for name in previous["mcpServers"] if name not in servers]
1463
+ # ...and stays retired until it publishes the name again: an account still holding
1464
+ # a copy must never have it adopted into the synced registry (adopt) once the next
1465
+ # import has forgotten the drop.
1466
+ kept = [name for name in previous["retired"] if name not in servers]
1254
1467
  section = _normalize_section({"mcpServers": servers,
1255
- "retired": sorted(set(retired) | set(dropped))})
1468
+ "retired": sorted(set(retired) | set(dropped) | set(kept))})
1256
1469
  if previous == section and owner in overlay["owners"]:
1257
1470
  changed = False
1258
1471
  else:
@@ -1330,7 +1543,16 @@ def main(argv=None):
1330
1543
  from codex_python import require_python311
1331
1544
  require_python311(os.path.abspath(__file__))
1332
1545
  if args.mode == "apply" and not _registry_present(args.root):
1333
- return 0 # nothing registered: every account is already "reconciled"
1546
+ # Nothing registered: every account is already "reconciled" — unless one holds a
1547
+ # server to adopt (the first this pool ever had, added where the mirror never saw).
1548
+ dirs = args.account_dir if args.account_dir and not args.all \
1549
+ else manifest_account_dirs(args.root)
1550
+ try:
1551
+ if not adopt(args.root, args.provider, dirs, dry=True):
1552
+ return 0
1553
+ except Exception as exc: # noqa: BLE001 — the old no-op, never a failure
1554
+ _warn(f"adopt: {type(exc).__name__}: {exc}")
1555
+ return 0
1334
1556
  if args.mode == "list":
1335
1557
  return cmd_list(args)
1336
1558
  if args.mode == "add":
@@ -0,0 +1,404 @@
1
+ {
2
+ "claude": {
3
+ "efforts": [
4
+ "low",
5
+ "medium",
6
+ "high",
7
+ "xhigh",
8
+ "max"
9
+ ],
10
+ "families": {
11
+ "fable": 5,
12
+ "haiku": 2,
13
+ "mythos": 5,
14
+ "opus": 4,
15
+ "sonnet": 3
16
+ },
17
+ "fetched_at": "2026-10-14T08:49:41Z",
18
+ "models": {
19
+ "claude-fable-5": {
20
+ "default_effort": "high",
21
+ "efforts": [
22
+ "low",
23
+ "medium",
24
+ "high",
25
+ "xhigh",
26
+ "max"
27
+ ],
28
+ "family": "fable",
29
+ "listed": false,
30
+ "upgrade": null,
31
+ "version": [
32
+ 5
33
+ ]
34
+ },
35
+ "claude-fable-5-1": {
36
+ "default_effort": "high",
37
+ "efforts": [
38
+ "low",
39
+ "medium",
40
+ "high",
41
+ "xhigh",
42
+ "max"
43
+ ],
44
+ "family": "fable",
45
+ "listed": true,
46
+ "upgrade": null,
47
+ "version": [
48
+ 5,
49
+ 1
50
+ ]
51
+ },
52
+ "claude-haiku-4-5-20251001": {
53
+ "default_effort": null,
54
+ "efforts": [],
55
+ "family": "haiku",
56
+ "listed": false,
57
+ "upgrade": null,
58
+ "version": [
59
+ 4,
60
+ 5
61
+ ]
62
+ },
63
+ "claude-haiku-5-5": {
64
+ "default_effort": "medium",
65
+ "efforts": [
66
+ "low",
67
+ "medium",
68
+ "high",
69
+ "xhigh",
70
+ "max"
71
+ ],
72
+ "family": "haiku",
73
+ "listed": true,
74
+ "upgrade": null,
75
+ "version": [
76
+ 5,
77
+ 5
78
+ ]
79
+ },
80
+ "claude-opus-4-6": {
81
+ "default_effort": "high",
82
+ "efforts": [
83
+ "low",
84
+ "medium",
85
+ "high",
86
+ "max"
87
+ ],
88
+ "family": "opus",
89
+ "listed": false,
90
+ "upgrade": null,
91
+ "version": [
92
+ 4,
93
+ 6
94
+ ]
95
+ },
96
+ "claude-opus-4-7": {
97
+ "default_effort": "xhigh",
98
+ "efforts": [
99
+ "low",
100
+ "medium",
101
+ "high",
102
+ "xhigh",
103
+ "max"
104
+ ],
105
+ "family": "opus",
106
+ "listed": false,
107
+ "upgrade": null,
108
+ "version": [
109
+ 4,
110
+ 7
111
+ ]
112
+ },
113
+ "claude-opus-4-8": {
114
+ "default_effort": "high",
115
+ "efforts": [
116
+ "low",
117
+ "medium",
118
+ "high",
119
+ "xhigh",
120
+ "max"
121
+ ],
122
+ "family": "opus",
123
+ "listed": false,
124
+ "upgrade": null,
125
+ "version": [
126
+ 4,
127
+ 8
128
+ ]
129
+ },
130
+ "claude-opus-5": {
131
+ "default_effort": "high",
132
+ "efforts": [
133
+ "low",
134
+ "medium",
135
+ "high",
136
+ "xhigh",
137
+ "max"
138
+ ],
139
+ "family": "opus",
140
+ "listed": false,
141
+ "upgrade": null,
142
+ "version": [
143
+ 5
144
+ ]
145
+ },
146
+ "claude-opus-5-5": {
147
+ "default_effort": "medium",
148
+ "efforts": [
149
+ "low",
150
+ "medium",
151
+ "high",
152
+ "xhigh",
153
+ "max"
154
+ ],
155
+ "family": "opus",
156
+ "listed": true,
157
+ "upgrade": null,
158
+ "version": [
159
+ 5,
160
+ 5
161
+ ]
162
+ },
163
+ "claude-sonnet-4-6": {
164
+ "default_effort": "high",
165
+ "efforts": [
166
+ "low",
167
+ "medium",
168
+ "high",
169
+ "max"
170
+ ],
171
+ "family": "sonnet",
172
+ "listed": false,
173
+ "upgrade": null,
174
+ "version": [
175
+ 4,
176
+ 6
177
+ ]
178
+ },
179
+ "claude-sonnet-5": {
180
+ "default_effort": "high",
181
+ "efforts": [
182
+ "low",
183
+ "medium",
184
+ "high",
185
+ "xhigh",
186
+ "max"
187
+ ],
188
+ "family": "sonnet",
189
+ "listed": false,
190
+ "upgrade": null,
191
+ "version": [
192
+ 5
193
+ ]
194
+ },
195
+ "claude-sonnet-5-5": {
196
+ "default_effort": "medium",
197
+ "efforts": [
198
+ "low",
199
+ "medium",
200
+ "high",
201
+ "xhigh",
202
+ "max"
203
+ ],
204
+ "family": "sonnet",
205
+ "listed": true,
206
+ "upgrade": null,
207
+ "version": [
208
+ 5,
209
+ 5
210
+ ]
211
+ }
212
+ },
213
+ "source": "release"
214
+ },
215
+ "codex": {
216
+ "efforts": [
217
+ "none",
218
+ "minimal",
219
+ "low",
220
+ "medium",
221
+ "high",
222
+ "xhigh",
223
+ "max",
224
+ "ultra"
225
+ ],
226
+ "families": {
227
+ "astra": 4,
228
+ "luna": 1,
229
+ "sol": 3,
230
+ "terra": 2
231
+ },
232
+ "fetched_at": "2026-10-14T08:54:28Z",
233
+ "models": {
234
+ "codex-auto-review": {
235
+ "default_effort": "medium",
236
+ "efforts": [
237
+ "low",
238
+ "medium",
239
+ "high",
240
+ "xhigh",
241
+ "max"
242
+ ],
243
+ "family": null,
244
+ "listed": false,
245
+ "upgrade": null,
246
+ "version": []
247
+ },
248
+ "gpt-5.5": {
249
+ "default_effort": "medium",
250
+ "efforts": [
251
+ "low",
252
+ "medium",
253
+ "high",
254
+ "xhigh"
255
+ ],
256
+ "family": null,
257
+ "listed": false,
258
+ "upgrade": "gpt-6.1-sol",
259
+ "version": [
260
+ 5,
261
+ 5
262
+ ]
263
+ },
264
+ "gpt-5.6-luna": {
265
+ "default_effort": "medium",
266
+ "efforts": [
267
+ "low",
268
+ "medium",
269
+ "high",
270
+ "xhigh",
271
+ "max"
272
+ ],
273
+ "family": "luna",
274
+ "listed": true,
275
+ "upgrade": null,
276
+ "version": [
277
+ 5,
278
+ 6
279
+ ]
280
+ },
281
+ "gpt-5.6-sol": {
282
+ "default_effort": "low",
283
+ "efforts": [
284
+ "low",
285
+ "medium",
286
+ "high",
287
+ "xhigh",
288
+ "max",
289
+ "ultra"
290
+ ],
291
+ "family": "sol",
292
+ "listed": true,
293
+ "upgrade": null,
294
+ "version": [
295
+ 5,
296
+ 6
297
+ ]
298
+ },
299
+ "gpt-5.6-terra": {
300
+ "default_effort": "medium",
301
+ "efforts": [
302
+ "low",
303
+ "medium",
304
+ "high",
305
+ "xhigh",
306
+ "max",
307
+ "ultra"
308
+ ],
309
+ "family": "terra",
310
+ "listed": true,
311
+ "upgrade": null,
312
+ "version": [
313
+ 5,
314
+ 6
315
+ ]
316
+ },
317
+ "gpt-6-astra": {
318
+ "default_effort": "medium",
319
+ "efforts": [
320
+ "low",
321
+ "medium",
322
+ "high",
323
+ "xhigh",
324
+ "max",
325
+ "ultra"
326
+ ],
327
+ "family": "astra",
328
+ "listed": true,
329
+ "upgrade": null,
330
+ "version": [
331
+ 6
332
+ ]
333
+ },
334
+ "gpt-6-luna": {
335
+ "default_effort": "medium",
336
+ "efforts": [
337
+ "low",
338
+ "medium",
339
+ "high",
340
+ "xhigh",
341
+ "max"
342
+ ],
343
+ "family": "luna",
344
+ "listed": true,
345
+ "upgrade": null,
346
+ "version": [
347
+ 6
348
+ ]
349
+ },
350
+ "gpt-6-sol": {
351
+ "default_effort": "medium",
352
+ "efforts": [
353
+ "low",
354
+ "medium",
355
+ "high",
356
+ "xhigh",
357
+ "max",
358
+ "ultra"
359
+ ],
360
+ "family": "sol",
361
+ "listed": true,
362
+ "upgrade": null,
363
+ "version": [
364
+ 6
365
+ ]
366
+ },
367
+ "gpt-6.1-sol": {
368
+ "default_effort": "low",
369
+ "efforts": [
370
+ "low",
371
+ "medium",
372
+ "high",
373
+ "xhigh",
374
+ "max",
375
+ "ultra"
376
+ ],
377
+ "family": "sol",
378
+ "listed": true,
379
+ "upgrade": null,
380
+ "version": [
381
+ 6,
382
+ 1
383
+ ]
384
+ },
385
+ "gpt-reserve": {
386
+ "default_effort": "medium",
387
+ "efforts": [
388
+ "low",
389
+ "medium",
390
+ "high",
391
+ "xhigh",
392
+ "max"
393
+ ],
394
+ "family": null,
395
+ "listed": false,
396
+ "upgrade": null,
397
+ "version": []
398
+ }
399
+ },
400
+ "source": "release"
401
+ },
402
+ "generated_at": "2026-10-14T08:55:08Z",
403
+ "schema": 1
404
+ }