Skip to content

Commit 154b0bb

Browse files
OriNachumclaude
andcommitted
Address PR #37 review: harden gen-api-key.py + correct semver bump
Triage of Qodo findings on #37: - #1 (rule 796119): CHANGELOG `~/.model-gear` → `$HOME/.model-gear`. (The .py --help/docstring keep `~/.model-gear` to match the repo-wide CLI help text; Qodo's docs/config rule only flags the markdown.) - #2 (unreadable/dir .env crash): main() preflights that .env is a regular file and wraps the read/update/write in try/except OSError -> EXIT_ENV_ERROR, matching _read_key()'s graceful degradation. - #3 (under-scoped bump): a new documented capability is a minor, not a patch — 0.18.1 -> 0.19.0. - #4 (unhandled chmod): os.chmod is now best-effort (try/except OSError with a note), so a chmod-unsupported FS doesn't crash after a successful write. - #5 (unvalidated --bytes): reject `< 16` (128-bit floor) with a user error before generating, so no weak key or token_urlsafe stack trace. New tests: too-few-bytes and non-regular-file .env. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 257895a commit 154b0bb

5 files changed

Lines changed: 43 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ All notable changes to this project are documented here. The format is based on
44
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
55
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## [0.18.1] - 2026-06-09
7+
## [0.19.0] - 2026-06-09
88

99
### Added
1010

1111
- **`scripts/gen-api-key.py`** — generate or rotate the bearer key
1212
(`CULTURE_VLLM_API_KEY`) that gates the served API. The secret is created with
1313
the stdlib `secrets` module and **never hardcoded**, so the script is safe in the
1414
open-source repo; the key only ever lands in the gitignored deployment `.env`
15-
(written `0o600`). Hidden by default (no echo into logs/scrollback); `--show`
16-
prints it, `--force` rotates an existing key. Resolves the deployment dir like
17-
the `model` CLI (`--dir``$MODEL_GEAR_DIR``~/.model-gear`). Referenced from
18-
the README "Expose the API" section.
15+
(written `0o600`, best-effort). Hidden by default (no echo into logs/scrollback);
16+
`--show` prints it, `--force` rotates an existing key, and `--bytes` (min 16) is
17+
validated. Resolves the deployment dir like the `model` CLI (`--dir`
18+
`$MODEL_GEAR_DIR``$HOME/.model-gear`), degrades gracefully on an unreadable or
19+
non-regular `.env`, and runs from a wheel install (no `model_gear` import).
20+
Referenced from the README "Expose the API" section.
1921

2022
## [0.18.0] - 2026-06-09
2123

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "model-gear"
3-
version = "0.18.1"
3+
version = "0.19.0"
44
description = "model-gear — run, assess, and switch the local vLLM model."
55
readme = "README.md"
66
license = "MIT"

scripts/gen-api-key.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
KEY = "CULTURE_VLLM_API_KEY"
3333
PREFIX = "mg-" # human-readable provenance marker; not a secret
34+
MIN_BYTES = 16 # 128-bit floor — below this the key is too weak to gate a public API
3435

3536

3637
def _deploy_dir(explicit: str | None) -> Path:
@@ -68,7 +69,11 @@ def _write_key(env_path: Path, value: str) -> None:
6869
if not seen:
6970
out.append(f"{KEY}={value}")
7071
env_path.write_text("\n".join(out) + "\n", encoding="utf-8")
71-
os.chmod(env_path, 0o600) # the .env holds a secret — keep it owner-only
72+
try:
73+
os.chmod(env_path, 0o600) # the .env holds a secret — keep it owner-only
74+
except OSError:
75+
# best-effort hardening; some filesystems / platforms don't support chmod
76+
print(f">> note: could not set 0o600 on {env_path} (left as-is)", file=sys.stderr)
7277

7378

7479
def main(argv: list[str] | None = None) -> int:
@@ -78,7 +83,10 @@ def main(argv: list[str] | None = None) -> int:
7883
parser.add_argument("--dir", help="Deployment dir (default: $MODEL_GEAR_DIR or ~/.model-gear).")
7984
parser.add_argument("--force", action="store_true", help="Rotate even if a key already exists.")
8085
parser.add_argument(
81-
"--bytes", type=int, default=32, help="Token entropy in bytes (default: 32)."
86+
"--bytes",
87+
type=int,
88+
default=32,
89+
help=f"Token entropy in bytes (default: 32, min: {MIN_BYTES}).",
8290
)
8391
parser.add_argument(
8492
"--show", action="store_true", help="Print the key on stdout (else hidden)."
@@ -93,6 +101,12 @@ def main(argv: list[str] | None = None) -> int:
93101
)
94102
print("hint: run 'model init --apply' to scaffold it first", file=sys.stderr)
95103
return 2
104+
if env_path.exists() and not env_path.is_file():
105+
print(f"error: {env_path} exists but is not a regular file", file=sys.stderr)
106+
return 2
107+
if args.bytes < MIN_BYTES:
108+
print(f"error: --bytes must be at least {MIN_BYTES} (got {args.bytes})", file=sys.stderr)
109+
return 1
96110

97111
existing = _read_key(env_path)
98112
if existing and not args.force:
@@ -101,7 +115,11 @@ def main(argv: list[str] | None = None) -> int:
101115
return 1
102116

103117
token = PREFIX + secrets.token_urlsafe(args.bytes)
104-
_write_key(env_path, token)
118+
try:
119+
_write_key(env_path, token)
120+
except OSError as exc:
121+
print(f"error: could not write {env_path}: {exc}", file=sys.stderr)
122+
return 2
105123

106124
verb = "rotated" if existing else "set"
107125
print(f">> {verb} {KEY} in {env_path}", file=sys.stderr)

tests/test_gen_api_key.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,16 @@ def test_env_perms_are_owner_only(tmp_path) -> None:
7272
env.write_text("", encoding="utf-8")
7373
gen.main(["--dir", str(tmp_path)])
7474
assert (env.stat().st_mode & 0o777) == 0o600
75+
76+
77+
def test_rejects_too_few_bytes(tmp_path, capsys) -> None:
78+
(tmp_path / ".env").write_text("", encoding="utf-8")
79+
assert gen.main(["--dir", str(tmp_path), "--bytes", "4"]) == 1
80+
assert "at least" in capsys.readouterr().err
81+
assert gen._read_key(tmp_path / ".env") is None # nothing written
82+
83+
84+
def test_non_regular_env_is_env_error(tmp_path, capsys) -> None:
85+
(tmp_path / ".env").mkdir() # a directory where a regular .env is expected
86+
assert gen.main(["--dir", str(tmp_path)]) == 2
87+
assert "not a regular file" in capsys.readouterr().err

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)