Skip to content

fix: replace <PACK> placeholders, harden path traversal checks, strengthen empty-list tests#52

Merged
Nelson Spence (Fieldnote-Echo) merged 5 commits into
mainfrom
copilot/update-pack-variable-reference
Apr 27, 2026
Merged

fix: replace <PACK> placeholders, harden path traversal checks, strengthen empty-list tests#52
Nelson Spence (Fieldnote-Echo) merged 5 commits into
mainfrom
copilot/update-pack-variable-reference

Conversation

Copilot AI commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Several issues across the agent script, CLI validation, and tests: literal <PACK> placeholders in the pack-validator agent rendered commands non-functional; .. path components were not blocked in spec/project name validation; and empty-list tests for hooks/validations lacked subprocess mock assertions.

.claude/agents/pack-validator.md

  • Wrap the validation chain in a for PACK in $(git diff ...) loop — <PACK> was a literal string, making every nboot apply/diff call invalid
  • Update pack_snake=$(echo "$PACK" | ...) to use the loop variable
  • Pack-specific pytest now runs inside the per-pack loop (Codex review fix 9465690) so each changed pack is tested, not just the last one
  • Remove 2>/dev/null stderr suppression from the first pytest invocation

src/navi_bootstrap/cli.py

  • Spec name validation in render_cmd (the only command that constructs an output Path from spec.name when --out is omitted): replace exact {".", ".."} membership check with ".." in stripped_name substring check; add startswith(".") guard; apply consistent name.strip() normalization. apply/diff are unchanged because they take --target as a Click Path, no name→path conversion.
  • Project name validation in new (CLI arg becomes the output Path): add ".." in name substring check; apply name.strip() normalization — render and new now use identical rules.
  • Error message wording corrected (Copilot review fix 9465690) to actually describe the rules being enforced: "must not start with '.', must not contain '..' or path separators".
# Before — misses "foo..bar", "foo/../bar", "  .."
if not name or "/" in name or "\\" in name:  # render (spec.name)
if not name or "/" in name or "\\" in name or name.startswith("."):  # new

# After — consistent substring + strip across both sites
stripped_name = name.strip() if isinstance(name, str) else ""
if not stripped_name or "/" in stripped_name or "\\" in stripped_name \
        or ".." in stripped_name or stripped_name.startswith("."):

Tests

  • test_empty_hooks / test_empty_validations: add @patch + mock_run.assert_not_called() — previously the tests only checked the return value, not that no subprocess was spawned
  • test_detects_async_test_functions: add assert result["test_framework"] == "pytest" to cover the previously untested field

Review remediation log

  • 3eec6efruff format on the new validation block to clear lint + quality-gate
  • 9465690 — Codex P1 (pack tests outside loop) + Copilot (error wording mismatch)

Copilot AI requested review from Copilot and removed request for Copilot April 22, 2026 03:41
Copilot AI requested review from Copilot and removed request for Copilot April 22, 2026 03:45
Copilot AI changed the title [WIP] Fix placeholder in pack validation commands fix: replace <PACK> placeholders, harden path traversal checks, strengthen empty-list tests Apr 22, 2026
@project-navi-bot
Navi Bot (project-navi-bot) marked this pull request as ready for review April 22, 2026 04:09
Nelson Spence (Fieldnote-Echo) added a commit that referenced this pull request Apr 26, 2026
…i-line)

The single-line if chain in new() (added in this PR) exceeds ruff-format's
preferred wrapping. Auto-applying `uv run ruff format` to clear the
`lint` (ruff format --check) and `quality-gate` failures on PR #52.

No semantic change. 381 tests still pass, mypy strict clean, both
ruff check and ruff format clean.
Copilot AI review requested due to automatic review settings April 26, 2026 20:21
@Fieldnote-Echo

Copy link
Copy Markdown
Member

/review

@Fieldnote-Echo

Copy link
Copy Markdown
Member

Codex (@codex) review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a broken pack-validator agent script by replacing literal <PACK> placeholders, hardens CLI name validation against path traversal patterns, and strengthens a few tests to ensure no subprocesses run for empty hook/validation lists.

