Add text parameter type; verify commands run in the owning repo root
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 295952@bnt-lap001 Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
This commit is contained in:
parent
f790c3c11b
commit
7f8b55d036
5 changed files with 26 additions and 4 deletions
|
|
@ -26,7 +26,10 @@ under any root given on the command line.
|
|||
## Parameters
|
||||
|
||||
Each parameter has `id` (`[a-z][a-z0-9_]*`), `label`, `type`
|
||||
(`string|enum|path|bool|int`), optional `values`, `default`, `pattern`, `help`.
|
||||
(`string|text|enum|path|bool|int`), optional `values`, `default`, `pattern`, `help`.
|
||||
`text` is single-line free text for use inside double quotes: spaces are allowed and
|
||||
shell metacharacters (`; & | ` $ < > \ ' "`) are not. Every other type also rejects spaces.
|
||||
|
||||
**`secret` may only be `false`**: a parameter is never a secret. Steps refer to
|
||||
parameters as `{{id}}`. An undefined placeholder, an unused parameter, an
|
||||
`enum` without `values`, or a default that violates its `pattern`/`values` is a
|
||||
|
|
@ -48,6 +51,9 @@ enabled for that step), `risk` (`read-only|changes-state|attended|destructive`),
|
|||
A step with `risk` of `changes-state`, `attended` or `destructive` must state a
|
||||
`rollback` (which may say "not reversible" and why).
|
||||
|
||||
Commands are relative to the root of the repository that owns the pack. Verify
|
||||
commands run there, so a pack can call `make` targets and repo-relative scripts.
|
||||
|
||||
## Exercise status
|
||||
|
||||
Following the attended-procedure standard, a pack is `unexercised` until an
|
||||
|
|
|
|||
|
|
@ -120,7 +120,10 @@ def render(command: str, values: dict[str, str], pack: Pack) -> str:
|
|||
raise ValueError(f"parameter {name}: {val!r} not in {p['values']}")
|
||||
if p.get("pattern") and not re.fullmatch(p["pattern"], val):
|
||||
raise ValueError(f"parameter {name}: {val!r} violates pattern")
|
||||
if re.search(r"[;&|`$<>\n\\'\"\s]", val) and p.get("type") != "path":
|
||||
# "text" is single-line free text meant to sit inside double quotes:
|
||||
# spaces are allowed, shell metacharacters are not.
|
||||
bad = r"[;&|`$<>\n\\'\"]" if p.get("type") in ("text", "path") else r"[;&|`$<>\n\\'\"\s]"
|
||||
if re.search(bad, val):
|
||||
raise ValueError(f"parameter {name}: unsafe characters")
|
||||
return val
|
||||
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ def verify(run: Run, step_id: str, confirm: bool = False, manual_ok: bool | None
|
|||
t0 = time.monotonic()
|
||||
try:
|
||||
cp = subprocess.run(cmd, shell=True, capture_output=True, text=True,
|
||||
timeout=VERIFY_TIMEOUT)
|
||||
timeout=VERIFY_TIMEOUT, cwd=repo_root(run.pack))
|
||||
code, out = cp.returncode, cp.stdout + cp.stderr
|
||||
except subprocess.TimeoutExpired:
|
||||
code, out = -1, ""
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$"},
|
||||
"label": {"type": "string"},
|
||||
"type": {"enum": ["string", "enum", "path", "bool", "int"]},
|
||||
"type": {"enum": ["string", "text", "enum", "path", "bool", "int"]},
|
||||
"values": {"type": "array", "items": {"type": "string"}},
|
||||
"default": {"type": ["string", "integer", "boolean"]},
|
||||
"pattern": {"type": "string"},
|
||||
|
|
|
|||
|
|
@ -61,3 +61,16 @@ def test_render():
|
|||
assert P.render(cmd, {"host": "example.org", "count": "2"}, pk) == "ping -c 2 example.org"
|
||||
with pytest.raises(ValueError):
|
||||
P.render(cmd, {"host": "a;rm -rf /"}, pk)
|
||||
|
||||
|
||||
def test_text_parameter_allows_spaces_but_not_metacharacters():
|
||||
d = copy.deepcopy(GOOD)
|
||||
d["parameters"].append({"id": "need", "label": "Need", "type": "text", "default": "read a password"})
|
||||
d["steps"][0]["command"] = 'echo "{{need}}" {{host}} {{count}}'
|
||||
pk = P.Pack(Path("x"), d)
|
||||
assert P.render(d["steps"][0]["command"], {"host": "h"}, pk) == 'echo "read a password" h 1'
|
||||
with pytest.raises(ValueError):
|
||||
P.render(d["steps"][0]["command"], {"host": "h", "need": 'x"; rm -rf /'}, pk)
|
||||
with pytest.raises(ValueError):
|
||||
P.render(d["steps"][0]["command"], {"host": "two words"}, pk)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue