mirror of
https://scm.tikali.ai/tikali/applications/monky/monky-deployd.git
synced 2026-09-18 05:36:15 +00:00
1c42e913a8
Stdlib-only Python 3.12 agent for docker VMs and laptops: flock → checkin
(bearer = the agent's OpenBao token, bootstrapped from the install kit's
jwt-tenancy deploy grant) → action apply|none|down → bundle (sha256
verified) → refusal checks (unresolved ${VAR} names only, manifest paths
pinned to monky/data/<env>/see/, privileged/host-network, rollback, disk
need×1.5+headroom) → lease → POST /v1/auth/jwt-tenancy/login → KV reads →
.env 0600 → promote → compose pull/up → wait healthy → report; finally
renew-self / re-lease before max TTL, scrub. Exit 0/75/78/1. Redactor log
filter. Transports sdk (openziti) / proxy (ziti tunnel proxy 18443/18200) /
system. Laptop mode.
Packaging: hardened oneshot + 60 s timer + proxy unit, nfpm .deb with
/opt/monky-deployd/venv, install.sh for Ubuntu 26.04 (Gitea release
download, enrol, ACLs, bootstrap from stdin), ansible role skeleton for
osg1-07. CI: lint/test on every change; wheel (openziti on ubuntu:26.04) and
package (nfpm) allow_failure until runner egress is proven; GitLab release +
release:gitea on v* tags. Docs: README, PROTOCOL, OPERATIONS, CHANGELOG,
CLAUDE/AGENTS.
Divergence noted: monky-tenancy main (MR !15) still ships the AppRole lease
and kit; this agent implements the plan's Gate 1 RESULT (login_jwt, no
unwrap) and refuses an AppRole lease loudly (LEASE_SHAPE).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLB7jieMNRkTsJ2epr4Ds1
78 lines
2.6 KiB
Python
78 lines
2.6 KiB
Python
"""Log redaction: token shapes + every secret value the agent has seen.
|
|
|
|
The agent's journal must never carry a bearer, a deploy grant or a `.env` value. `Redactor`
|
|
is a `logging.Filter` installed on every handler; `add()` registers a value the moment it is
|
|
read from OpenBao (or minted by OpenBao), and `scrub()` is also applied to the `log_tail`
|
|
sent to tenancy. Secret NAMES are fine; values never."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
import threading
|
|
|
|
# Token / credential shapes redacted even before a value is known to us.
|
|
_SHAPES = [
|
|
re.compile(r"\bhv[sbr]\.[A-Za-z0-9_\-]{20,}"), # OpenBao/Vault service, batch, recovery
|
|
re.compile(r"\bs\.[A-Za-z0-9]{24,}\b"), # legacy vault token
|
|
re.compile(r"\beyJ[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}"), # JWT
|
|
re.compile(r"(?i)(authorization:\s*bearer\s+)\S+"),
|
|
re.compile(r"(?i)(x-vault-token:\s*)\S+"),
|
|
]
|
|
|
|
MASK = "[REDACTED]"
|
|
|
|
|
|
class Redactor(logging.Filter):
|
|
def __init__(self) -> None:
|
|
super().__init__("monky-deployd.redactor")
|
|
self._values: set[str] = set()
|
|
self._lock = threading.Lock()
|
|
|
|
def add(self, value: str | None) -> None:
|
|
"""Register a secret value (min 4 chars — shorter ones would shred the log)."""
|
|
if value and len(value) >= 4:
|
|
with self._lock:
|
|
self._values.add(value)
|
|
|
|
def forget_all(self) -> None:
|
|
with self._lock:
|
|
self._values.clear()
|
|
|
|
def scrub(self, text: str) -> str:
|
|
if not text:
|
|
return text
|
|
for pat in _SHAPES:
|
|
if pat.groups:
|
|
text = pat.sub(lambda m: m.group(1) + MASK, text)
|
|
else:
|
|
text = pat.sub(MASK, text)
|
|
with self._lock:
|
|
values = sorted(self._values, key=len, reverse=True)
|
|
for v in values:
|
|
if v in text:
|
|
text = text.replace(v, MASK)
|
|
return text
|
|
|
|
# logging.Filter
|
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
try:
|
|
msg = record.getMessage()
|
|
except Exception: # pragma: no cover - a broken format string is not our problem
|
|
msg = str(record.msg)
|
|
record.msg = self.scrub(msg)
|
|
record.args = ()
|
|
return True
|
|
|
|
|
|
REDACTOR = Redactor()
|
|
|
|
|
|
def install(logger: logging.Logger | None = None) -> Redactor:
|
|
"""Attach the singleton to every handler of `logger` (root by default)."""
|
|
logger = logger or logging.getLogger()
|
|
for h in logger.handlers:
|
|
if REDACTOR not in h.filters:
|
|
h.addFilter(REDACTOR)
|
|
return REDACTOR
|