Harden registry release preflight

This commit is contained in:
rob 2026-07-21 12:53:38 -03:00
parent 9bedf70371
commit 64fc7339ef
4 changed files with 67 additions and 1 deletions

View File

@ -522,6 +522,17 @@ def _cmd_registry_publish(args):
if not name or not version:
print("Error: config.yaml must have 'name' and 'version' fields", file=sys.stderr)
return 1
# Registry fields are local installation/status metadata. Including a
# previous release's hash or owner in a new payload makes content
# identity depend on stale registry state.
for key in (
"registry_hash",
"registry_status",
"registry_owner",
"registry_feedback",
):
data.pop(key, None)
config_yaml = yaml.dump(data, default_flow_style=False, sort_keys=False)
except yaml.YAMLError as e:
print(f"Error: Invalid YAML in config.yaml: {e}", file=sys.stderr)
return 1

View File

@ -207,7 +207,14 @@ def _check_secrets(tool: Tool, report: PreflightReport):
if hasattr(step, "prompt"):
prompt_lower = step.prompt.lower()
for pat in secret_patterns:
if pat in prompt_lower:
# Do not flag schema/field names such as max_tokens or
# token_count. Underscores are identifier characters here,
# even though regex \b would treat them inconsistently around
# compound names.
if re.search(
rf"(?<![a-z0-9_]){re.escape(pat)}(?![a-z0-9_])",
prompt_lower,
):
report.warnings.append(
f"Prompt step contains potential secret pattern '{pat}'"
)

View File

@ -911,6 +911,38 @@ class TestRegistryPublishDryRun:
assert _cmd_registry_publish(self.args(tool_dir)) == 1
assert "Tool rejected" in capsys.readouterr().err
def test_strips_local_registry_metadata_from_payload(
self, tool_dir, monkeypatch
):
import yaml
from cmdforge.cli.registry_commands import _cmd_registry_publish
config_path = tool_dir / "config.yaml"
config_path.write_text(
config_path.read_text()
+ "registry_hash: sha256:stale\n"
+ "registry_status: approved\n"
+ "registry_owner: previous-owner\n"
+ "registry_feedback: old feedback\n"
)
client = MagicMock()
client.publish_tool.return_value = {"preflight": {"errors": []}}
monkeypatch.setattr(
"cmdforge.cli.registry_commands.load_config",
lambda: SimpleNamespace(registry=SimpleNamespace(token="token")),
)
monkeypatch.setattr("cmdforge.registry_client.get_client", lambda: client)
assert _cmd_registry_publish(self.args(tool_dir)) == 0
payload = yaml.safe_load(client.publish_tool.call_args.args[0])
assert not {
"registry_hash",
"registry_status",
"registry_owner",
"registry_feedback",
}.intersection(payload)
def test_non_mapping_config_returns_failure(self, tool_dir, capsys):
from cmdforge.cli.registry_commands import _cmd_registry_publish

View File

@ -107,6 +107,22 @@ class TestAnalyzeTool:
_check_secrets(tool, report)
assert any("secret" in w.lower() for w in report.warnings)
def test_secret_pattern_does_not_match_compound_schema_fields(self):
from cmdforge.tool import PromptStep, Tool
tool = Tool(
name="schema-docs",
steps=[PromptStep(
prompt="Document max_tokens and token_count fields",
provider="mock",
output_var="out",
)],
)
report = PreflightReport()
from cmdforge.preflight import _check_secrets
_check_secrets(tool, report)
assert report.warnings == []
@pytest.mark.parametrize("version", ["1.2.3garbage", "1.2.3.4", "1.2"])
def test_semver_rejects_trailing_or_incomplete_values(self, version):
assert _is_semver(version) is False