getcodexy 1.7.0__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 (85) hide show
  1. {getcodexy-1.7.0 → getcodexy-1.7.2}/PKG-INFO +1 -1
  2. {getcodexy-1.7.0 → getcodexy-1.7.2}/pyproject.toml +1 -1
  3. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/_version_data/uv.lock +1 -1
  4. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component-manifest.json +11 -9
  5. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_cli.py +7 -0
  6. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_health.py +17 -3
  7. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_health_support.py +29 -12
  8. {getcodexy-1.7.0 → 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.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_recovery.py +9 -4
  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.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_registration_health.py +11 -26
  15. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_watcher_probe.py +2 -25
  16. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/github_pre_session.py +13 -1
  17. {getcodexy-1.7.0 → 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.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/pre_session.py +8 -0
  20. {getcodexy-1.7.0 → getcodexy-1.7.2}/uv.lock +1 -1
  21. {getcodexy-1.7.0 → getcodexy-1.7.2}/README.md +0 -0
  22. {getcodexy-1.7.0 → getcodexy-1.7.2}/build_backend.py +0 -0
  23. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/__init__.py +0 -0
  24. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/activation_transaction.py +0 -0
  25. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/cache.py +0 -0
  26. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_capability_observation.py +0 -0
  27. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_capability_probe.py +0 -0
  28. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_capability_probe_process.py +0 -0
  29. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_core_hooks.py +0 -0
  30. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_hook_activation.py +0 -0
  31. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_hook_activation_expected.py +0 -0
  32. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_hook_activation_host.py +0 -0
  33. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_hook_activation_validation.py +0 -0
  34. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_inspection_types.py +0 -0
  35. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_integrity.py +0 -0
  36. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_interactive_probe.py +0 -0
  37. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_inventory_classification.py +0 -0
  38. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_inventory_records.py +0 -0
  39. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle.py +0 -0
  40. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_admission.py +0 -0
  41. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_finish.py +0 -0
  42. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_interlock.py +0 -0
  43. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_operation.py +0 -0
  44. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_preflight.py +0 -0
  45. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_support.py +0 -0
  46. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_lifecycle_terminal.py +0 -0
  47. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_manifest.py +0 -0
  48. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_manifest_dependencies.py +0 -0
  49. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_manifest_values.py +0 -0
  50. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_mcp_probe.py +0 -0
  51. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_observed_inventory.py +0 -0
  52. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_resolver.py +0 -0
  53. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transaction_durability.py +0 -0
  54. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transaction_identity.py +0 -0
  55. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transaction_receipts.py +0 -0
  56. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transaction_snapshot.py +0 -0
  57. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transaction_state.py +0 -0
  58. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transition_journal.py +0 -0
  59. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transition_model.py +0 -0
  60. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transition_receipt.py +0 -0
  61. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/component_transition_rejections.py +0 -0
  62. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/contract.py +0 -0
  63. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/github_checks.py +0 -0
  64. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/identity.py +0 -0
  65. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/local_marketplace.py +0 -0
  66. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/marketplace_identity.py +0 -0
  67. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/marketplace_repin.py +0 -0
  68. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/monolith_baseline.py +0 -0
  69. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/monolith_classifier.py +0 -0
  70. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/monolith_migration.py +0 -0
  71. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/monolith_migration_host.py +0 -0
  72. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/monolith_migration_plan.py +0 -0
  73. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/monolith_migration_state.py +0 -0
  74. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/package.py +0 -0
  75. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/package_archive.py +0 -0
  76. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/plugin_resolution.py +0 -0
  77. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/release_contract_validation.py +0 -0
  78. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/release_provenance.py +0 -0
  79. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/release_watcher_contract.py +0 -0
  80. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/runtime.py +0 -0
  81. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/runtime_configuration.py +0 -0
  82. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/source.py +0 -0
  83. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/title_policy.py +0 -0
  84. {getcodexy-1.7.0 → getcodexy-1.7.2}/src/codexy_runtime_tools/updater.py +0 -0
  85. {getcodexy-1.7.0 → 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.0
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.0"
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.0"
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.0",
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.0",
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.0",
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.0"
87
+ "version": "1.7.2"
86
88
  },
87
89
  {
88
90
  "components": [
89
91
  "core"
90
92
  ],
91
- "version": "1.7.0"
93
+ "version": "1.7.2"
92
94
  },
93
95
  {
94
96
  "components": [
95
97
  "core",
96
98
  "github"
97
99
  ],
98
- "version": "1.7.0"
100
+ "version": "1.7.2"
99
101
  },
100
102
  {
101
103
  "components": [
102
104
  "core",
103
105
  "devtools"
104
106
  ],
105
- "version": "1.7.0"
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.0"
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,6 +4,11 @@ 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
14
  from .component_resolver import ComponentResolutionError, compare_versions
@@ -14,17 +19,23 @@ SURFACE_PATHS = {
14
19
  "agents/catalog.toml",
15
20
  "hooks/hooks.json",
16
21
  "skills/wiki/SKILL.md",
17
- ".mcp.json",
18
- "mcp/codexy-mcp-watcher.sh",
19
- "mcp/codexy-mcp-watcher.cmd",
22
+ *(path.as_posix() for path in mcp_spec("core").surface_paths),
20
23
  ),
21
24
  "github": ("agents/catalog.toml", "hooks/hooks.json"),
22
- "devtools": ("mcp/codexy-mcp-devtools", ".mcp.json"),
25
+ "devtools": tuple(path.as_posix() for path in mcp_spec("devtools").surface_paths),
23
26
  }
24
27
  AUTHORITY_KEYS = ("authority", "artifact_authority", "artifactAuthority")
25
28
 
26
29
 
27
- 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
+ ):
28
39
  record = records.get(component)
29
40
  if admission_error or host_error:
30
41
  return "incompatible"
@@ -33,17 +44,13 @@ def _legacy_state(manifest, component, actual, records, admission_error, host_er
33
44
  relation = version_relation(manifest, record)
34
45
  if relation != 0:
35
46
  return "stale" if relation < 0 else "incompatible"
36
- plugin = _plugin_root(record)
37
- 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):
38
49
  return "stale"
39
50
  if not manifest_is_valid(
40
51
  plugin, manifest.component(component).plugin, record_version(record)
41
52
  ) or not valid_registration(plugin, component):
42
53
  return "incompatible"
43
- if component == "devtools" and not os.access(
44
- plugin / "mcp/codexy-mcp-devtools", os.X_OK
45
- ):
46
- return "stale"
47
54
  if component == "core" and any(
48
55
  os.path.lexists(plugin / path)
49
56
  for path in (".codex/lsp-client.json", "lsp", "runtime-release.json")
@@ -54,7 +61,7 @@ def _legacy_state(manifest, component, actual, records, admission_error, host_er
54
61
  return "healthy"
55
62
 
56
63
 
57
- def _required_files(manifest, component, plugin):
64
+ def _required_files(manifest, component, plugin, codex_home=None):
58
65
  paths = (
59
66
  manifest.component(component).asset.required_paths + SURFACE_PATHS[component]
60
67
  )
@@ -63,6 +70,16 @@ def _required_files(manifest, component, plugin):
63
70
  )
64
71
 
65
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)
81
+
82
+
66
83
  def _plugin_root(record):
67
84
  source = (record or {}).get("source")
68
85
  value = source.get("path") if isinstance(source, dict) else None
@@ -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,6 +8,7 @@ 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
14
  from .component_resolver import (
@@ -115,7 +116,7 @@ def rollback_or_raise(
115
116
  ) -> None:
116
117
  try:
117
118
  write_journal(home, journal.with_phase("rolling-back"))
118
- restore_selection(executable, invoke, manifest, root, journal.before)
119
+ restore_selection(home, executable, invoke, manifest, root, journal.before)
119
120
  if (
120
121
  selection(manifest, list_installed(executable, invoke), root)
121
122
  != journal.before
@@ -160,6 +161,7 @@ def write_completed(
160
161
 
161
162
 
162
163
  def restore_selection(
164
+ home: Path,
163
165
  executable: Path,
164
166
  invoke: Runner,
165
167
  manifest: ComponentManifest,
@@ -167,12 +169,14 @@ def restore_selection(
167
169
  before: tuple[str, ...],
168
170
  ) -> None:
169
171
  current = selection(manifest, list_installed(executable, invoke), root)
172
+ materialize_mcp_sources(root, manifest, before)
170
173
  for component in before:
171
174
  if component not in current:
172
175
  mutate(executable, invoke, "add", manifest, component)
173
176
  for component in reversed(manifest.component_ids):
174
177
  if component in current and component not in before:
175
178
  mutate(executable, invoke, "remove", manifest, component)
179
+ materialize_mcp_caches(home, root, manifest, before)
176
180
 
177
181
 
178
182
  def apply_forward(
@@ -196,13 +200,14 @@ def apply_forward(
196
200
  manifest.version,
197
201
  home,
198
202
  )
203
+ materialize_mcp_sources(root, manifest, journal.target)
199
204
  for component in adds:
200
205
  mutate(executable, invoke, "add", manifest, component)
201
206
  for component in removes:
202
207
  mutate(executable, invoke, "remove", manifest, component)
203
- return verify_post_operation_inventory(
204
- manifest, list_installed(executable, invoke), journal.target, root
205
- )
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)
206
211
 
207
212
 
208
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)