pactkit-opencode 3.0.3__tar.gz → 3.0.5__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_opencode-3.0.3 → pactkit_opencode-3.0.5}/PKG-INFO +1 -1
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/pyproject.toml +1 -1
- pactkit_opencode-3.0.5/src/pactkit_opencode/__init__.py +20 -0
- pactkit_opencode-3.0.5/src/pactkit_opencode/declarations.py +209 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/src/pactkit_opencode/deployer.py +11 -2
- pactkit_opencode-3.0.5/src/pactkit_opencode/enforcement.py +59 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/src/pactkit_opencode.egg-info/PKG-INFO +1 -1
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/src/pactkit_opencode.egg-info/SOURCES.txt +4 -0
- pactkit_opencode-3.0.5/tests/test_opencode_enforcement_channel.py +115 -0
- pactkit_opencode-3.0.5/tests/test_step6_self_sufficiency.py +128 -0
- pactkit_opencode-3.0.3/src/pactkit_opencode/__init__.py +0 -8
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/README.md +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/setup.cfg +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/src/pactkit_opencode.egg-info/dependency_links.txt +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/src/pactkit_opencode.egg-info/entry_points.txt +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/src/pactkit_opencode.egg-info/requires.txt +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/src/pactkit_opencode.egg-info/top_level.txt +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_a1_config_recovery.py +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_a2_mcp_retired.py +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_bug035_opencode_dual_layer.py +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_deploy_opencode_parity.py +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_gate_plugin_deploy.py +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_host_body_snapshot.py +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_story069_opencode_format.py +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_story070_opencode_compliance.py +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_story071_opencode_config_parity.py +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_story073_command_model_routing.py +0 -0
- {pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_story_work_units.py +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""PactKit adapter for OpenCode IDE.
|
|
2
|
+
|
|
3
|
+
Auto-registers OpenCodeDeployer via entry_points when imported.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pactkit_opencode.deployer import OpenCodeDeployer
|
|
7
|
+
|
|
8
|
+
# The second import is for its side effect: it registers OpenCode's
|
|
9
|
+
# enforcement channel (SPEC-4.0-06 R3), and registration IS Core's
|
|
10
|
+
# availability signal — an adapter that is not installed cannot register, so
|
|
11
|
+
# `format == "all"` never installs a channel for a host that never deployed.
|
|
12
|
+
#
|
|
13
|
+
# This adapter's channel is the one exception to that sentence: Core installs
|
|
14
|
+
# nothing for opencode, because `_deploy_gate_plugin()` installs the plugin
|
|
15
|
+
# itself, globally. Registering still matters — without it Core answers "no
|
|
16
|
+
# channel" for a host that has one, and `gate_coverage` reports the pre-tool
|
|
17
|
+
# gates as unenforced while the plugin is on disk and blocking.
|
|
18
|
+
from pactkit_opencode.enforcement import opencode_channel # noqa: F401
|
|
19
|
+
|
|
20
|
+
__all__ = ["OpenCodeDeployer"]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Opencode's own declarations (SPEC-4.0-22 Step 6).
|
|
2
|
+
|
|
3
|
+
These lived in Core's transitional seed (`_CORE_CAPABILITIES`, `_CORE_SECURITY`,
|
|
4
|
+
`_CORE_PRETOOL_SURFACES`, `_CORE_DEPLOYMENT_CLAIMS`) until Step 6. While they
|
|
5
|
+
did, this adapter could not answer for itself: `import pactkit_opencode` read Core's
|
|
6
|
+
table
|
|
7
|
+
for its own identity, so removing the seed broke the import rather than the
|
|
8
|
+
deployment.
|
|
9
|
+
|
|
10
|
+
Core still owns the **vocabulary** (`capability.CAPABILITY_VOCABULARY`), the
|
|
11
|
+
four levels, and the hole-filling rule in `resolve_capabilities`. What a given
|
|
12
|
+
Host scores on them is the Host's own declaration — that is the split
|
|
13
|
+
SPEC-4.0-22 R1 draws, and this module is the Opencode side of it.
|
|
14
|
+
|
|
15
|
+
Every value here was materialised from the seed Core was carrying, so this is a
|
|
16
|
+
move, not a re-measurement. The `Capability.evidence` strings are preserved
|
|
17
|
+
verbatim for the same reason: they are what the seed said, and re-wording them
|
|
18
|
+
here would quietly change what the registry reports.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from pactkit.capability import Capability, CapabilityLevel
|
|
24
|
+
from pactkit.profiles import (
|
|
25
|
+
CLIPolicy,
|
|
26
|
+
DeploymentClaim,
|
|
27
|
+
FormatProfile,
|
|
28
|
+
InputTranslation,
|
|
29
|
+
SecurityDeclaration,
|
|
30
|
+
SecurityFacts,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"CAPABILITIES",
|
|
35
|
+
"DEPLOYMENT_CLAIM",
|
|
36
|
+
"INPUT_TRANSLATION",
|
|
37
|
+
"PRETOOL_SURFACE",
|
|
38
|
+
"PROFILE",
|
|
39
|
+
"SECURITY",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _cap(
|
|
44
|
+
name: str, level: CapabilityLevel, implemented: bool, evidence: str
|
|
45
|
+
) -> Capability:
|
|
46
|
+
return Capability(
|
|
47
|
+
name=name, level=level, implemented=implemented, evidence=evidence
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
#: The Host's profile. Core no longer owns it; this module is the source.
|
|
52
|
+
PROFILE = FormatProfile(
|
|
53
|
+
name='opencode',
|
|
54
|
+
display_name='OpenCode',
|
|
55
|
+
global_config_dir='~/.config/opencode',
|
|
56
|
+
project_config_dir='.opencode',
|
|
57
|
+
skills_dir='~/.config/opencode/skills',
|
|
58
|
+
agents_dir='~/.config/opencode/agents',
|
|
59
|
+
commands_dir='~/.config/opencode/commands',
|
|
60
|
+
rules_dir='~/.config/opencode/rules',
|
|
61
|
+
project_instructions_file='AGENTS.md',
|
|
62
|
+
global_instructions_file='AGENTS.md',
|
|
63
|
+
pactkit_yaml_path='.opencode/pactkit.yaml',
|
|
64
|
+
agent_format='md',
|
|
65
|
+
rules_import_style='inline',
|
|
66
|
+
excluded_agent_fields=frozenset({'memory', 'permissionMode', 'skills'}),
|
|
67
|
+
excluded_commands=frozenset(),
|
|
68
|
+
supports_model_routing=True,
|
|
69
|
+
skills_path_var='~/.config/opencode/skills',
|
|
70
|
+
command_deploy_form='flat',
|
|
71
|
+
agent_dialect='opencode',
|
|
72
|
+
command_dialect='opencode',
|
|
73
|
+
core_bundled=False,
|
|
74
|
+
cli_policy=CLIPolicy.REQUIRED,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
#: What this Host scores on the capability vocabulary.
|
|
78
|
+
CAPABILITIES: dict[str, Capability] = {
|
|
79
|
+
'completion_hook': _cap('completion_hook', CapabilityLevel.DEGRADED, False,
|
|
80
|
+
"unverified — not measured on this Host; absence of the capability is NOT "
|
|
81
|
+
"established (SPEC-4.0-04 R2: unverified is not unavailable)",
|
|
82
|
+
),
|
|
83
|
+
'git_hook': _cap('git_hook', CapabilityLevel.NATIVE, True,
|
|
84
|
+
'shipped (commit_gate pre-commit chain)',
|
|
85
|
+
),
|
|
86
|
+
'global_instruction': _cap('global_instruction', CapabilityLevel.DEGRADED, False,
|
|
87
|
+
"unverified — not measured on this Host; absence of the capability is NOT "
|
|
88
|
+
"established (SPEC-4.0-04 R2: unverified is not unavailable)",
|
|
89
|
+
),
|
|
90
|
+
'native_command': _cap('native_command', CapabilityLevel.NATIVE, True,
|
|
91
|
+
'shipped (generators.deployer command deploy)',
|
|
92
|
+
),
|
|
93
|
+
'native_skill': _cap('native_skill', CapabilityLevel.NATIVE, True,
|
|
94
|
+
'shipped (generators.deployer._deploy_skills)',
|
|
95
|
+
),
|
|
96
|
+
'persistent_session': _cap('persistent_session', CapabilityLevel.DEGRADED, False,
|
|
97
|
+
"unverified — not measured on this Host; absence of the capability is NOT "
|
|
98
|
+
"established (SPEC-4.0-04 R2: unverified is not unavailable)",
|
|
99
|
+
),
|
|
100
|
+
'post_tool_hook': _cap('post_tool_hook', CapabilityLevel.DEGRADED, False,
|
|
101
|
+
"unverified — not measured on this Host; absence of the capability is NOT "
|
|
102
|
+
"established (SPEC-4.0-04 R2: unverified is not unavailable)",
|
|
103
|
+
),
|
|
104
|
+
'pre_tool_hook': _cap('pre_tool_hook', CapabilityLevel.EQUIVALENT, True,
|
|
105
|
+
"shipped as a plugin veto (tool.execute.before) — Equivalent, not Native: it "
|
|
106
|
+
"is PactKit's plugin, not a host facility. End-to-end in a real session "
|
|
107
|
+
"2026-09-18 (docs/guides/opencode-pretool-channel-evidence.md)",
|
|
108
|
+
),
|
|
109
|
+
'project_instruction': _cap('project_instruction', CapabilityLevel.EQUIVALENT, True,
|
|
110
|
+
'shipped (deployed instruction file)',
|
|
111
|
+
),
|
|
112
|
+
'rule_import': _cap('rule_import', CapabilityLevel.DEGRADED, False,
|
|
113
|
+
"unverified — not measured on this Host; absence of the capability is NOT "
|
|
114
|
+
"established (SPEC-4.0-04 R2: unverified is not unavailable)",
|
|
115
|
+
),
|
|
116
|
+
'session_resume': _cap('session_resume', CapabilityLevel.DEGRADED, False,
|
|
117
|
+
"unverified — not measured on this Host; absence of the capability is NOT "
|
|
118
|
+
"established (SPEC-4.0-04 R2: unverified is not unavailable)",
|
|
119
|
+
),
|
|
120
|
+
'shell_execution': _cap('shell_execution', CapabilityLevel.NATIVE, True,
|
|
121
|
+
'shipped (native bash tool)',
|
|
122
|
+
),
|
|
123
|
+
'structured_tool_result': _cap('structured_tool_result', CapabilityLevel.DEGRADED,
|
|
124
|
+
False,
|
|
125
|
+
"unverified — not measured on this Host; absence of the capability is NOT "
|
|
126
|
+
"established (SPEC-4.0-04 R2: unverified is not unavailable)",
|
|
127
|
+
),
|
|
128
|
+
'subagent': _cap('subagent', CapabilityLevel.DEGRADED, False,
|
|
129
|
+
"unverified — not measured on this Host; absence of the capability is NOT "
|
|
130
|
+
"established (SPEC-4.0-04 R2: unverified is not unavailable)",
|
|
131
|
+
),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#: Files a gate must treat specially on this Host (SPEC-4.0-22 R8).
|
|
135
|
+
SECURITY = SecurityDeclaration(
|
|
136
|
+
env_capable=SecurityFacts.declared(['opencode.json']),
|
|
137
|
+
protected=SecurityFacts.empty(),
|
|
138
|
+
conditional=SecurityFacts.empty(),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
#: Which mechanism this Host offers for pre-tool interception, when one
|
|
142
|
+
#: has been measured. `None` means unverified — not "cannot".
|
|
143
|
+
PRETOOL_SURFACE = 'tool.execute.before (plugin; veto by throwing)'
|
|
144
|
+
|
|
145
|
+
#: How this Host executes and where its verification comes from.
|
|
146
|
+
DEPLOYMENT_CLAIM = DeploymentClaim(
|
|
147
|
+
execution_mode='portable',
|
|
148
|
+
verification_source='adapter_native_command_facade',
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
#: Read-side wire-format data (SPEC-4.0-22 Step 4/7).
|
|
152
|
+
#:
|
|
153
|
+
#: **This Host's wire format, not Core's data about it.** Until Step 7 this
|
|
154
|
+
#: lived in Core (`host_payloads.OPENCODE_INPUT`) and was registered into the
|
|
155
|
+
#: registry at import time. That made Core the owner of a Host-specific
|
|
156
|
+
#: format, and it stayed invisible for exactly as long as Core seeded this Host
|
|
157
|
+
#: at import — the moment the seed went, Core's import-time registration raised
|
|
158
|
+
#: `UnknownAdapterError` in every process that had not run entry-point
|
|
159
|
+
#: discovery, and the gate it feeds failed **open** (measured 2026-09-24).
|
|
160
|
+
#:
|
|
161
|
+
#: The Step 6 commit on this branch declared the slot but left it `None`, which
|
|
162
|
+
#: would have been worse than the bug it replaced: with Core's copy deleted,
|
|
163
|
+
#: nothing would recognise an OpenCode payload at all and the whole channel
|
|
164
|
+
#: would fail open. The values below are Core's, moved **verbatim** — this is
|
|
165
|
+
#: a move, not a re-measurement, and re-deriving them would quietly change what
|
|
166
|
+
#: the gate reads.
|
|
167
|
+
#:
|
|
168
|
+
#: Two entries are explicitly *not* evidence, and say so: `patch` has an
|
|
169
|
+
#: unmeasured argument shape (it is listed so that "every write tool is in the
|
|
170
|
+
#: table" holds, and it is refused through `uncovered_write_tools`), and
|
|
171
|
+
#: unknown tools carrying a path-ish argument are treated as writes as a
|
|
172
|
+
#: fallback.
|
|
173
|
+
#:
|
|
174
|
+
#: `tool_map` is **not injective** — `patch` and `write` both canonicalise to
|
|
175
|
+
#: `Write`. The order below is therefore not semantic: Core's reverse lookup
|
|
176
|
+
#: breaks the tie on `uncovered_write_tools` (preferring the verified spelling)
|
|
177
|
+
#: rather than on iteration order, precisely so this declaration can be written
|
|
178
|
+
#: in whatever order reads best.
|
|
179
|
+
INPUT_TRANSLATION: InputTranslation | None = InputTranslation(
|
|
180
|
+
requires_keys=('tool', 'args'),
|
|
181
|
+
excludes_keys=('tool_name',),
|
|
182
|
+
tool_map={
|
|
183
|
+
'bash': 'Bash',
|
|
184
|
+
'edit': 'Edit',
|
|
185
|
+
'patch': 'Write',
|
|
186
|
+
'write': 'Write',
|
|
187
|
+
},
|
|
188
|
+
arg_map={
|
|
189
|
+
'bash': {
|
|
190
|
+
'command': 'command',
|
|
191
|
+
},
|
|
192
|
+
'edit': {
|
|
193
|
+
'filePath': 'file_path',
|
|
194
|
+
'newString': 'new_string',
|
|
195
|
+
'oldString': 'old_string',
|
|
196
|
+
'replaceAll': 'replace_all',
|
|
197
|
+
},
|
|
198
|
+
'patch': {
|
|
199
|
+
'filePath': 'file_path',
|
|
200
|
+
'patchText': 'content',
|
|
201
|
+
},
|
|
202
|
+
'write': {
|
|
203
|
+
'content': 'content',
|
|
204
|
+
'filePath': 'file_path',
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
read_only_tools=frozenset({'glob', 'grep', 'list', 'read', 'task', 'todoread', 'todowrite', 'webfetch'}),
|
|
208
|
+
uncovered_write_tools=frozenset({'patch'}),
|
|
209
|
+
path_arg_keys=('filePath', 'file_path', 'path'))
|
|
@@ -28,7 +28,7 @@ from pactkit.generators.deployer import (
|
|
|
28
28
|
_deploy_rules,
|
|
29
29
|
_deploy_skills,
|
|
30
30
|
)
|
|
31
|
-
from
|
|
31
|
+
from pactkit_opencode import declarations
|
|
32
32
|
from pactkit.utils import atomic_write
|
|
33
33
|
|
|
34
34
|
# All canonical commands deploy. Sprint has a sequential capability fallback.
|
|
@@ -121,7 +121,16 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
121
121
|
registers, nor recommends external services)
|
|
122
122
|
"""
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
# SPEC-4.0-22 Step 6: this Host answers for itself. These were Core's
|
|
125
|
+
# transitional seed until now; `complete_registration` harvests them off the
|
|
126
|
+
# class, so the adapter no longer reads a table Core owns. `input_translation`
|
|
127
|
+
# rides its own lifecycle — it is installed after the registration exists.
|
|
128
|
+
profile = declarations.PROFILE
|
|
129
|
+
capabilities = declarations.CAPABILITIES
|
|
130
|
+
security = declarations.SECURITY
|
|
131
|
+
pretool_surface = declarations.PRETOOL_SURFACE
|
|
132
|
+
deployment_claim = declarations.DEPLOYMENT_CLAIM
|
|
133
|
+
input_translation = declarations.INPUT_TRANSLATION
|
|
125
134
|
|
|
126
135
|
def deploy(self, config=None, target=None):
|
|
127
136
|
opencode_root = Path(target) if target else Path.home() / ".config" / "opencode"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""OpenCode's enforcement channel, stated by its owner (SPEC-4.0-06 R3).
|
|
2
|
+
|
|
3
|
+
OpenCode's veto surface is a plugin hook (``tool.execute.before``, throwing
|
|
4
|
+
denies the call), not a PreToolUse hook. Unlike Codex's — and unlike the
|
|
5
|
+
classic host's — **Core does not install it**: ``_deploy_gate_plugin()``
|
|
6
|
+
writes it, globally, from this adapter. There is no project-relative config
|
|
7
|
+
file for Core to merge into, and the plugin lives outside the deploy root, so
|
|
8
|
+
Core can neither install nor verify it.
|
|
9
|
+
|
|
10
|
+
So the provider returns None. That is the registry's word for "this host has
|
|
11
|
+
no channel for Core to install": ``_selected_channels`` yields nothing for
|
|
12
|
+
opencode and ``deploy("all")`` skips it, both correct — the plugin is this
|
|
13
|
+
adapter's to deploy and always was.
|
|
14
|
+
|
|
15
|
+
What registering buys is the *other* reader. ``gate_matrix.host_channel_registered``
|
|
16
|
+
asks "did this host register?", and until this module existed the answer was
|
|
17
|
+
no, so ``gate_coverage("opencode")`` reported ``spec_guard`` and
|
|
18
|
+
``tamper_guard`` as not enforced — while the plugin was on disk and blocking.
|
|
19
|
+
Measured 2026-09-22: those two gates flipped from ``not_enforced`` to
|
|
20
|
+
``enforced`` with the registration and nothing else, and no deploy behaviour
|
|
21
|
+
changed.
|
|
22
|
+
|
|
23
|
+
The looseness is the same one Codex already carries, accepted deliberately
|
|
24
|
+
(owner ruling 2026-09-22): registration is evidence that the *adapter* is
|
|
25
|
+
installed, not that the plugin reached disk. A project that installed this
|
|
26
|
+
adapter and never ran ``pactkit init --format opencode`` is reported as
|
|
27
|
+
covered. Making that claim verifiable needs a channel shape for
|
|
28
|
+
adapter-installed plugins; it is not this module's job to fake it with a
|
|
29
|
+
``HookChannel`` whose ``relative_path`` would make Core write a
|
|
30
|
+
project-relative file for a channel that is global by design.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from pactkit.commit_gate import HookChannel, register_enforcement_channel
|
|
38
|
+
|
|
39
|
+
#: MUST equal the string `gate_matrix.channels_for("opencode")` advertises, or
|
|
40
|
+
#: the coverage notice names a channel that is not the one installed — the
|
|
41
|
+
#: divergence SPEC-4.0-06 R4 records as `HookChannel`'s standing tension.
|
|
42
|
+
#: There is no `HookChannel` here to carry it (Core installs nothing), so the
|
|
43
|
+
#: agreement is asserted by `test_opencode_enforcement_channel.py` instead of
|
|
44
|
+
#: being structural.
|
|
45
|
+
ADVERTISED_IDENTITY = "tool.execute.before plugin (veto by throwing)"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def opencode_channel(root: Path) -> HookChannel | None:
|
|
49
|
+
"""None: Core installs nothing for this host, and that is the whole point.
|
|
50
|
+
|
|
51
|
+
Returning a `HookChannel` would send Core's installer after a
|
|
52
|
+
project-relative file for a channel that is global by design. A None
|
|
53
|
+
provider is a registered host whose channel Core does not own — distinct
|
|
54
|
+
from a host that never registered at all.
|
|
55
|
+
"""
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
register_enforcement_channel("opencode", opencode_channel)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
3
|
src/pactkit_opencode/__init__.py
|
|
4
|
+
src/pactkit_opencode/declarations.py
|
|
4
5
|
src/pactkit_opencode/deployer.py
|
|
6
|
+
src/pactkit_opencode/enforcement.py
|
|
5
7
|
src/pactkit_opencode.egg-info/PKG-INFO
|
|
6
8
|
src/pactkit_opencode.egg-info/SOURCES.txt
|
|
7
9
|
src/pactkit_opencode.egg-info/dependency_links.txt
|
|
@@ -14,6 +16,8 @@ tests/test_bug035_opencode_dual_layer.py
|
|
|
14
16
|
tests/test_deploy_opencode_parity.py
|
|
15
17
|
tests/test_gate_plugin_deploy.py
|
|
16
18
|
tests/test_host_body_snapshot.py
|
|
19
|
+
tests/test_opencode_enforcement_channel.py
|
|
20
|
+
tests/test_step6_self_sufficiency.py
|
|
17
21
|
tests/test_story069_opencode_format.py
|
|
18
22
|
tests/test_story070_opencode_compliance.py
|
|
19
23
|
tests/test_story071_opencode_config_parity.py
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""OpenCode's enforcement channel is registered by its owner, not by Core.
|
|
2
|
+
|
|
3
|
+
OpenCode's veto surface is a plugin hook (`tool.execute.before`, throwing
|
|
4
|
+
denies the call). `_deploy_gate_plugin()` installs it, globally, from this
|
|
5
|
+
adapter — Core does not install it and cannot verify it, so the provider
|
|
6
|
+
returns None and `_selected_channels` yields nothing.
|
|
7
|
+
|
|
8
|
+
That makes this the one host whose channel is registered but not installable
|
|
9
|
+
by Core, and the distinction is the whole point of the file:
|
|
10
|
+
|
|
11
|
+
* registering is what makes `gate_matrix.host_channel_registered` answer yes,
|
|
12
|
+
which is what makes `gate_coverage("opencode")` credit `spec_guard` and
|
|
13
|
+
`tamper_guard` — measured 2026-09-22, those two flipped from `not_enforced`
|
|
14
|
+
to `enforced` with this registration and nothing else;
|
|
15
|
+
* it must NOT make Core write anything, because the plugin is already this
|
|
16
|
+
adapter's to deploy and is global by design.
|
|
17
|
+
|
|
18
|
+
The looseness carried here is the same one Codex already has, accepted
|
|
19
|
+
deliberately (owner ruling 2026-09-22): registration is evidence that the
|
|
20
|
+
*adapter* is installed, not that the plugin reached disk.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from pactkit import commit_gate, gate_matrix
|
|
26
|
+
from pactkit_opencode.enforcement import ADVERTISED_IDENTITY, opencode_channel
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def gate_coverage_for(fmt: str) -> dict:
|
|
30
|
+
"""The default (measured) path — `has_git=False` so only the host layer moves."""
|
|
31
|
+
from pactkit.gate_matrix import gate_coverage
|
|
32
|
+
|
|
33
|
+
return gate_coverage(fmt, has_git=False)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestRegistration:
|
|
37
|
+
"""The registry, not the deploy summary, is the availability signal."""
|
|
38
|
+
|
|
39
|
+
def test_importing_the_adapter_registers_a_channel(self):
|
|
40
|
+
import pactkit_opencode # noqa: F401 — the import IS the registration
|
|
41
|
+
|
|
42
|
+
assert gate_matrix.host_channel_registered("opencode"), (
|
|
43
|
+
"importing this adapter registered no opencode enforcement channel — "
|
|
44
|
+
"Core will then answer 'no channel' for a host whose plugin is on "
|
|
45
|
+
"disk and blocking"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def test_the_registration_is_what_credits_the_host_gates(self):
|
|
49
|
+
"""The observable consequence, and the reason this module exists.
|
|
50
|
+
|
|
51
|
+
Before it, `gate_coverage("opencode")` reported `spec_guard` and
|
|
52
|
+
`tamper_guard` as not enforced. Both halves are asserted: the default
|
|
53
|
+
(measured) path credits them, and taking the registration away puts
|
|
54
|
+
them back — so the credit is coming from the registry, not from the
|
|
55
|
+
channel list alone.
|
|
56
|
+
"""
|
|
57
|
+
import pactkit_opencode # noqa: F401
|
|
58
|
+
|
|
59
|
+
measured = gate_coverage_for("opencode")
|
|
60
|
+
assert measured["channel_basis"] == "registered"
|
|
61
|
+
for gate in ("spec_guard", "tamper_guard"):
|
|
62
|
+
assert gate in measured["enforced"], (
|
|
63
|
+
f"{gate} is not credited although the adapter registered — this "
|
|
64
|
+
"is the display defect the registration fixes"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
saved = dict(commit_gate._ENFORCEMENT_CHANNELS)
|
|
68
|
+
try:
|
|
69
|
+
commit_gate._ENFORCEMENT_CHANNELS.pop("opencode")
|
|
70
|
+
gap = gate_coverage_for("opencode")
|
|
71
|
+
for gate in ("spec_guard", "tamper_guard"):
|
|
72
|
+
assert gate in gap["not_enforced"], gate
|
|
73
|
+
assert "not registered" in gap["not_enforced"]["tamper_guard"], (
|
|
74
|
+
"the reason must name the missing registration, not blame the "
|
|
75
|
+
"host — opencode does offer a pre-tool surface"
|
|
76
|
+
)
|
|
77
|
+
finally:
|
|
78
|
+
commit_gate._ENFORCEMENT_CHANNELS.clear()
|
|
79
|
+
commit_gate._ENFORCEMENT_CHANNELS.update(saved)
|
|
80
|
+
|
|
81
|
+
def test_core_still_installs_nothing_for_opencode(self, tmp_path):
|
|
82
|
+
"""The negative: registering must not send Core's installer anywhere.
|
|
83
|
+
|
|
84
|
+
The plugin is global and lives outside the deploy root; a HookChannel
|
|
85
|
+
here would make Core write a project-relative file for it.
|
|
86
|
+
"""
|
|
87
|
+
import pactkit_opencode # noqa: F401
|
|
88
|
+
|
|
89
|
+
assert commit_gate._selected_channels(tmp_path, "opencode") == [], (
|
|
90
|
+
"Core selected a channel to install for opencode — it owns none"
|
|
91
|
+
)
|
|
92
|
+
assert opencode_channel(tmp_path) is None
|
|
93
|
+
|
|
94
|
+
def test_all_does_not_pick_opencode_up(self, tmp_path):
|
|
95
|
+
"""`all` traverses the registry, and opencode contributes no channel."""
|
|
96
|
+
import pactkit_opencode # noqa: F401
|
|
97
|
+
|
|
98
|
+
identities = [
|
|
99
|
+
channel.identity
|
|
100
|
+
for channel in commit_gate._selected_channels(tmp_path, "all")
|
|
101
|
+
]
|
|
102
|
+
assert ADVERTISED_IDENTITY not in identities
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestTheAdvertisedName:
|
|
106
|
+
def test_the_identity_is_the_advertised_one(self):
|
|
107
|
+
"""R4's tension, asserted: `gate_matrix.channels_for` names this
|
|
108
|
+
channel in the coverage notice, and there is no `HookChannel` here to
|
|
109
|
+
carry the string — so nothing structural holds the two together."""
|
|
110
|
+
advertised = gate_matrix.channels_for("opencode", has_git=False)
|
|
111
|
+
assert advertised, "anti-vacuity: opencode advertises no channel at all"
|
|
112
|
+
assert ADVERTISED_IDENTITY in advertised, (
|
|
113
|
+
f"this module names the channel {ADVERTISED_IDENTITY!r} but the "
|
|
114
|
+
f"coverage notice advertises {advertised!r}"
|
|
115
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""SPEC-4.0-22 Step 6: this adapter answers for itself.
|
|
2
|
+
|
|
3
|
+
The claim under test is not "registration is complete" — it already was, because
|
|
4
|
+
Core's transitional seed filled the same members. The claim is that **every one
|
|
5
|
+
of them comes from the adapter**.
|
|
6
|
+
|
|
7
|
+
The probe removes Core's declaration for this Host and re-seeds, then imports
|
|
8
|
+
the adapter in that state. It does so **without naming Core's tables**: the
|
|
9
|
+
claim does not depend on whether they are called `_CORE_*` (everything
|
|
10
|
+
published up to 3.0.4) or `_BUILTIN_*` (Step 7, where they hold the builtin
|
|
11
|
+
alone). A probe that hardcodes either name is coupled to a Core version — and
|
|
12
|
+
adapter CI installs Core from PyPI, so it would be testing the version it
|
|
13
|
+
happens to pull rather than the adapter.
|
|
14
|
+
|
|
15
|
+
The assertions name **properties**, not literals from the seed — the seed is
|
|
16
|
+
gone, so quoting it would assert the thing being ruled out.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
HOST = "opencode"
|
|
25
|
+
MODULE = "pactkit_opencode"
|
|
26
|
+
|
|
27
|
+
#: Asserts Core declares nothing for this Host, then imports the adapter. Runs
|
|
28
|
+
#: in a fresh interpreter so the registry is exactly what `profiles` leaves
|
|
29
|
+
#: behind — no earlier import of this adapter can have registered it.
|
|
30
|
+
_UNSEED_TEMPLATE = '''
|
|
31
|
+
import sys
|
|
32
|
+
from pactkit import profiles as P
|
|
33
|
+
|
|
34
|
+
# Remove every trace of Core's declaration for this Host — by SHAPE, not by a
|
|
35
|
+
# hardcoded table name. Naming `_CORE_*` breaks on Step 7 core; naming
|
|
36
|
+
# `_BUILTIN_*` breaks on everything published before it. The claim under test
|
|
37
|
+
# does not depend on what the table is called, so the probe must not either.
|
|
38
|
+
removed = [n for n, v in list(vars(P).items())
|
|
39
|
+
if isinstance(v, dict) and HOST in v]
|
|
40
|
+
for n in removed:
|
|
41
|
+
vars(P)[n].pop(HOST, None)
|
|
42
|
+
|
|
43
|
+
# Re-seed through whatever this Core calls its seeder, so the registry is a
|
|
44
|
+
# consistent post-seed state rather than a half-emptied one.
|
|
45
|
+
for seeder in ("_seed_builtin", "_seed_from_core"):
|
|
46
|
+
fn = getattr(P, seeder, None)
|
|
47
|
+
if fn is not None:
|
|
48
|
+
fn()
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
# The precondition the import has to survive — and the guard against a probe
|
|
52
|
+
# that proves nothing. On Core 3.0.4 the removal above is what establishes it;
|
|
53
|
+
# on Step 7 Core it was already true, because there is no seed to remove. Both
|
|
54
|
+
# readings are the same claim, so this assertion is meaningful on both rather
|
|
55
|
+
# than demanding that something was removed (which Step 7 Core would fail for
|
|
56
|
+
# having done its job).
|
|
57
|
+
assert HOST not in P.FORMAT_PROFILES, (
|
|
58
|
+
"Core still carries this Host's profile when the adapter loads"
|
|
59
|
+
)
|
|
60
|
+
assert HOST not in P._ADAPTER_REGISTRY, (
|
|
61
|
+
"this Host is registered before its adapter has loaded"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
import MODULE # noqa: F401 — the import IS the assertion
|
|
65
|
+
|
|
66
|
+
from pactkit.profiles import get_registration
|
|
67
|
+
reg = get_registration(HOST)
|
|
68
|
+
assert reg.complete, "registration incomplete after an unseeded import: " + reg.state
|
|
69
|
+
assert reg.source == P.SOURCE_ENTRY_POINT, "source is " + reg.source
|
|
70
|
+
print("OK")
|
|
71
|
+
'''
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _code(import_adapter: bool = True) -> str:
|
|
75
|
+
"""The probe, optionally without the adapter import (the control).
|
|
76
|
+
|
|
77
|
+
The control substitution happens **before** the MODULE/HOST placeholders are
|
|
78
|
+
filled in. Doing it after silently matched nothing — the literal it looked
|
|
79
|
+
for no longer contained `MODULE` — and the control then ran the full probe
|
|
80
|
+
and passed for the wrong reason. Measured 2026-09-24.
|
|
81
|
+
"""
|
|
82
|
+
code = _UNSEED_TEMPLATE
|
|
83
|
+
if not import_adapter:
|
|
84
|
+
before = code
|
|
85
|
+
code = code.replace(
|
|
86
|
+
"import MODULE # noqa: F401 — the import IS the assertion",
|
|
87
|
+
"pass # control: do NOT import the adapter",
|
|
88
|
+
)
|
|
89
|
+
assert code != before, "the control substitution matched nothing"
|
|
90
|
+
return code.replace("HOST", repr(HOST)).replace("MODULE", MODULE)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _run(code: str) -> subprocess.CompletedProcess:
|
|
94
|
+
return subprocess.run(
|
|
95
|
+
[sys.executable, "-c", code], capture_output=True, text=True, timeout=180
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_the_adapter_stands_up_without_its_core_seed():
|
|
100
|
+
"""The unseeded import is the whole claim — it raised before Step 6."""
|
|
101
|
+
result = _run(_code())
|
|
102
|
+
assert result.returncode == 0, (
|
|
103
|
+
"the adapter could not stand up without Core's transitional seed — "
|
|
104
|
+
"which is exactly what Step 7 deletes:\n" + result.stderr[-2000:]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_the_probe_moves_when_the_adapter_is_not_imported():
|
|
109
|
+
"""Control: the probe's assertions must distinguish the two states.
|
|
110
|
+
|
|
111
|
+
A control that merely re-runs the probe with one of its own assertions
|
|
112
|
+
disabled proves nothing — it passes because the check was removed, not
|
|
113
|
+
because the probe is sound. (Measured: the first version of this control did
|
|
114
|
+
exactly that and passed for that reason.)
|
|
115
|
+
|
|
116
|
+
The discriminating question is whether the probe's assertions *move*. So
|
|
117
|
+
this runs the same removal **without importing the adapter**: now nothing
|
|
118
|
+
has registered opencode, and the probe must report that — a registration
|
|
119
|
+
that is absent or incomplete, and a host still counted as transitional.
|
|
120
|
+
"""
|
|
121
|
+
probe = _code(import_adapter=False)
|
|
122
|
+
result = _run(probe)
|
|
123
|
+
assert result.returncode != 0, (
|
|
124
|
+
"with no adapter imported, the probe still reported a complete "
|
|
125
|
+
"entry-point registration — so its assertions do not depend on the "
|
|
126
|
+
"adapter at all, and the main test is not measuring self-sufficiency"
|
|
127
|
+
)
|
|
128
|
+
assert "Traceback" in result.stderr, result.stderr[-800:]
|
|
File without changes
|
|
File without changes
|
{pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/src/pactkit_opencode.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/src/pactkit_opencode.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/src/pactkit_opencode.egg-info/requires.txt
RENAMED
|
File without changes
|
{pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/src/pactkit_opencode.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_story070_opencode_compliance.py
RENAMED
|
File without changes
|
{pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_story071_opencode_config_parity.py
RENAMED
|
File without changes
|
{pactkit_opencode-3.0.3 → pactkit_opencode-3.0.5}/tests/test_story073_command_model_routing.py
RENAMED
|
File without changes
|
|
File without changes
|