Changes:

  • Update .claude/agents/pack-validator.md to iterate over changed packs and use $PACK consistently; stop suppressing stderr for the primary pytest run.
  • Harden nboot render (spec name → output dir) and nboot new (project name → output dir) by stripping whitespace and rejecting separators, .., and leading-dot names.
  • Improve tests by asserting subprocess.run is not invoked for empty hooks/validations; add an assertion for test_framework in async test detection.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
.claude/agents/pack-validator.md Fix pack placeholder usage by looping over changed packs and using $PACK.
src/navi_bootstrap/cli.py Strengthen name/path validation for output directories derived from spec/project names.
tests/test_hooks.py Ensure empty-hooks path does not spawn subprocesses.
tests/test_validate.py Ensure empty-validations path does not spawn subprocesses.
tests/test_init.py Add assertion to cover test_framework output from test detection.
Comments suppressed due to low confidence (1)

src/navi_bootstrap/cli.py:496

  • stripped_name is used for output_dir, but the unstripped name is still passed into build_spec_for_new() (and influences spec["name"] and pkg_dir). This can create a project directory like "myproj" while generating a spec/package path based on " myproj ". Consider normalizing once and using stripped_name consistently for both the filesystem path and the generated spec.
    stripped_name = name.strip() if name else ""
    if (
        not stripped_name
        or "/" in stripped_name
        or "\\" in stripped_name
        or ".." in stripped_name
        or stripped_name.startswith(".")
    ):
        raise click.ClickException(
            f"Unsafe project name {name!r}. "
            "Names must not contain path separators, '..', or start with a dot."
        )
    output_dir = Path(stripped_name)
    if output_dir.exists():
        raise click.ClickException(
            f"Directory {name!r} already exists. nboot new is for greenfield projects only."
        )

    # Build spec from CLI args
    try:
        spec_data = build_spec_for_new(
            name,
            description=description,
            license_id=license_id,
            python_version=python_version,
            author_name=author,
        )

Comment thread .claude/agents/pack-validator.md Outdated
Comment on lines 29 to 35
3. Run the pack-specific test:

```bash
pack_snake=$(echo <PACK> | tr '-' '_')
uv run pytest tests/test_${pack_snake}_pack.py -v 2>/dev/null || \
pack_snake=$(echo "$PACK" | tr '-' '_')
uv run pytest tests/test_${pack_snake}_pack.py -v || \
uv run pytest tests/ -k "$pack_snake" -v
```

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Step 3 uses "$PACK" but, as written, it runs after the loop in step 2. In a shell this will only test the last pack (or fail if step 3 is run standalone). Consider moving the pack-specific test commands inside the same for PACK in ...; do ...; done loop, or adding a separate loop for step 3 so each changed pack gets tested deterministically.

Copilot uses AI. Check for mistakes.
Comment thread src/navi_bootstrap/cli.py Outdated
Comment on lines 129 to 130
"Name must be a non-empty single path segment without '.' or '..' components. "
"Use --out to specify an explicit output path."

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error text says "without '.' or '..' components", but the actual validation is stripped_name.startswith('.') and ".." in stripped_name (substring), which also rejects names like ".foo" and "foo..bar". Update the message to match the exact rules being enforced (e.g., “must not start with '.' and must not contain '..'”), or adjust the checks to match the wording.

Suggested change
"Name must be a non-empty single path segment without '.' or '..' components. "
"Use --out to specify an explicit output path."
"Name must be a non-empty single path segment, must not start with '.', "
"and must not contain '..'. Use --out to specify an explicit output path."

Copilot uses AI. Check for mistakes.
Comment thread src/navi_bootstrap/cli.py
Comment on lines 117 to 133
if out is None:
name = spec_data["name"]
if not name or "/" in name or "\\" in name:
stripped_name = name.strip() if isinstance(name, str) else ""
if (
not stripped_name
or "/" in stripped_name
or "\\" in stripped_name
or ".." in stripped_name
or stripped_name.startswith(".")
):
raise click.ClickException(
f"Unsafe spec name {name!r} cannot be used as output directory. "
"Name must be a non-empty single path segment without '.' or '..' components. "
"Use --out to specify an explicit output path."
)
output_dir = Path(name)
output_dir = Path(stripped_name)
else:

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions tightening spec-name validation for apply/diff, but the only spec-name-as-path validation appears under the render command (render_cmd). If apply/diff also need similar hardening, it looks missing; otherwise, the PR description should be updated to reference render instead.

