hexcli 2.12.0__tar.gz → 2.13.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hexcli-2.12.0 → hexcli-2.13.0}/CHANGELOG.md +21 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/PKG-INFO +1 -1
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/__init__.py +1 -1
- hexcli-2.13.0/hexcli/psparse.py +227 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/safety.py +60 -6
- {hexcli-2.12.0 → hexcli-2.13.0}/.gitignore +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/Hex CLI.cmd +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/LICENSE +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/README.md +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/agent.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/assets/hexcli.ico +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/assets/hexcli.png +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/cancel.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/chatlog.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/commands.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/compaction.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/config.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/diffview.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/distribution.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/doctor.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/editing.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/http_client.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/launcher.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/lineedit.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/llm.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/lockfile.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/markdown_stream.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/memory.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/network.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/parsing.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/paths.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/prompts.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/repl.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/sessions.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/setup_wizard.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/statusbar.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/stream_render.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/telemetry.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/tools.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/hexcli/ui.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/install.ps1 +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/launcher.py +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/pyproject.toml +0 -0
- {hexcli-2.12.0 → hexcli-2.13.0}/shellai.example.json +0 -0
|
@@ -4,6 +4,27 @@ Full evidence for every claim below — including the experiments that failed
|
|
|
4
4
|
lives in `docs/V2_PLAN.md` §14. Numbers are pass^k over repeated live runs on
|
|
5
5
|
the Hexagon NPU, not single-run anecdotes.
|
|
6
6
|
|
|
7
|
+
## 2.13.0 — 2026-09-17
|
|
8
|
+
|
|
9
|
+
- Commands are classified by the name the shell will really run, not by the
|
|
10
|
+
text as typed. PowerShell resolves aliases before executing, so the
|
|
11
|
+
pattern tiers were reading a different command from the one that ran:
|
|
12
|
+
`ri C:\data`, `rmdir C:\data`, `& ('Remove'+'-Item') C:\data` and
|
|
13
|
+
`sl C:\ ; ri *` all classified as *caution* and executed with **no
|
|
14
|
+
confirmation at all**. `hexcli/psparse.py` runs PowerShell's own parser
|
|
15
|
+
(`[Parser]::ParseInput`) in one long-lived `-NoProfile` process, resolves
|
|
16
|
+
each command name through `Get-Alias`, and hands the real names to the
|
|
17
|
+
existing tiers; resolving a name applies the existing policy rather than
|
|
18
|
+
inventing one, so `ri` is destructive because `Remove-Item` always was.
|
|
19
|
+
346 ms to start, 0.12 ms a parse after that, cached, and the helper exits
|
|
20
|
+
on EOF when the session does.
|
|
21
|
+
|
|
22
|
+
Measured before merging, over every command Hex CLI has actually been
|
|
23
|
+
asked to run — 353 commands, 119 distinct, from the owner's sessions and
|
|
24
|
+
every recorded eval run: **0 change tier**. The change cannot alter
|
|
25
|
+
behaviour on observed traffic; its whole effect is the four bypasses
|
|
26
|
+
above. Offline suites green, smoke 10/10 on a fresh server.
|
|
27
|
+
|
|
7
28
|
## 2.12.0 — 2026-09-17
|
|
8
29
|
|
|
9
30
|
Ten changes on one theme: the harness now checks that a turn did the kind of
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hexcli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.13.0
|
|
4
4
|
Summary: Local Hexagon NPU terminal agent for Snapdragon X Elite Windows ARM64
|
|
5
5
|
Project-URL: Homepage, https://github.com/NathanL15/Hex-CLI
|
|
6
6
|
Project-URL: Repository, https://github.com/NathanL15/Hex-CLI
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""hexcli.psparse — structural facts about a PowerShell command line.
|
|
3
|
+
|
|
4
|
+
The safety classifier matched patterns against raw text, which fails in both
|
|
5
|
+
directions (measured 2026-09-15): `ri C:\\data` and `clc notes.txt` ran with no
|
|
6
|
+
confirmation because the alias never appears in a pattern, while
|
|
7
|
+
`Write-Output 'this will erase nothing'` was called destructive because the
|
|
8
|
+
word sat inside a string. Both need to know which text is a command name and
|
|
9
|
+
which is an inert argument, and that needs a parser for the language.
|
|
10
|
+
|
|
11
|
+
PowerShell has one built in: `[Parser]::ParseInput`. This module runs a small
|
|
12
|
+
helper script inside ONE long-lived `pwsh -NoProfile` process, sends it a
|
|
13
|
+
command per line and reads back a JSON summary. The process starts lazily on
|
|
14
|
+
first use and is reused for the session; results are cached by command string,
|
|
15
|
+
so a repeated command costs nothing. Everything degrades to "unavailable",
|
|
16
|
+
and the caller then keeps its pattern verdict unchanged.
|
|
17
|
+
|
|
18
|
+
The agent runs commands with `-NoProfile`, so the only aliases that can exist
|
|
19
|
+
are PowerShell's built-ins; the helper resolves them from the live session's
|
|
20
|
+
own alias table rather than a table copied into Python.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import base64
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import queue
|
|
28
|
+
import shutil
|
|
29
|
+
import subprocess
|
|
30
|
+
import threading
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
|
|
33
|
+
# One command in, one JSON line out. Base64 keeps newlines and quoting out of
|
|
34
|
+
# the protocol. `Ping` is answered before anything is parsed, so the caller can
|
|
35
|
+
# tell "the helper is up" from "the helper is wedged".
|
|
36
|
+
_HELPER = r"""
|
|
37
|
+
$ErrorActionPreference = 'Stop'
|
|
38
|
+
$aliases = @{}
|
|
39
|
+
foreach ($a in Get-Alias) {
|
|
40
|
+
# Definition, not ResolvedCommandName: the latter is null for most aliases
|
|
41
|
+
# unless the target module is already loaded (measured 2026-09-15: 78 of
|
|
42
|
+
# 135 came back empty and the table was useless).
|
|
43
|
+
$d = $a.Definition
|
|
44
|
+
if ($d) { $aliases[$a.Name.ToLowerInvariant()] = $d.ToLowerInvariant() }
|
|
45
|
+
}
|
|
46
|
+
Write-Output 'READY'
|
|
47
|
+
while ($null -ne ($line = [Console]::In.ReadLine())) {
|
|
48
|
+
try {
|
|
49
|
+
$cmd = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($line))
|
|
50
|
+
$errors = $null
|
|
51
|
+
$ast = [System.Management.Automation.Language.Parser]::ParseInput($cmd, [ref]$null, [ref]$errors)
|
|
52
|
+
$names = New-Object System.Collections.ArrayList
|
|
53
|
+
$nonliteral = $false
|
|
54
|
+
foreach ($c in $ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.CommandAst] }, $true)) {
|
|
55
|
+
$n = $c.GetCommandName()
|
|
56
|
+
if ($null -eq $n) { $nonliteral = $true; continue }
|
|
57
|
+
$n = $n.ToLowerInvariant()
|
|
58
|
+
if ($aliases.ContainsKey($n)) { $n = $aliases[$n] }
|
|
59
|
+
[void]$names.Add($n)
|
|
60
|
+
}
|
|
61
|
+
$strings = New-Object System.Collections.ArrayList
|
|
62
|
+
foreach ($s in $ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true)) {
|
|
63
|
+
$p = $s.Parent
|
|
64
|
+
$isName = ($p -is [System.Management.Automation.Language.CommandAst]) -and ($p.CommandElements.Count -gt 0) -and ($p.CommandElements[0] -eq $s)
|
|
65
|
+
if (-not $isName) {
|
|
66
|
+
[void]$strings.Add(@($s.Extent.StartOffset, $s.Extent.EndOffset))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
$out = @{ ok = $true; names = @($names); nonliteral = $nonliteral; strings = @($strings) }
|
|
70
|
+
} catch {
|
|
71
|
+
$out = @{ ok = $false; names = @(); nonliteral = $false; strings = @() }
|
|
72
|
+
}
|
|
73
|
+
Write-Output ($out | ConvertTo-Json -Compress -Depth 4)
|
|
74
|
+
}
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
_START_TIMEOUT_S = 10.0
|
|
78
|
+
_PARSE_TIMEOUT_S = 5.0
|
|
79
|
+
_CACHE_MAX = 512
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class Facts:
|
|
84
|
+
"""What the parser saw. `ok` False means nothing was learned and the
|
|
85
|
+
caller must keep whatever verdict it already had."""
|
|
86
|
+
ok: bool = False
|
|
87
|
+
names: tuple[str, ...] = () # canonical command names, aliases resolved
|
|
88
|
+
nonliteral: bool = False # a command whose name is not a literal
|
|
89
|
+
strings: tuple[tuple[int, int], ...] = field(default=()) # inert string-literal extents
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
UNAVAILABLE = Facts()
|
|
93
|
+
|
|
94
|
+
_lock = threading.Lock()
|
|
95
|
+
_proc: subprocess.Popen[str] | None = None
|
|
96
|
+
_replies: queue.Queue[str] | None = None
|
|
97
|
+
_disabled = False
|
|
98
|
+
_cache: dict[str, Facts] = {}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _reader(proc: subprocess.Popen[str], q: queue.Queue[str]) -> None:
|
|
102
|
+
assert proc.stdout is not None
|
|
103
|
+
for line in iter(proc.stdout.readline, ""):
|
|
104
|
+
q.put(line.strip())
|
|
105
|
+
q.put("")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _shell_exe() -> str | None:
|
|
109
|
+
for candidate in ("pwsh.exe", "powershell.exe", "pwsh"):
|
|
110
|
+
found = shutil.which(candidate)
|
|
111
|
+
if found:
|
|
112
|
+
return found
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _start() -> bool:
|
|
117
|
+
"""Bring the helper up. False (and disabled for good) when it cannot be."""
|
|
118
|
+
global _proc, _replies, _disabled
|
|
119
|
+
if _disabled:
|
|
120
|
+
return False
|
|
121
|
+
if _proc is not None and _proc.poll() is None:
|
|
122
|
+
return True
|
|
123
|
+
exe = _shell_exe()
|
|
124
|
+
if exe is None:
|
|
125
|
+
_disabled = True
|
|
126
|
+
return False
|
|
127
|
+
try:
|
|
128
|
+
env = dict(os.environ, POWERSHELL_TELEMETRY_OPTOUT="1")
|
|
129
|
+
proc = subprocess.Popen(
|
|
130
|
+
[exe, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "-"],
|
|
131
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
132
|
+
text=True, encoding="utf-8", errors="replace", env=env,
|
|
133
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
134
|
+
)
|
|
135
|
+
q: queue.Queue[str] = queue.Queue()
|
|
136
|
+
threading.Thread(target=_reader, args=(proc, q), daemon=True).start()
|
|
137
|
+
assert proc.stdin is not None
|
|
138
|
+
proc.stdin.write(_HELPER + "\n")
|
|
139
|
+
proc.stdin.flush()
|
|
140
|
+
if q.get(timeout=_START_TIMEOUT_S) != "READY":
|
|
141
|
+
raise RuntimeError("helper did not announce itself")
|
|
142
|
+
except Exception: # noqa: BLE001 — no parser is a supported state, never an error
|
|
143
|
+
_disabled = True
|
|
144
|
+
_proc = None
|
|
145
|
+
return False
|
|
146
|
+
_proc, _replies = proc, q
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _ask(cmd: str) -> Facts:
|
|
151
|
+
global _proc, _disabled
|
|
152
|
+
if not _start():
|
|
153
|
+
return UNAVAILABLE
|
|
154
|
+
assert _proc is not None and _proc.stdin is not None and _replies is not None
|
|
155
|
+
try:
|
|
156
|
+
payload = base64.b64encode(cmd.encode("utf-8")).decode("ascii")
|
|
157
|
+
_proc.stdin.write(payload + "\n")
|
|
158
|
+
_proc.stdin.flush()
|
|
159
|
+
line = _replies.get(timeout=_PARSE_TIMEOUT_S)
|
|
160
|
+
if not line:
|
|
161
|
+
raise RuntimeError("helper closed")
|
|
162
|
+
data = json.loads(line)
|
|
163
|
+
except Exception: # noqa: BLE001 — a wedged helper must not wedge the agent
|
|
164
|
+
try:
|
|
165
|
+
if _proc is not None:
|
|
166
|
+
_proc.kill()
|
|
167
|
+
except Exception: # noqa: BLE001
|
|
168
|
+
pass
|
|
169
|
+
_proc = None
|
|
170
|
+
_disabled = True # one failure is enough; the patterns still hold
|
|
171
|
+
return UNAVAILABLE
|
|
172
|
+
if not data.get("ok"):
|
|
173
|
+
return UNAVAILABLE
|
|
174
|
+
strings = tuple((int(a), int(b)) for a, b in (data.get("strings") or []))
|
|
175
|
+
return Facts(ok=True,
|
|
176
|
+
names=tuple(str(n) for n in (data.get("names") or [])),
|
|
177
|
+
nonliteral=bool(data.get("nonliteral")),
|
|
178
|
+
strings=strings)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def facts(cmd: str) -> Facts:
|
|
182
|
+
"""Structural facts about `cmd`, or UNAVAILABLE. Cached by command string:
|
|
183
|
+
the parse costs a round trip once per distinct command, and an agent that
|
|
184
|
+
retries the same call pays it once."""
|
|
185
|
+
key = cmd.strip()
|
|
186
|
+
if not key:
|
|
187
|
+
return UNAVAILABLE
|
|
188
|
+
with _lock:
|
|
189
|
+
hit = _cache.get(key)
|
|
190
|
+
if hit is not None:
|
|
191
|
+
return hit
|
|
192
|
+
got = _ask(key)
|
|
193
|
+
if len(_cache) >= _CACHE_MAX:
|
|
194
|
+
_cache.clear()
|
|
195
|
+
_cache[key] = got
|
|
196
|
+
return got
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def shutdown() -> None:
|
|
200
|
+
"""Stop the helper. Tests call this; production does not need to.
|
|
201
|
+
|
|
202
|
+
The helper blocks on OUR stdin pipe, so when this process dies -- exit,
|
|
203
|
+
crash or taskkill alike -- the OS closes the write end, the helper reads
|
|
204
|
+
EOF and leaves. Verified 2026-09-17: a session that starts the helper and
|
|
205
|
+
exits without calling this leaves no PowerShell behind. Do not "fix" that
|
|
206
|
+
with an atexit hook; there is nothing to clean up, and an exit hook that
|
|
207
|
+
waits on a pipe is a way to hang a shutdown, not to speed one."""
|
|
208
|
+
global _proc
|
|
209
|
+
with _lock:
|
|
210
|
+
proc, _proc = _proc, None
|
|
211
|
+
_cache.clear()
|
|
212
|
+
if proc is not None:
|
|
213
|
+
try:
|
|
214
|
+
if proc.stdin is not None:
|
|
215
|
+
proc.stdin.close()
|
|
216
|
+
proc.wait(timeout=2)
|
|
217
|
+
except Exception: # noqa: BLE001
|
|
218
|
+
try:
|
|
219
|
+
proc.kill()
|
|
220
|
+
except Exception: # noqa: BLE001
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _reset_for_tests() -> None:
|
|
225
|
+
global _disabled
|
|
226
|
+
shutdown()
|
|
227
|
+
_disabled = False
|
|
@@ -7,6 +7,8 @@ import re
|
|
|
7
7
|
from datetime import UTC, datetime
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from . import psparse
|
|
11
|
+
|
|
10
12
|
# Patterns checked in order: first match wins. Destructive > safe > caution.
|
|
11
13
|
|
|
12
14
|
_DESTRUCTIVE: list[re.Pattern[str]] = [re.compile(p, re.IGNORECASE) for p in [
|
|
@@ -84,13 +86,19 @@ _SAFE: list[re.Pattern[str]] = [re.compile(p, re.IGNORECASE) for p in [
|
|
|
84
86
|
]]
|
|
85
87
|
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
"""Return 'safe', 'caution', 'sensitive', or 'destructive'.
|
|
89
|
+
_SEVERITY = {"safe": 0, "caution": 1, "sensitive": 2, "destructive": 3}
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"""
|
|
93
|
-
|
|
91
|
+
|
|
92
|
+
def _worse(a: str, b: str) -> str:
|
|
93
|
+
"""The more severe of two verdicts. An unknown or empty verdict ranks
|
|
94
|
+
below every real one, so "nothing to say about this name" can never
|
|
95
|
+
displace `safe` — it did while the default was `caution`'s rank, which
|
|
96
|
+
turned `Get-ChildItem` into an empty classification."""
|
|
97
|
+
return a if _SEVERITY.get(a, -1) >= _SEVERITY.get(b, -1) else b
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _classify_text(s: str) -> str:
|
|
101
|
+
"""The pattern tiers, matched against the command as written."""
|
|
94
102
|
for pat in _DESTRUCTIVE:
|
|
95
103
|
if pat.search(s):
|
|
96
104
|
return "destructive"
|
|
@@ -103,6 +111,52 @@ def classify_command(cmd: str) -> str:
|
|
|
103
111
|
return "caution"
|
|
104
112
|
|
|
105
113
|
|
|
114
|
+
def _tier_for_name(name: str) -> str:
|
|
115
|
+
"""What the tiers would say about a bare command name. Deriving this from
|
|
116
|
+
the same patterns is deliberate: resolving an alias must not invent a
|
|
117
|
+
policy, only apply the existing one to the name the shell will really
|
|
118
|
+
run. `ri` becomes `remove-item` and is destructive because
|
|
119
|
+
`Remove-Item` always was; `clc` becomes `clear-content` and stays
|
|
120
|
+
caution because `Clear-Content` is caution today."""
|
|
121
|
+
probe = name + " "
|
|
122
|
+
for pat in _DESTRUCTIVE:
|
|
123
|
+
if pat.search(probe):
|
|
124
|
+
return "destructive"
|
|
125
|
+
for pat in _SENSITIVE:
|
|
126
|
+
if pat.search(probe):
|
|
127
|
+
return "sensitive"
|
|
128
|
+
return ""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def classify_command(cmd: str) -> str:
|
|
132
|
+
"""Return 'safe', 'caution', 'sensitive', or 'destructive'.
|
|
133
|
+
|
|
134
|
+
Priority: destructive > sensitive > safe > caution. Sensitive must outrank
|
|
135
|
+
the safe list, or read-only cmdlet prefixes whitelist credential access.
|
|
136
|
+
|
|
137
|
+
Text alone is not enough. Measured 2026-09-15: `ri C:\\data`,
|
|
138
|
+
`rmdir C:\\data` and `$c='Remove-Item'; & ($a+$b)` all ran with no
|
|
139
|
+
confirmation, because an alias or a computed name never appears in a
|
|
140
|
+
pattern. `hexcli.psparse` resolves the command names the shell will
|
|
141
|
+
actually run (the agent's shell is `-NoProfile`, so only built-in
|
|
142
|
+
aliases exist) and reports a name that is not a literal at all. The
|
|
143
|
+
result can only ever be MORE severe than the text verdict: when the
|
|
144
|
+
parser is unavailable this is exactly the behaviour it always had.
|
|
145
|
+
"""
|
|
146
|
+
s = cmd.strip()
|
|
147
|
+
verdict = _classify_text(s)
|
|
148
|
+
facts = psparse.facts(s)
|
|
149
|
+
if not facts.ok:
|
|
150
|
+
return verdict
|
|
151
|
+
for name in facts.names:
|
|
152
|
+
verdict = _worse(verdict, _tier_for_name(name))
|
|
153
|
+
if facts.nonliteral:
|
|
154
|
+
# `& ($a + $b)` — the name is computed, so nothing can be checked
|
|
155
|
+
# about it before it runs. Confirm rather than assume.
|
|
156
|
+
verdict = _worse(verdict, "sensitive")
|
|
157
|
+
return verdict
|
|
158
|
+
|
|
159
|
+
|
|
106
160
|
def append_audit_log(
|
|
107
161
|
session_id: str | None,
|
|
108
162
|
classification: str,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|