fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
85 lines
2.6 KiB
Python
85 lines
2.6 KiB
Python
"""Normalize a command segment to its sticky-approval pattern.
|
|
|
|
Rule: head + first sub-token (when present), wildcard the rest.
|
|
``git checkout main -- file.txt`` -> ``git checkout *``
|
|
``npm install foo bar`` -> ``npm install *``
|
|
``ls -la /tmp`` -> ``ls *``
|
|
``cat /etc/passwd`` -> ``cat *``
|
|
|
|
Compound commands are normalized segment-by-segment and rejoined with
|
|
`` && ``, so a sticky only matches a command with the same normalized
|
|
segment sequence:
|
|
``ls /tmp && whoami`` -> ``ls * && whoami``
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shlex
|
|
from typing import Optional
|
|
|
|
from application.devices.splitter import split_command
|
|
|
|
|
|
# Commands whose first arg is a sub-command (so the pattern is "cmd subcmd *").
|
|
_TWO_WORD_HEADS = frozenset(
|
|
{
|
|
"git", "npm", "pnpm", "yarn", "pip", "uv", "poetry", "cargo",
|
|
"docker", "kubectl", "brew", "apt", "apt-get", "dnf", "yum",
|
|
"go", "rustup", "rustc", "make", "gradle", "mvn",
|
|
"systemctl", "service",
|
|
}
|
|
)
|
|
|
|
|
|
def normalize_segment(segment: str) -> str:
|
|
"""Reduce a segment to its sticky-pattern form.
|
|
|
|
Args:
|
|
segment: A single shell command segment (no compound connectors).
|
|
|
|
Returns:
|
|
Pattern like ``git checkout *``, ``ls *``, or just ``cat`` if the
|
|
original had no args. Empty string for empty input.
|
|
"""
|
|
if not segment or not segment.strip():
|
|
return ""
|
|
try:
|
|
tokens = shlex.split(segment.strip(), posix=True)
|
|
except ValueError:
|
|
tokens = segment.strip().split()
|
|
if not tokens:
|
|
return ""
|
|
|
|
head = tokens[0]
|
|
|
|
if head in _TWO_WORD_HEADS and len(tokens) >= 2:
|
|
subcmd = tokens[1]
|
|
if len(tokens) > 2:
|
|
return f"{head} {subcmd} *"
|
|
return f"{head} {subcmd}"
|
|
|
|
if len(tokens) > 1:
|
|
return f"{head} *"
|
|
return head
|
|
|
|
|
|
def normalize_command(command: str) -> Optional[str]:
|
|
"""Normalize an entire command into its joined sticky pattern.
|
|
|
|
Every segment is normalized and the results are joined with `` && ``,
|
|
so a compound command's sticky only matches a command with the same
|
|
normalized segment sequence (approving ``ls /tmp && whoami`` won't
|
|
auto-approve ``ls /tmp && rm /tmp/x``). Empty segments are skipped;
|
|
returns ``None`` if there are no non-empty segments.
|
|
"""
|
|
if not command:
|
|
return None
|
|
patterns = [
|
|
normalize_segment(segment)
|
|
for segment in split_command(command)
|
|
]
|
|
patterns = [p for p in patterns if p]
|
|
if not patterns:
|
|
return None
|
|
return " && ".join(patterns)
|