Copilot uses AI. Check for mistakes.
@qodo-code-review

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Validation mismatch

The new spec/project name validation rejects any occurrence of .. anywhere in the string (not just path components), while the error message still describes blocking “'.' or '..' components”. Confirm this stricter behavior is intended (it will also reject names like foo..bar) and align messaging/requirements accordingly.

    name = spec_data["name"]
    stripped_name = name.strip() if isinstance(name, str) else ""
    if (
        not stripped_name
        or "/" in stripped_name
        or "\\" in stripped_name
        or ".." in stripped_name
        or stripped_name.startswith(".")
    ):
        raise click.ClickException(
            f"Unsafe spec name {name!r} cannot be used as output directory. "
            "Name must be a non-empty single path segment without '.' or '..' components. "
            "Use --out to specify an explicit output path."
        )
    output_dir = Path(stripped_name)
else:
Shell robustness

The for PACK in $(...) construction relies on command substitution and word-splitting; it can behave unexpectedly with unusual pack directory names and is less robust than a line-delimited read loop. Consider a safer pattern (e.g., piping to while IFS= read -r PACK) and ensure the loop handles the “no changed packs” case cleanly.

for PACK in $(git diff --name-only origin/main...HEAD | grep '^packs/' | cut -d'/' -f2 | sort -u); do
  uv run nboot validate --spec nboot-spec.json
  scratch=$(mktemp -d -t nboot-scratch-XXXX)
  uv run nboot new "$scratch"
  uv run nboot apply --spec nboot-spec.json --pack "$PACK" --target "$scratch"
  uv run nboot diff  --spec nboot-spec.json --pack "$PACK" --target "$scratch"
done

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3eec6ef804

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread .claude/agents/pack-validator.md Outdated
Comment on lines +32 to +33
pack_snake=$(echo "$PACK" | tr '-' '_')
uv run pytest tests/test_${pack_snake}_pack.py -v || \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run pack tests inside the per-pack validation loop

When multiple packs are changed, step 2 loops over each PACK, but step 3 executes only once after done, so pack_snake is derived from the last loop value and only that pack’s test runs. This can let regressions in earlier changed packs pass unnoticed because their tests/test_<pack>_pack.py (or fallback -k) never executes.

Useful? React with 👍 / 👎.

Nelson Spence (Fieldnote-Echo) added a commit that referenced this pull request Apr 26, 2026
1. [Codex P1 + Copilot] pack-validator.md: pack-specific pytest step
   was outside the per-pack `for PACK ... done` loop, so $PACK held
   only the LAST iteration's value and earlier packs' tests were
   silently skipped. A regression in earlier-changed packs would slip
   through the validator. Moved the test inside the same loop body
   and renumbered the cross-cutting tests as step 3.

2. [Copilot] cli.py render_cmd error message: previous wording
   ('without dot or dot-dot components') didn't match the actual
   checks (`startswith('.')` rejects '.foo'; `'..' in name`
   substring rejects 'foo..bar'). Reworded to match the rules:
   'must not start with .', 'must not contain .. or path separators'.

Skipped from the same review batch:
  - Copilot's note that PR description claims apply/diff hardening:
    only render and new construct Path-from-name; apply/diff take
    --target as a Click Path. PR description should be updated but
    code is correct.

381 tests still pass, mypy strict clean, ruff + format clean.
Copilot AI and others added 4 commits April 26, 2026 18:56
…ngthen empty-list tests

Agent-Logs-Url: https://github.com/Project-Navi/navi-bootstrap/sessions/604f9871-000e-4f48-b728-1c2a85fe6c4c

Co-authored-by: Fieldnote-Echo <202828230+Fieldnote-Echo@users.noreply.github.com>
…i-line)

The single-line if chain in new() (added in this PR) exceeds ruff-format's
preferred wrapping. Auto-applying `uv run ruff format` to clear the
`lint` (ruff format --check) and `quality-gate` failures on PR #52.

