Harden registry release preflight
This commit is contained in:
parent
9bedf70371
commit
64fc7339ef
|
|
@ -522,6 +522,17 @@ def _cmd_registry_publish(args):
|
||||||
if not name or not version:
|
if not name or not version:
|
||||||
print("Error: config.yaml must have 'name' and 'version' fields", file=sys.stderr)
|
print("Error: config.yaml must have 'name' and 'version' fields", file=sys.stderr)
|
||||||
return 1
|
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:
|
except yaml.YAMLError as e:
|
||||||
print(f"Error: Invalid YAML in config.yaml: {e}", file=sys.stderr)
|
print(f"Error: Invalid YAML in config.yaml: {e}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,14 @@ def _check_secrets(tool: Tool, report: PreflightReport):
|
||||||
if hasattr(step, "prompt"):
|
if hasattr(step, "prompt"):
|
||||||
prompt_lower = step.prompt.lower()
|
prompt_lower = step.prompt.lower()
|
||||||
for pat in secret_patterns:
|
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(
|
report.warnings.append(
|
||||||
f"Prompt step contains potential secret pattern '{pat}'"
|
f"Prompt step contains potential secret pattern '{pat}'"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -911,6 +911,38 @@ class TestRegistryPublishDryRun:
|
||||||
assert _cmd_registry_publish(self.args(tool_dir)) == 1
|
assert _cmd_registry_publish(self.args(tool_dir)) == 1
|
||||||
assert "Tool rejected" in capsys.readouterr().err
|
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):
|
def test_non_mapping_config_returns_failure(self, tool_dir, capsys):
|
||||||
from cmdforge.cli.registry_commands import _cmd_registry_publish
|
from cmdforge.cli.registry_commands import _cmd_registry_publish
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,22 @@ class TestAnalyzeTool:
|
||||||
_check_secrets(tool, report)
|
_check_secrets(tool, report)
|
||||||
assert any("secret" in w.lower() for w in report.warnings)
|
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"])
|
@pytest.mark.parametrize("version", ["1.2.3garbage", "1.2.3.4", "1.2"])
|
||||||
def test_semver_rejects_trailing_or_incomplete_values(self, version):
|
def test_semver_rejects_trailing_or_incomplete_values(self, version):
|
||||||
assert _is_semver(version) is False
|
assert _is_semver(version) is False
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue