web: fix chat (#800)

* chore: add guardrails

* docs: fix meshcore badge

* web: life chat message cap at 1000 in frontend

* web: honor protocol in chat

* web: deprioritize test channels

* fix ci
This commit is contained in:
l5y authored and GitHub committed 2026-06-19 16:37:41 +02:00
1 parent efd2c59fb8
commit bb95b0792d
31 files changed
+1830 -66

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
# Copyright © 2025-26 l5yth & contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""PreToolUse guard for Bash: deny destructive actions, confirm risky ones.
Enforces the risky-action policy from the Phase 2 environment audit so that
destructive git, recursive deletion, publishing, secret reads, and external
network egress cannot run by accident.
"""
import json
import re
import sys
def decision(kind, reason):
"""Emit a PreToolUse permission decision (``deny`` or ``ask``) and exit."""
print(
json.dumps(
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": kind,
"permissionDecisionReason": reason,
}
}
)
)
sys.exit(0)
DENY = [
(
re.compile(r"\bgit\s+push\b[^\n]*(--force\b|--force-with-lease\b|(?<!\w)-f\b)"),
"Force-push denied (history rewrite). Push without --force, or run it manually.",
),
(
re.compile(
r"\b(gem\s+push|npm\s+publish|cargo\s+publish|twine\s+upload"
r"|docker\s+push|flutter\s+pub\s+publish|gh\s+release\s+create)\b"
),
"Publishing/release denied — must be performed by a human.",
),
]
ASK = [
(
re.compile(
r"\bgit\s+(reset\s+--hard|clean\s+-[A-Za-z]*f|checkout\s+--\s"
r"|rebase\b|branch\s+-D\b|push\s+[^\n]*--delete\b)"
),
"Confirm: this git command discards or rewrites work.",
),
(re.compile(r"\bgit\s+push\b"), "Confirm before pushing to a remote."),
(
re.compile(r"(?:^|[\s=:'\"/])\.env\b(?!\.example)"),
"Confirm: reading/using a secrets file (.env).",
),
(
re.compile(r"\bkeyfile\b|web/\.config\b|\.sqlite\b|\bmesh\.db\b"),
"Confirm: accessing credentials or a database dump.",
),
]
NET = re.compile(r"\b(curl|wget|nc|ncat|telnet|scp|sftp|rsync)\b")
LOCAL = re.compile(r"127\.0\.0\.1|localhost|0\.0\.0\.0|::1")
RM, RM_R, RM_F = (
re.compile(r"\brm\b"),
re.compile(r"(?:^|\s)-[A-Za-z]*r|--recursive\b"),
re.compile(r"(?:^|\s)-[A-Za-z]*f|--force\b"),
)
def main():
"""Read the PreToolUse payload and gate the Bash command."""
try:
cmd = (json.load(sys.stdin).get("tool_input", {}) or {}).get(
"command", ""
) or ""
except Exception:
sys.exit(0)
if RM.search(cmd) and RM_R.search(cmd) and RM_F.search(cmd):
decision(
"deny",
"Recursive force-delete (rm -r -f) denied. Remove specific paths explicitly.",
)
for rx, why in DENY:
if rx.search(cmd):
decision("deny", why)
for rx, why in ASK:
if rx.search(cmd):
decision("ask", why)
if NET.search(cmd) and not LOCAL.search(cmd):
decision("ask", "Confirm: network egress to a non-local host.")
sys.exit(0)
if __name__ == "__main__":
main()
+80
View File
@@ -0,0 +1,80 @@
# Copyright © 2025-26 l5yth & contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""PreToolUse guard protecting the apex invariant (SPEC.md §1, ACCEPTANCE.md A1).
Blocks Edit/Write/MultiEdit calls that would add an MQTT or other message-broker
dependency to a package manifest. The Meshtastic ``via_mqtt`` provenance flag is
explicitly allowed and scrubbed before scanning.
"""
import json
import os
import re
import sys
# Dependency-declaration manifests that must never gain a broker client.
MANIFESTS = (
"Gemfile",
"requirements.txt",
"Cargo.toml",
"pubspec.yaml",
"package.json",
)
# "mqtt" et al. match as substrings so embedded crate/package names (e.g.
# ``rumqttc``, ``paho-mqtt``) are caught; generic ``broker`` stays word-bounded.
BROKER = re.compile(r"(?i)(mqtt|mosquitto|paho|amqp|kafka|rabbitmq|\bbroker\b)")
ALLOWED = re.compile(r"(?i)via_?mqtt")
def added_text(tool, ti):
"""Return the text a tool call would introduce into the file."""
if tool == "Write":
return ti.get("content", "")
if tool == "Edit":
return ti.get("new_string", "")
if tool == "MultiEdit":
return "\n".join(e.get("new_string", "") for e in ti.get("edits", []))
return ""
def main():
"""Read the PreToolUse payload and deny apex-violating manifest edits."""
try:
payload = json.load(sys.stdin)
except Exception:
sys.exit(0) # never break a tool call on a parse error
ti = payload.get("tool_input", {}) or {}
if os.path.basename(ti.get("file_path", "") or "") not in MANIFESTS:
sys.exit(0)
scrubbed = ALLOWED.sub("", added_text(payload.get("tool_name", ""), ti))
if BROKER.search(scrubbed):
print(
json.dumps(
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": (
"Blocked by the PotatoMesh apex invariant (SPEC.md §1): refusing to add "
"an MQTT/cloud-broker dependency. PotatoMesh is local-LoRa only."
),
}
}
)
)
sys.exit(0)
if __name__ == "__main__":
main()
+63
View File
@@ -0,0 +1,63 @@
{
"permissions": {
"allow": [
"Bash(bundle install)",
"Bash(bundle exec rspec:*)",
"Bash(bundle exec rufo:*)",
"Bash(npm ci)",
"Bash(npm test)",
"Bash(pytest:*)",
"Bash(python -m pytest:*)",
"Bash(python -m venv:*)",
"Bash(black:*)",
"Bash(cargo test:*)",
"Bash(cargo build:*)",
"Bash(cargo fmt:*)",
"Bash(cargo clippy:*)",
"Bash(cargo doc:*)",
"Bash(flutter test:*)",
"Bash(flutter analyze)",
"Bash(flutter pub get)",
"Bash(dart format:*)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git show:*)",
"Bash(git branch:*)",
"Bash(curl http://127.0.0.1:*)"
],
"deny": [
"Bash(git push --force:*)",
"Bash(git push -f:*)",
"Bash(git push --force-with-lease:*)",
"Bash(rm -rf:*)",
"Bash(rm -fr:*)",
"Bash(gem push:*)",
"Bash(npm publish:*)",
"Bash(cargo publish:*)",
"Bash(docker push:*)",
"Read(./.env)"
],
"ask": [
"Bash(git push:*)",
"Bash(git reset --hard:*)",
"Bash(git clean:*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-bash.py\"" }
]
},
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{ "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-edits.py\"" }
]
}
]
}
}