pactkit-codex 2.25.1__tar.gz → 2.26.0__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.
- {pactkit_codex-2.25.1/src/pactkit_codex.egg-info → pactkit_codex-2.26.0}/PKG-INFO +2 -2
- {pactkit_codex-2.25.1 → pactkit_codex-2.26.0}/pyproject.toml +2 -2
- {pactkit_codex-2.25.1 → pactkit_codex-2.26.0}/src/pactkit_codex/deployer.py +30 -18
- {pactkit_codex-2.25.1 → pactkit_codex-2.26.0/src/pactkit_codex.egg-info}/PKG-INFO +2 -2
- {pactkit_codex-2.25.1 → pactkit_codex-2.26.0}/src/pactkit_codex.egg-info/SOURCES.txt +2 -1
- {pactkit_codex-2.25.1 → pactkit_codex-2.26.0}/src/pactkit_codex.egg-info/requires.txt +1 -1
- pactkit_codex-2.26.0/tests/test_host_body_snapshot.py +126 -0
- {pactkit_codex-2.25.1 → pactkit_codex-2.26.0}/LICENSE +0 -0
- {pactkit_codex-2.25.1 → pactkit_codex-2.26.0}/README.md +0 -0
- {pactkit_codex-2.25.1 → pactkit_codex-2.26.0}/setup.cfg +0 -0
- {pactkit_codex-2.25.1 → pactkit_codex-2.26.0}/src/pactkit_codex/__init__.py +0 -0
- {pactkit_codex-2.25.1 → pactkit_codex-2.26.0}/src/pactkit_codex.egg-info/dependency_links.txt +0 -0
- {pactkit_codex-2.25.1 → pactkit_codex-2.26.0}/src/pactkit_codex.egg-info/entry_points.txt +0 -0
- {pactkit_codex-2.25.1 → pactkit_codex-2.26.0}/src/pactkit_codex.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pactkit-codex
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.26.0
|
|
4
4
|
Summary: PactKit adapter for OpenAI Codex CLI
|
|
5
5
|
Author: Slim
|
|
6
6
|
License: MIT
|
|
@@ -21,7 +21,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: pactkit<2.
|
|
24
|
+
Requires-Dist: pactkit<2.27.0,>=2.26.0
|
|
25
25
|
Requires-Dist: tomli>=1.1.0; python_version < "3.11"
|
|
26
26
|
Dynamic: license-file
|
|
27
27
|
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pactkit-codex"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.26.0"
|
|
8
8
|
description = "PactKit adapter for OpenAI Codex CLI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
dependencies = [
|
|
13
|
-
"pactkit>=2.
|
|
13
|
+
"pactkit>=2.26.0,<2.27.0",
|
|
14
14
|
"tomli>=1.1.0; python_version < '3.11'",
|
|
15
15
|
]
|
|
16
16
|
|
|
@@ -701,6 +701,14 @@ class CodexDeployer(DeployerBase):
|
|
|
701
701
|
previous_hashes = files
|
|
702
702
|
except (OSError, ValueError, TypeError):
|
|
703
703
|
pass
|
|
704
|
+
# STORY-slim-20260903a24e1ece0d7f: merged ownership view — the deploy
|
|
705
|
+
# check previously read only .pactkit-deployed.json (which never carries
|
|
706
|
+
# reference paths), so every content change produced a .pactkit-new
|
|
707
|
+
# candidate. Same merge as _cleanup_stale_command_references.
|
|
708
|
+
ownership_proofs = {
|
|
709
|
+
**previous_hashes,
|
|
710
|
+
**read_command_references(codex_root / "skills"),
|
|
711
|
+
}
|
|
704
712
|
rendered: dict[str, str] = {}
|
|
705
713
|
references_by_command: dict[str, dict[Path, str]] = {}
|
|
706
714
|
|
|
@@ -844,7 +852,13 @@ class CodexDeployer(DeployerBase):
|
|
|
844
852
|
)
|
|
845
853
|
rendered_bytes = reference_content.encode("utf-8")
|
|
846
854
|
relative_to_root = reference.relative_to(codex_root).as_posix()
|
|
847
|
-
|
|
855
|
+
# STORY-slim-20260903a24e1ece0d7f: merge the command-manifest
|
|
856
|
+
# references table into the ownership view — the deploy path
|
|
857
|
+
# previously read only .pactkit-deployed.json, which never
|
|
858
|
+
# carried reference paths, so every content change produced a
|
|
859
|
+
# .pactkit-new candidate (three recurrences on 2026-09-03).
|
|
860
|
+
# Same merged view as _cleanup_stale_command_references.
|
|
861
|
+
expected_hash = ownership_proofs.get(relative_to_root)
|
|
848
862
|
if reference.is_file():
|
|
849
863
|
actual_hash = hashlib.sha256(reference.read_bytes()).hexdigest()
|
|
850
864
|
rendered_hash = hashlib.sha256(rendered_bytes).hexdigest()
|
|
@@ -909,15 +923,17 @@ class CodexDeployer(DeployerBase):
|
|
|
909
923
|
|
|
910
924
|
@staticmethod
|
|
911
925
|
def generate_codex_config_toml(codex_root):
|
|
912
|
-
"""
|
|
926
|
+
"""Suggest Codex CLI settings — but NEVER write config.toml.
|
|
913
927
|
|
|
914
928
|
config.toml carries the user's providers, MCP servers, project trust
|
|
915
|
-
and other sensitive state.
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
- File exists -> leave it BYTE-IDENTICAL
|
|
919
|
-
|
|
920
|
-
|
|
929
|
+
and other sensitive state. Policy (2026-08-13, user directive after
|
|
930
|
+
two wipe incidents; extended 2026-09-11 by
|
|
931
|
+
STORY-slim-20260911b2bbd79889e0 A2/R3+R4):
|
|
932
|
+
- File exists -> leave it BYTE-IDENTICAL.
|
|
933
|
+
- File missing -> ALSO leave it missing. Creating it seeded with
|
|
934
|
+
host-permission scalars and an MCP server was injection, not
|
|
935
|
+
convenience — the user starts their own config, and PactKit
|
|
936
|
+
prints what it would have suggested instead.
|
|
921
937
|
"""
|
|
922
938
|
config_path = codex_root / "config.toml"
|
|
923
939
|
|
|
@@ -925,16 +941,12 @@ class CodexDeployer(DeployerBase):
|
|
|
925
941
|
print(" ℹ️ config.toml exists — left untouched (PactKit never modifies it)")
|
|
926
942
|
return
|
|
927
943
|
|
|
928
|
-
|
|
929
|
-
"
|
|
930
|
-
|
|
931
|
-
'
|
|
932
|
-
""
|
|
933
|
-
|
|
934
|
-
"[mcp_servers.context7]",
|
|
935
|
-
'url = "https://mcp.context7.com/mcp"',
|
|
936
|
-
]
|
|
937
|
-
atomic_write(config_path, chr(10).join(lines) + chr(10))
|
|
944
|
+
print(
|
|
945
|
+
" ℹ️ no config.toml — none created (PactKit never writes it).\n"
|
|
946
|
+
" If you want the settings PactKit was designed around, add by hand:\n"
|
|
947
|
+
' sandbox_mode = "workspace-write"\n'
|
|
948
|
+
' approval_policy = "on-request"'
|
|
949
|
+
)
|
|
938
950
|
|
|
939
951
|
@staticmethod
|
|
940
952
|
def generate_codex_project_files(project_root):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pactkit-codex
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.26.0
|
|
4
4
|
Summary: PactKit adapter for OpenAI Codex CLI
|
|
5
5
|
Author: Slim
|
|
6
6
|
License: MIT
|
|
@@ -21,7 +21,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: pactkit<2.
|
|
24
|
+
Requires-Dist: pactkit<2.27.0,>=2.26.0
|
|
25
25
|
Requires-Dist: tomli>=1.1.0; python_version < "3.11"
|
|
26
26
|
Dynamic: license-file
|
|
27
27
|
|
|
@@ -8,4 +8,5 @@ src/pactkit_codex.egg-info/SOURCES.txt
|
|
|
8
8
|
src/pactkit_codex.egg-info/dependency_links.txt
|
|
9
9
|
src/pactkit_codex.egg-info/entry_points.txt
|
|
10
10
|
src/pactkit_codex.egg-info/requires.txt
|
|
11
|
-
src/pactkit_codex.egg-info/top_level.txt
|
|
11
|
+
src/pactkit_codex.egg-info/top_level.txt
|
|
12
|
+
tests/test_host_body_snapshot.py
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""This host's deployed prompt bodies, pinned against a golden (R7/AC10).
|
|
2
|
+
|
|
3
|
+
2026-09-12 review (83/100, gap 2): the zero-credential acceptance split
|
|
4
|
+
left adapter-host body comparison out of CI — the core workflow compares
|
|
5
|
+
classic only. This suite pins the CODEX deployment's prompt bodies:
|
|
6
|
+
|
|
7
|
+
- digest drift (a rule edited or deleted inside a prompt body) fails,
|
|
8
|
+
naming the file;
|
|
9
|
+
- a core version mismatch fails with an explicit action — regenerate the
|
|
10
|
+
golden against the new core, or publish the matching core release
|
|
11
|
+
(PENDING-COMPATIBLE-CORE, see the spec R8 release-window semantics);
|
|
12
|
+
- a probe test deploys, edits one body in place, and proves the SAME
|
|
13
|
+
comparison logic flags it (删改必被检出, not vacuous).
|
|
14
|
+
|
|
15
|
+
Regenerate with: tests/scripts/refresh_host_body_golden.py
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import pytest
|
|
27
|
+
|
|
28
|
+
HOST = "codex"
|
|
29
|
+
GOLDEN = Path(__file__).parent / "fixtures" / "host_body_golden.json"
|
|
30
|
+
DEPLOY_SUFFIXES = (".md", ".json", ".toml")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _core_version() -> str:
|
|
34
|
+
import importlib.metadata as m
|
|
35
|
+
|
|
36
|
+
return m.version("pactkit")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _normalize(content: str, deploy_root: str) -> str:
|
|
40
|
+
"""Strip only the deterministic environment difference: the deploy root
|
|
41
|
+
path baked into rendered prompts. Rule text stays — normalizing it would
|
|
42
|
+
swallow the drift this suite exists to catch."""
|
|
43
|
+
return content.replace(deploy_root, "<DEPLOY_ROOT>")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _deployed_digests(tmp_path: Path) -> tuple[dict[str, str] | None, subprocess.CompletedProcess]:
|
|
47
|
+
home = tmp_path / "home"
|
|
48
|
+
target = tmp_path / "deploy"
|
|
49
|
+
home.mkdir()
|
|
50
|
+
proc = subprocess.run(
|
|
51
|
+
[sys.executable, "-m", "pactkit", "init", "--format", HOST, "--target", str(target)],
|
|
52
|
+
capture_output=True, text=True, timeout=180,
|
|
53
|
+
env={"PATH": "/usr/bin:/bin", "HOME": str(home), "COLUMNS": "200"},
|
|
54
|
+
)
|
|
55
|
+
if proc.returncode != 0:
|
|
56
|
+
return None, proc
|
|
57
|
+
digests: dict[str, str] = {}
|
|
58
|
+
for path in sorted(target.rglob("*")):
|
|
59
|
+
if not path.is_file() or path.suffix not in DEPLOY_SUFFIXES:
|
|
60
|
+
continue
|
|
61
|
+
body = _normalize(path.read_text(encoding="utf-8"), str(target))
|
|
62
|
+
digests[path.relative_to(target).as_posix()] = hashlib.sha256(
|
|
63
|
+
body.encode("utf-8")
|
|
64
|
+
).hexdigest()
|
|
65
|
+
return digests, proc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _assert_no_drift(golden_digests: dict[str, str], live: dict[str, str]) -> None:
|
|
69
|
+
"""The one comparison both the golden test and the tamper probe use."""
|
|
70
|
+
changed = sorted(
|
|
71
|
+
k for k in set(golden_digests) & set(live) if golden_digests[k] != live[k]
|
|
72
|
+
)
|
|
73
|
+
removed = sorted(set(golden_digests) - set(live))
|
|
74
|
+
added = sorted(set(live) - set(golden_digests))
|
|
75
|
+
assert not (changed or removed or added), (
|
|
76
|
+
f"prompt body drift: changed={changed} removed={removed} added={added} "
|
|
77
|
+
f"— if the core prompts changed, regenerate the golden "
|
|
78
|
+
f"(tests/scripts/refresh_host_body_golden.py) or publish the matching "
|
|
79
|
+
f"core release first (PENDING-COMPATIBLE-CORE)"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestHostBodySnapshot:
|
|
84
|
+
def test_bodies_match_the_golden(self, tmp_path):
|
|
85
|
+
golden = json.loads(GOLDEN.read_text(encoding="utf-8"))
|
|
86
|
+
core_v = _core_version()
|
|
87
|
+
if golden["core_version"] != core_v:
|
|
88
|
+
pytest.fail(
|
|
89
|
+
f"PENDING-COMPATIBLE-CORE: the {HOST} golden was generated "
|
|
90
|
+
f"against core {golden['core_version']}, this environment has "
|
|
91
|
+
f"{core_v}. Either regenerate it after the core change "
|
|
92
|
+
f"(tests/scripts/refresh_host_body_golden.py) or publish the "
|
|
93
|
+
f"matching core release first (spec R8 release-window semantics)."
|
|
94
|
+
)
|
|
95
|
+
live, proc = _deployed_digests(tmp_path)
|
|
96
|
+
assert live is not None, (
|
|
97
|
+
f"{HOST} deploy failed: {proc.stdout[-300:]}{proc.stderr[-300:]}"
|
|
98
|
+
)
|
|
99
|
+
_assert_no_drift(golden["body_digests"], live)
|
|
100
|
+
|
|
101
|
+
def test_a_body_edit_is_caught_by_the_same_comparison(self, tmp_path):
|
|
102
|
+
"""Non-vacuity, end to end: deploy, edit one prompt body in place,
|
|
103
|
+
and the SAME comparison that guards the golden must flag it."""
|
|
104
|
+
golden = json.loads(GOLDEN.read_text(encoding="utf-8"))
|
|
105
|
+
live, proc = _deployed_digests(tmp_path)
|
|
106
|
+
assert live is not None, f"{HOST} deploy failed: {proc.stderr[-300:]}"
|
|
107
|
+
target = tmp_path / "deploy"
|
|
108
|
+
# Pick a real deployed prompt body and delete one rule line from it.
|
|
109
|
+
victims = sorted(
|
|
110
|
+
p for p in target.rglob("*.md") if p.relative_to(target).as_posix() in golden["body_digests"]
|
|
111
|
+
)
|
|
112
|
+
assert victims, "no deployed .md body found to tamper with"
|
|
113
|
+
victim = victims[0]
|
|
114
|
+
rel = victim.relative_to(target).as_posix()
|
|
115
|
+
body = victim.read_text(encoding="utf-8")
|
|
116
|
+
victim.write_text(body + "\n- deleted in a review\n", encoding="utf-8")
|
|
117
|
+
|
|
118
|
+
tampered = {
|
|
119
|
+
p.relative_to(target).as_posix(): hashlib.sha256(
|
|
120
|
+
_normalize(p.read_text(encoding="utf-8"), str(target)).encode("utf-8")
|
|
121
|
+
).hexdigest()
|
|
122
|
+
for p in sorted(target.rglob("*"))
|
|
123
|
+
if p.is_file() and p.suffix in DEPLOY_SUFFIXES
|
|
124
|
+
}
|
|
125
|
+
with pytest.raises(AssertionError, match=rel):
|
|
126
|
+
_assert_no_drift(golden["body_digests"], tampered)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pactkit_codex-2.25.1 → pactkit_codex-2.26.0}/src/pactkit_codex.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|