No semantic change. 381 tests still pass, mypy strict clean, both
ruff check and ruff format clean.
1. [Codex P1 + Copilot] pack-validator.md: pack-specific pytest step
   was outside the per-pack `for PACK ... done` loop, so $PACK held
   only the LAST iteration's value and earlier packs' tests were
   silently skipped. A regression in earlier-changed packs would slip
   through the validator. Moved the test inside the same loop body
   and renumbered the cross-cutting tests as step 3.

2. [Copilot] cli.py render_cmd error message: previous wording
   ('without dot or dot-dot components') didn't match the actual
   checks (`startswith('.')` rejects '.foo'; `'..' in name`
   substring rejects 'foo..bar'). Reworded to match the rules:
   'must not start with .', 'must not contain .. or path separators'.

Skipped from the same review batch:
  - Copilot's note that PR description claims apply/diff hardening:
    only render and new construct Path-from-name; apply/diff take
    --target as a Click Path. PR description should be updated but
    code is correct.

381 tests still pass, mypy strict clean, ruff + format clean.
Copilot AI review requested due to automatic review settings April 26, 2026 23:56
@Fieldnote-Echo
Nelson Spence (Fieldnote-Echo) force-pushed the copilot/update-pack-variable-reference branch from 9465690 to 765a9a3 Compare April 26, 2026 23:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comment thread src/navi_bootstrap/cli.py
Comment on lines +471 to +483
stripped_name = name.strip() if name else ""
if (
not stripped_name
or "/" in stripped_name
or "\\" in stripped_name
or ".." in stripped_name
or stripped_name.startswith(".")
):
raise click.ClickException(
f"Unsafe project name {name!r}. "
"Names must not contain path separators or start with a dot."
"Names must not contain path separators, '..', or start with a dot."
)
output_dir = Path(name)
output_dir = Path(stripped_name)

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new() strips whitespace into stripped_name for output_dir, but later uses the original name when building the spec (build_spec_for_new(name, ...)). If the user passes leading/trailing whitespace, the directory created and the spec’s internal name (and derived paths like src_dir) can diverge, potentially creating unexpected paths in the generated project. Consider normalizing once (e.g., replace name with stripped_name) and using the normalized value consistently for both output_dir and the spec construction.

Copilot uses AI. Check for mistakes.
Comment thread src/navi_bootstrap/cli.py
raise click.ClickException(
f"Unsafe project name {name!r}. "
"Names must not contain path separators or start with a dot."
"Names must not contain path separators, '..', or start with a dot."

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new() validation rejects empty/whitespace-only names (not stripped_name), but the error message only mentions path separators/".."/dot prefix. Consider updating the message to also reflect the non-empty requirement (and that surrounding whitespace is stripped) so users get an accurate failure reason.

Suggested change
"Names must not contain path separators, '..', or start with a dot."
"Surrounding whitespace is stripped, and the resulting name must not be empty, "
"contain path separators, contain '..', or start with a dot."

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +26
scratch=$(mktemp -d -t nboot-scratch-XXXX)
uv run nboot new "$scratch"

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation snippet creates an absolute scratch path via mktemp -d ... and then runs nboot new "$scratch", but nboot new rejects names containing path separators (see src/navi_bootstrap/cli.py:new), so this command chain will fail as written. Adjust the instructions to create a scratch parent directory and run nboot new <project-name> inside it (or otherwise ensure the name argument is a single path segment), then point --target at the created project directory.

Suggested change
scratch=$(mktemp -d -t nboot-scratch-XXXX)
uv run nboot new "$scratch"
scratch_parent=$(mktemp -d -t nboot-scratch-XXXX)
scratch_name=scratch
scratch="$scratch_parent/$scratch_name"
(
cd "$scratch_parent" &&
uv run nboot new "$scratch_name"
)

Copilot uses AI. Check for mistakes.
@Fieldnote-Echo
Nelson Spence (Fieldnote-Echo) merged commit 92ac6df into main Apr 27, 2026
15 checks passed
@Fieldnote-Echo
Nelson Spence (Fieldnote-Echo) deleted the copilot/update-pack-variable-reference branch April 27, 2026 01:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants