getcodexy 1.7.1__tar.gz → 1.7.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 (86) hide show
  1. {getcodexy-1.7.1 → getcodexy-1.7.2}/PKG-INFO +1 -1
  2. {getcodexy-1.7.1 → getcodexy-1.7.2}/pyproject.toml +1 -1
  3. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/_version_data/uv.lock +1 -1
  4. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component-manifest.json +11 -9
  5. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_cli.py +7 -0
  6. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_health.py +17 -3
  7. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_health_support.py +30 -14
  8. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_inspection.py +5 -23
  9. getcodexy-1.7.2/src/codexy_runtime_tools/component_inspection_host.py +33 -0
  10. getcodexy-1.7.2/src/codexy_runtime_tools/component_lifecycle_mcp.py +45 -0
  11. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_recovery.py +10 -8
  12. getcodexy-1.7.2/src/codexy_runtime_tools/component_mcp_cache.py +84 -0
  13. getcodexy-1.7.2/src/codexy_runtime_tools/component_mcp_materialization.py +201 -0
  14. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_registration_health.py +11 -28
  15. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_watcher_probe.py +2 -18
  16. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/github_pre_session.py +11 -2
  17. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/installer.py +4 -1
  18. getcodexy-1.7.2/src/codexy_runtime_tools/mcp_bootstrap.py +90 -0
  19. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/pre_session.py +8 -2
  20. {getcodexy-1.7.1 → getcodexy-1.7.2}/uv.lock +1 -1
  21. getcodexy-1.7.1/src/codexy_runtime_tools/component_watcher_materialization.py +0 -116
  22. {getcodexy-1.7.1 → getcodexy-1.7.2}/README.md +0 -0
  23. {getcodexy-1.7.1 → getcodexy-1.7.2}/build_backend.py +0 -0
  24. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/__init__.py +0 -0
  25. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/activation_transaction.py +0 -0
  26. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/cache.py +0 -0
  27. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_capability_observation.py +0 -0
  28. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_capability_probe.py +0 -0
  29. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_capability_probe_process.py +0 -0
  30. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_core_hooks.py +0 -0
  31. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_hook_activation.py +0 -0
  32. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_hook_activation_expected.py +0 -0
  33. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_hook_activation_host.py +0 -0
  34. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_hook_activation_validation.py +0 -0
  35. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_inspection_types.py +0 -0
  36. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_integrity.py +0 -0
  37. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_interactive_probe.py +0 -0
  38. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_inventory_classification.py +0 -0
  39. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_inventory_records.py +0 -0
  40. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle.py +0 -0
  41. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_admission.py +0 -0
  42. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_finish.py +0 -0
  43. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_interlock.py +0 -0
  44. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_operation.py +0 -0
  45. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_preflight.py +0 -0
  46. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_support.py +0 -0
  47. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_terminal.py +0 -0
  48. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_manifest.py +0 -0
  49. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_manifest_dependencies.py +0 -0
  50. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_manifest_values.py +0 -0
  51. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_mcp_probe.py +0 -0
  52. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_observed_inventory.py +0 -0
  53. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_resolver.py +0 -0
  54. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transaction_durability.py +0 -0
  55. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transaction_identity.py +0 -0
  56. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transaction_receipts.py +0 -0
  57. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transaction_snapshot.py +0 -0
  58. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transaction_state.py +0 -0
  59. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transition_journal.py +0 -0
  60. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transition_model.py +0 -0
  61. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transition_receipt.py +0 -0
  62. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transition_rejections.py +0 -0
  63. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/contract.py +0 -0
  64. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/github_checks.py +0 -0
  65. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/identity.py +0 -0
  66. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/local_marketplace.py +0 -0
  67. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/marketplace_identity.py +0 -0
  68. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/marketplace_repin.py +0 -0
  69. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/monolith_baseline.py +0 -0
  70. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/monolith_classifier.py +0 -0
  71. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/monolith_migration.py +0 -0
  72. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/monolith_migration_host.py +0 -0
  73. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/monolith_migration_plan.py +0 -0
  74. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/monolith_migration_state.py +0 -0
  75. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/package.py +0 -0
  76. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/package_archive.py +0 -0
  77. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/plugin_resolution.py +0 -0
  78. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/release_contract_validation.py +0 -0
  79. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/release_provenance.py +0 -0
  80. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/release_watcher_contract.py +0 -0
  81. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/runtime.py +0 -0
  82. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/runtime_configuration.py +0 -0
  83. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/source.py +0 -0
  84. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/title_policy.py +0 -0
  85. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/updater.py +0 -0
  86. {getcodexy-1.7.1 → getcodexy-1.7.2}/src/codexy_runtime_tools/version_lock.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getcodexy
