mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-10-31 03:51:58 +01:00
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:
31 files changed
+1830
-66
No files matched your search
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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\"" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user