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
|
## Parameters
|
||||||
|
|
||||||
Each parameter has `id` (`[a-z][a-z0-9_]*`), `label`, `type`
|
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
|
**`secret` may only be `false`**: a parameter is never a secret. Steps refer to
|
||||||
parameters as `{{id}}`. An undefined placeholder, an unused parameter, an
|
parameters as `{{id}}`. An undefined placeholder, an unused parameter, an
|
||||||
`enum` without `values`, or a default that violates its `pattern`/`values` is a
|
`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
|
A step with `risk` of `changes-state`, `attended` or `destructive` must state a
|
||||||
`rollback` (which may say "not reversible" and why).
|
`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
|
## Exercise status
|
||||||
|
|
||||||
Following the attended-procedure standard, a pack is `unexercised` until an
|
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']}")
|
raise ValueError(f"parameter {name}: {val!r} not in {p['values']}")
|
||||||
if p.get("pattern") and not re.fullmatch(p["pattern"], val):
|
if p.get("pattern") and not re.fullmatch(p["pattern"], val):
|
||||||
raise ValueError(f"parameter {name}: {val!r} violates pattern")
|
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")
|
raise ValueError(f"parameter {name}: unsafe characters")
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ def verify(run: Run, step_id: str, confirm: bool = False, manual_ok: bool | None
|
||||||
t0 = time.monotonic()
|
t0 = time.monotonic()
|
||||||
try:
|
try:
|
||||||
cp = subprocess.run(cmd, shell=True, capture_output=True, text=True,
|
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
|
code, out = cp.returncode, cp.stdout + cp.stderr
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
code, out = -1, ""
|
code, out = -1, ""
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$"},
|
"id": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$"},
|
||||||
"label": {"type": "string"},
|
"label": {"type": "string"},
|
||||||
"type": {"enum": ["string", "enum", "path", "bool", "int"]},
|
"type": {"enum": ["string", "text", "enum", "path", "bool", "int"]},
|
||||||
"values": {"type": "array", "items": {"type": "string"}},
|
"values": {"type": "array", "items": {"type": "string"}},
|
||||||
"default": {"type": ["string", "integer", "boolean"]},
|
"default": {"type": ["string", "integer", "boolean"]},
|
||||||
"pattern": {"type": "string"},
|
"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"
|
assert P.render(cmd, {"host": "example.org", "count": "2"}, pk) == "ping -c 2 example.org"
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
P.render(cmd, {"host": "a;rm -rf /"}, pk)
|
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