3
- Version: 1.7.1
3
+ Version: 1.7.2
4
4
  Summary: A Codex harness for coordinating agents through planning, implementation, verification, and review.
5
5
  Author: Eunsoo Lee
6
6
  License-Expression: MIT
@@ -5,7 +5,7 @@ backend-path = ["."]
5
5
 
6
6
  [project]
7
7
  name = "getcodexy"
8
- version = "1.7.1"
8
+ version = "1.7.2"
9
9
  description = "A Codex harness for coordinating agents through planning, implementation, verification, and review."
10
10
  readme = { file = "README.md", content-type = "text/markdown" }
11
11
  requires-python = ">=3.10"
@@ -4,5 +4,5 @@ requires-python = ">=3.10"
4
4
 
5
5
  [[package]]
6
6
  name = "getcodexy"
7
- version = "1.7.1"
7
+ version = "1.7.2"
8
8
  source = { editable = "." }
@@ -29,7 +29,7 @@
29
29
  {
30
30
  "id": "core",
31
31
  "plugin": "codexy",
32
- "version": "1.7.1",
32
+ "version": "1.7.2",
33
33
  "dependencies": [],
34
34
  "asset": {
35
35
  "pluginId": "codexy@codexy",
@@ -38,6 +38,7 @@
38
38
  ".codex-plugin/plugin.json",
39
39
  "assets/codexy-icon.png",
40
40
  ".mcp.json",
41
+ "mcp/codexy_mcp_bootstrap.py",
41
42
  "mcp/codexy-mcp-watcher.sh",
42
43
  "mcp/codexy-mcp-watcher.cmd",
43
44
  "skills/dreaming/scripts/resumable-context-capsule.sh",
@@ -49,7 +50,7 @@
49
50
  {
50
51
  "id": "github",
51
52
  "plugin": "codexy-github",
52
- "version": "1.7.1",
53
+ "version": "1.7.2",
53
54
  "dependencies": [
54
55
  "core"
55
56
  ],
@@ -65,7 +66,7 @@
65
66
  {
66
67
  "id": "devtools",
67
68
  "plugin": "codexy-devtools",
68
- "version": "1.7.1",
69
+ "version": "1.7.2",
69
70
  "dependencies": [
70
71
  "core"
71
72
  ],
@@ -74,7 +75,8 @@
74
75
  "packageRoot": "plugins/codexy-devtools",
75
76
  "requiredPaths": [
76
77
  ".codex-plugin/plugin.json",
77
- ".mcp.json"
78
+ ".mcp.json",
79
+ "mcp/codexy_mcp_bootstrap.py"
78
80
  ]
79
81
  }
80
82
  }
@@ -82,27 +84,27 @@
82
84
  "compatibleCombinations": [
83
85
  {
84
86
  "components": [],
85
- "version": "1.7.1"
87
+ "version": "1.7.2"
86
88
  },
87
89
  {
88
90
  "components": [
89
91
  "core"
90
92
  ],
91
- "version": "1.7.1"
93
+ "version": "1.7.2"
92
94
  },
93
95
  {
94
96
  "components": [
95
97
  "core",
96
98
  "github"
97
99
  ],
98
- "version": "1.7.1"
100
+ "version": "1.7.2"
99
101
  },
100
102
  {
101
103
  "components": [
102
104
  "core",
103
105
  "devtools"
104
106
  ],
105
- "version": "1.7.1"
107
+ "version": "1.7.2"
106
108
  },
107
109
  {
108
110
  "components": [
@@ -110,7 +112,7 @@
110
112
  "github",
111
113
  "devtools"
112
114
  ],
113
- "version": "1.7.1"
115
+ "version": "1.7.2"
114
116
  }
115
117
  ]
116
118
  }
@@ -138,6 +138,13 @@ def _human(command: str, receipt: dict[str, object]) -> str:
138
138
  )
139
139
  or "none",
140
140
  )
141
+ if command in {"install", "update", "remove", "bootstrap"} and receipt.get(
142
+ "outcome"
143
+ ) in {"completed", "pending-action"}:
144
+ return (
145
+ f"getcodexy {command}: {receipt['outcome']}; "
146
+ "start a fresh Codex task to refresh installed plugins and MCP tools"
147
+ )
141
148
  if command == "doctor":
142
149
  health = receipt.get("component_health", [])
143
150
  summary = (
@@ -10,11 +10,12 @@ from .component_capability_probe import (
10
10
  )
11
11
  from .component_capability_observation import component_observations
12
12
  from .component_hook_activation import ACTIVATION_STATES
13
+ from .component_mcp_materialization import MCP_COMPONENTS, valid_component_mcp_cache
13
14
  from .component_health_support import (
14
15
  _authority_valid,
16
+ _health_plugin,
15
17
  _legacy_state,
16
18
  _observed,
17
- _plugin_root,
18
19
  manifest_is_valid,
19
20
  record_version,
20
21
  version_relation,
@@ -31,6 +32,7 @@ def health(
31
32
  admission_error: str | None,
32
33
  host_error: bool,
33
34
  activation: dict[str, str] | None = None,
35
+ codex_home=None,
34
36
  ) -> list[dict[str, object]]:
35
37
  expected = set(recorded or ()) | set(actual)
36
38
  return [
@@ -42,6 +44,7 @@ def health(
42
44
  admission_error,
43
45
  host_error,
44
46
  activation,
47
+ codex_home,
45
48
  )
46
49
  for component in manifest.component_ids
47
50
  if component in expected
@@ -56,10 +59,11 @@ def _component_health(
56
59
  admission_error,
57
60
  host_error,
58
61
  activation,
62
+ codex_home,
59
63
  ):
60
64
  record = records.get(component)
61
65
  installed = component in actual
62
- plugin = _plugin_root(record)
66
+ plugin = _health_plugin(manifest, component, record, codex_home)
63
67
  configured = bool(
64
68
  installed
65
69
  and plugin
@@ -67,11 +71,21 @@ def _component_health(
67
71
  plugin, manifest.component(component).plugin, record_version(record)
68
72
  )
69
73
  and valid_registration(plugin, component)
74
+ and (
75
+ component not in MCP_COMPONENTS
76
+ or valid_component_mcp_cache(codex_home, component, manifest.version)
77
+ )
70
78
  )
71
79
  result = dict(
72
80
  component=component,
73
81
  state=_legacy_state(
74
- manifest, component, actual, records, admission_error, host_error
82
+ manifest,
83
+ component,
84
+ actual,
85
+ records,
86
+ admission_error,
87
+ host_error,
88
+ codex_home,
75
89
  ),
76
90
  installed=installed,
77
91
  configured=configured,
@@ -4,9 +4,13 @@ import json
4
4
  import os
5
5
  from pathlib import Path
6
6
 
7
+ from .component_mcp_materialization import (
8
+ MCP_COMPONENTS,
9
+ component_cache_plugin,
10
+ mcp_spec,
11
+ )
7
12
  from .component_manifest import ComponentManifest
8
13
  from .component_registration_health import valid_registration
9
- from .component_watcher_materialization import watcher_entrypoint
10
14
  from .component_resolver import ComponentResolutionError, compare_versions
11
15
 
12
16
 
@@ -15,17 +19,23 @@ SURFACE_PATHS = {
15
19
  "agents/catalog.toml",
16
20
  "hooks/hooks.json",
17
21
  "skills/wiki/SKILL.md",
18
- ".mcp.json",
19
- "mcp/codexy-mcp-watcher.sh",
20
- "mcp/codexy-mcp-watcher.cmd",
22
+ *(path.as_posix() for path in mcp_spec("core").surface_paths),
21
23
  ),
22
24
  "github": ("agents/catalog.toml", "hooks/hooks.json"),
23
- "devtools": ("mcp/codexy-mcp-devtools", ".mcp.json"),
25
+ "devtools": tuple(path.as_posix() for path in mcp_spec("devtools").surface_paths),
24
26
  }
25
27
  AUTHORITY_KEYS = ("authority", "artifact_authority", "artifactAuthority")
26
28
 
27
29
 
28
- def _legacy_state(manifest, component, actual, records, admission_error, host_error):
30
+ def _legacy_state(
31
+ manifest,
32
+ component,
33
+ actual,
34
+ records,
35
+ admission_error,
36
+ host_error,
37
+ codex_home=None,
38
+ ):
29
39
  record = records.get(component)
30
40
  if admission_error or host_error:
31
41
  return "incompatible"
@@ -34,17 +44,13 @@ def _legacy_state(manifest, component, actual, records, admission_error, host_er
34
44
  relation = version_relation(manifest, record)
35
45
  if relation != 0:
36
46
  return "stale" if relation < 0 else "incompatible"
37
- plugin = _plugin_root(record)
38
- if plugin is None or not _required_files(manifest, component, plugin):
47
+ plugin = _health_plugin(manifest, component, record, codex_home)
48
+ if plugin is None or not _required_files(manifest, component, plugin, codex_home):
39
49
  return "stale"
40
50
  if not manifest_is_valid(
41
51
  plugin, manifest.component(component).plugin, record_version(record)
42
52
  ) or not valid_registration(plugin, component):
43
53
  return "incompatible"
44
- if component == "devtools" and not os.access(
45
- plugin / "mcp/codexy-mcp-devtools", os.X_OK
46
- ):
47
- return "stale"
48
54
  if component == "core" and any(
49
55
  os.path.lexists(plugin / path)
50
56
  for path in (".codex/lsp-client.json", "lsp", "runtime-release.json")
@@ -55,13 +61,23 @@ def _legacy_state(manifest, component, actual, records, admission_error, host_er
55
61
  return "healthy"
56
62
 
57
63
 
58
- def _required_files(manifest, component, plugin):
64
+ def _required_files(manifest, component, plugin, codex_home=None):
59
65
  paths = (
60
66
  manifest.component(component).asset.required_paths + SURFACE_PATHS[component]
61
67
  )
62
68
  return all(
63
69
  (plugin / path).is_file() and not (plugin / path).is_symlink() for path in paths
64
- ) and (component != "core" or watcher_entrypoint(plugin).is_file())
70
+ )
71
+
72
+
73
+ def _health_plugin(manifest, component, record, codex_home=None):
74
+ plugin = _plugin_root(record)
75
+ if component not in MCP_COMPONENTS or codex_home is None:
76
+ return plugin
77
+ cache_root = codex_home / "plugins" / "cache"
78
+ if cache_root.is_symlink() or not cache_root.is_dir():
79
+ return plugin
80
+ return component_cache_plugin(codex_home, component, manifest.version)
65
81
 
66
82
 
67
83
  def _plugin_root(record):
@@ -6,12 +6,12 @@ from __future__ import annotations
6
6
 
7
7
  import os
8
8
  import subprocess
9
- from enum import Enum
10
9
  from pathlib import Path
11
10
  from typing import Callable
12
11
 
13
12
  from .component_hook_activation import HookLister, activation_for_inventory
14
13
  from .component_health import health as _health
14
+ from .component_inspection_host import ProbeStage, host as _host
15
15
  from .component_inspection_types import InspectionReport
16
16
  from .component_inventory_classification import ComponentResolutionError
17
17
  from .component_inventory_classification import classify_installed_inventory
@@ -19,14 +19,13 @@ from .component_manifest import ComponentManifest, load_component_manifest
19
19
  from .component_observed_inventory import observe_installed_inventory
20
20
  from .component_resolver import admit_installed_inventory, canonical_components
21
21
  from .component_transaction_state import read_inventory
22
- from .github_pre_session import trusted_codex
23
22
  from .plugin_resolution import (
24
23
  MarketplaceBinding,
25
24
  marketplace_identity,
26
25
  named_marketplace,
27
26
  validate_local_marketplace,
28
27
  )
29
- from .pre_session import _find_codex, _json, _run
28
+ from .pre_session import _json
30
29
  from .updater import _absolute, _validate_real_path
31
30
 
32
31
 
@@ -35,12 +34,6 @@ STATUS_SCHEMA = "getcodexy.status.v1"
35
34
  DOCTOR_SCHEMA = "getcodexy.doctor.v1"
36
35
 
37
36
 
38
- class ProbeStage(str, Enum):
39
- EXECUTABLE = "codex-executable"
40
- PLUGIN_LIST = "codex-plugin-list"
41
- MARKETPLACE_LIST = "codex-marketplace-list"
42
-
43
-
44
37
  def status(
45
38
  codex_home: str | os.PathLike[str],
46
39
  *,
@@ -71,7 +64,8 @@ def doctor(
71
64
  hook_lister: HookLister | None = None,
72
65
  ) -> dict[str, object]:
73
66
  """Inspect canonical managed files and return actionable repairs."""
74
- report = _inspect(codex_home, codex, runner, hook_lister)
67
+ home = _absolute(codex_home)
68
+ report = _inspect(home, codex, runner, hook_lister)
75
69
  missing_requirements: list[str] = (
76
70
  [report["host_error"]] if report["host_error"] else []
77
71
  )
@@ -97,6 +91,7 @@ def doctor(
97
91
  report["admission_error"],
98
92
  bool(report["host_error"]),
99
93
  report["activation"],
94
+ codex_home=home,
100
95
  ),
101
96
  "source_of_truth": "installed-component-inventory",
102
97
  "errors": report["errors"],
@@ -177,19 +172,6 @@ def _inspect(
177
172
  }
178
173
 
179
174
 
180
- def _host(
181
- home: Path, codex: Path | None, runner: Runner | None
182
- ) -> tuple[Path, Runner, None] | tuple[None, None, ProbeStage]:
183
- try:
184
- return (
185
- trusted_codex(codex or _find_codex()),
186
- runner or (lambda command: _run(command, home)),
187
- None,
188
- )
189
- except (OSError, RuntimeError, ValueError):
190
- return None, None, ProbeStage.EXECUTABLE
191
-
192
-
193
175
  def _recorded(
194
176
  home: Path,
195
177
  ) -> tuple[tuple[str, ...] | None, dict[str, object], str | None]:
@@ -0,0 +1,33 @@
1
+ """Trusted host setup used by component inspection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from enum import Enum
7
+ from pathlib import Path
8
+ from typing import Callable
9
+
10
+ from .github_pre_session import trusted_codex
11
+ from .pre_session import _find_codex, _run
12
+
13
+
14
+ Runner = Callable[[list[str]], subprocess.CompletedProcess[str]]
15
+
16
+
17
+ class ProbeStage(str, Enum):
18
+ EXECUTABLE = "codex-executable"
19
+ PLUGIN_LIST = "codex-plugin-list"
20
+ MARKETPLACE_LIST = "codex-marketplace-list"
21
+
22
+
23
+ def host(
24
+ home: Path, codex: Path | None, runner: Runner | None
25
+ ) -> tuple[Path, Runner, None] | tuple[None, None, ProbeStage]:
26
+ try:
27
+ return (
28
+ trusted_codex(codex or _find_codex()),
29
+ runner or (lambda command: _run(command, home)),
30
+ None,
31
+ )
32
+ except (OSError, RuntimeError, ValueError):
33
+ return None, None, ProbeStage.EXECUTABLE
@@ -0,0 +1,45 @@
1
+ """MCP source and cache materialization for component lifecycle operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from .component_manifest import ComponentManifest
8
+ from .component_mcp_materialization import (
9
+ MCP_COMPONENTS,
10
+ materialize_component_mcp,
11
+ materialize_component_mcp_cache,
12
+ )
13
+ from .plugin_resolution import MarketplaceBinding, marketplace_path
14
+
15
+
16
+ def materialize_mcp_sources(
17
+ root: MarketplaceBinding,
18
+ manifest: ComponentManifest,
19
+ components: tuple[str, ...],
20
+ ) -> None:
21
+ marketplace = marketplace_path(root)
22
+ for component in manifest.component_ids:
23
+ if component not in components or component not in MCP_COMPONENTS:
24
+ continue
25
+ plugin = marketplace / "plugins" / manifest.component(component).plugin
26
+ materialize_component_mcp(plugin, component, manifest.version)
27
+
28
+
29
+ def materialize_mcp_caches(
30
+ home: Path,
31
+ root: MarketplaceBinding,
32
+ manifest: ComponentManifest,
33
+ components: tuple[str, ...],
34
+ ) -> None:
35
+ marketplace = marketplace_path(root)
36
+ for component in manifest.component_ids:
37
+ if component not in components or component not in MCP_COMPONENTS:
38
+ continue
39
+ source = marketplace / "plugins" / manifest.component(component).plugin
40
+ materialize_component_mcp_cache(
41
+ home,
42
+ component,
43
+ manifest.version,
44
+ source_plugin=source,
45
+ )
@@ -8,9 +8,9 @@ from typing import Callable
8
8
 
9
9
  from .component_hook_activation import HookLister
10
10
  from .component_lifecycle_finish import finish_committed
11
+ from .component_lifecycle_mcp import materialize_mcp_caches, materialize_mcp_sources
11
12
  from .component_lifecycle_preflight import existing_marketplace
12
13
  from .component_manifest import ComponentManifest
13
- from .component_watcher_materialization import materialize_watcher
14
14
  from .component_resolver import (
15
15
  ComponentResolutionError,
16
16
  reconcile_installed_inventory,
@@ -28,7 +28,7 @@ from .component_transaction_state import (
28
28
  from .component_lifecycle_terminal import terminal
29
29
  from .marketplace_repin import reconcile_official_marketplace_root
30
30
  from .pre_session import _json, official_marketplace_root
31
- from .plugin_resolution import MarketplaceBinding, MarketplaceIdentity, marketplace_path
31
+ from .plugin_resolution import MarketplaceBinding, MarketplaceIdentity
32
32
 
33
33
 
34
34
  Runner = Callable[[list[str]], subprocess.CompletedProcess[str]]
@@ -116,7 +116,7 @@ def rollback_or_raise(
116
116
  ) -> None:
117
117
  try:
118
118
  write_journal(home, journal.with_phase("rolling-back"))
119
- restore_selection(executable, invoke, manifest, root, journal.before)
119
+ restore_selection(home, executable, invoke, manifest, root, journal.before)
120
120
  if (
121
121
  selection(manifest, list_installed(executable, invoke), root)
122
122
  != journal.before
@@ -161,6 +161,7 @@ def write_completed(
161
161
 
162
162
 
163
163
  def restore_selection(
164
+ home: Path,
164
165
  executable: Path,
165
166
  invoke: Runner,
166
167
  manifest: ComponentManifest,
@@ -168,12 +169,14 @@ def restore_selection(
168
169
  before: tuple[str, ...],
169
170
  ) -> None:
170
171
  current = selection(manifest, list_installed(executable, invoke), root)
172
+ materialize_mcp_sources(root, manifest, before)
171
173
  for component in before:
172
174
  if component not in current:
173
175
  mutate(executable, invoke, "add", manifest, component)
174
176
  for component in reversed(manifest.component_ids):
175
177
  if component in current and component not in before:
176
178
  mutate(executable, invoke, "remove", manifest, component)
179
+ materialize_mcp_caches(home, root, manifest, before)
177
180
 
178
181
 
179
182
  def apply_forward(
@@ -197,15 +200,14 @@ def apply_forward(
197
200
  manifest.version,
198
201
  home,
199
202
  )
200
- if "core" in journal.target:
201
- materialize_watcher(marketplace_path(root) / "plugins/codexy", home)
203
+ materialize_mcp_sources(root, manifest, journal.target)
202
204
  for component in adds:
203
205
  mutate(executable, invoke, "add", manifest, component)
204
206
  for component in removes:
205
207
  mutate(executable, invoke, "remove", manifest, component)
206
- return verify_post_operation_inventory(
207
- manifest, list_installed(executable, invoke), journal.target, root
208
- )
208
+ installed = list_installed(executable, invoke)
209
+ materialize_mcp_caches(home, root, manifest, journal.target)
210
+ return verify_post_operation_inventory(manifest, installed, journal.target, root)
209
211
 
210
212
 
211
213
  def mutate(
@@ -0,0 +1,84 @@
1
+ """Safe synchronization helpers for component MCP cache surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import stat
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Iterable
10
+
11
+ from .updater import _absolute, _validate_real_path
12
+
13
+
14
+ def _needs_executable(path: Path) -> bool:
15
+ return path.name != "runtime-platform.sh" and path.suffix in {"", ".sh"}
16
+
17
+
18
+ def _valid_surface_file(path: Path, *, executable: bool) -> bool:
19
+ try:
20
+ _validate_real_path(_absolute(path), require_exists=True)
21
+ metadata = path.lstat()
22
+ except (OSError, ValueError):
23
+ return False
24
+ if not stat.S_ISREG(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode):
25
+ return False
26
+ return not executable or os.name == "nt" or os.access(path, os.X_OK)
27
+
28
+
29
+ def _require_directory(path: Path, label: str) -> None:
30
+ _validate_real_path(_absolute(path), require_exists=True)
31
+ metadata = path.lstat()
32
+ if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
33
+ raise RuntimeError(f"{label} is not a regular directory: {path}")
34
+
35
+
36
+ def _sync_surface(source: Path, target: Path, paths: Iterable[Path]) -> None:
37
+ _require_directory(source, "MCP source plugin")
38
+ _require_directory(target, "MCP cache plugin")
39
+ for relative in paths:
40
+ source_path, target_path = source / relative, target / relative
41
+ if not _valid_surface_file(source_path, executable=_needs_executable(relative)):
42
+ raise RuntimeError(f"MCP source file is missing or invalid: {source_path}")
43
+ _validate_real_path(_absolute(target_path.parent), require_exists=False)
44
+ target_path.parent.mkdir(parents=True, exist_ok=True)
45
+ _validate_real_path(_absolute(target_path.parent), require_exists=True)
46
+ if _same_file(source_path, target_path):
47
+ continue
48
+ if os.path.lexists(target_path):
49
+ metadata = target_path.lstat()
50
+ if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode):
51
+ raise RuntimeError(
52
+ f"MCP cache file is not a regular file: {target_path}"
53
+ )
54
+ _atomic_copy(source_path, target_path)
55
+
56
+
57
+ def _same_file(source: Path, target: Path) -> bool:
58
+ try:
59
+ target_metadata = target.lstat()
60
+ return (
61
+ stat.S_ISREG(target_metadata.st_mode)
62
+ and not stat.S_ISLNK(target_metadata.st_mode)
63
+ and source.read_bytes() == target.read_bytes()
64
+ and stat.S_IMODE(source.stat().st_mode)
65
+ == stat.S_IMODE(target_metadata.st_mode)
66
+ )
67
+ except OSError:
68
+ return False
69
+
70
+
71
+ def _atomic_copy(source: Path, target: Path) -> None:
72
+ descriptor, temporary_name = tempfile.mkstemp(
73
+ prefix=f".{target.name}.", suffix=".tmp", dir=target.parent
74
+ )
75
+ temporary = Path(temporary_name)
76
+ try:
77
+ with os.fdopen(descriptor, "wb") as output:
78
+ output.write(source.read_bytes())
79
+ output.flush()
80
+ os.fsync(output.fileno())
81
+ temporary.chmod(stat.S_IMODE(source.stat().st_mode))
82
+ os.replace(temporary, target)
83
+ finally:
84
+ temporary.unlink(missing_ok=True)