Add railiance deploy manifests, mas.coulomb.social statute example, probe JSONL log + bind option

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1370019@bnt-lap001
Assistant-Session: d462dd63-16de-4189-ae70-b9cd6566bf87
This commit is contained in:
tegwick 2026-10-11 20:22:37 +02:00
parent edfbb49022
commit e517f2b81a
5 changed files with 216 additions and 2 deletions

View file

@ -0,0 +1,15 @@
# MAS live-test MX for mas.coulomb.social (MAS-WP-0003).
# Build from the repo root: docker build -f deploy/railiance/Dockerfile .
FROM debian:bookworm-slim
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
postfix python3 python3-jsonschema ssl-cert ca-certificates && rm -rf /var/lib/apt/lists/*
RUN useradd -r -s /usr/sbin/nologin mas
COPY mas_core /app/mas_core
COPY mas_gateway /app/mas_gateway
COPY examples /app/examples
COPY experiments/postfix-probe/capture.py /app/capture.py
COPY deploy/railiance/entrypoint.sh /entrypoint.sh
ENV PYTHONPATH=/app
EXPOSE 25 8080
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]
CMD ["mx"]

View file

@ -0,0 +1,53 @@
#!/bin/sh
# mx : Postfix -> pipe -> mas-gateway -> capture (JSONL evidence) -> ticket service
# statute : static /.well-known/mas only (the only thing exposed over HTTPS)
set -e
DOMAIN="${MAS_DOMAIN:-mas.coulomb.social}"
STATUTE_DIR="/app/examples/statute/$DOMAIN"
if [ "$1" = "statute" ]; then
exec python3 /app/examples/statute/serve.py "$STATUTE_DIR" --host 0.0.0.0 --port 8080
fi
cat > /usr/local/bin/mas-gateway <<SH
#!/bin/sh
exec env PYTHONPATH=/app python3 -m mas_gateway.main "\$@"
SH
chmod +x /usr/local/bin/mas-gateway
# Not an open relay: no relay_domains, mynetworks is loopback only, so the
# only accepted recipients are @$DOMAIN, all delivered to the mas pipe.
# Stock reject_unauth_destination refuses any local-part containing '!' or '%'
# (treated as sender-specified routing), which kills every P2 address, and an
# access-map OK is ignored for such routed addresses. So relay control is an
# explicit regexp: anything not ending in @$DOMAIN is refused, the rest is
# accepted. With swap_bangpath/percent_hack off and local_transport=mas, those
# addresses can only reach the mas pipe.
DOMAIN_RE=$(printf '%s' "$DOMAIN" | sed 's/\./\\./g')
printf '!/@%s$/ REJECT 5.7.1 Relay access denied\n' "$DOMAIN_RE" > /etc/postfix/mas_rcpt.regexp
postconf -e \
"maillog_file=/dev/stdout" "myhostname=$DOMAIN" "mydomain=$DOMAIN" \
"mydestination=$DOMAIN" "relay_domains=" "mynetworks=127.0.0.0/8" \
"smtpd_relay_restrictions=permit_mynetworks,check_recipient_access regexp:/etc/postfix/mas_rcpt.regexp,permit" \
"local_transport=mas" "local_recipient_maps=" "mas_destination_recipient_limit=1" \
"recipient_delimiter=+" "swap_bangpath=no" "allow_percent_hack=no" \
"disable_vrfy_command=yes" "smtpd_helo_required=yes" "smtpd_recipient_limit=20" \
"message_size_limit=10240000" "smtpd_banner=\$myhostname ESMTP MAS live test" \
"smtpd_tls_security_level=may" \
"smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem" \
"smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key" \
"inet_protocols=ipv4" "compatibility_level=3.6"
cat >> /etc/postfix/master.cf <<CF
mas unix - n n - - pipe
flags=Rq user=mas argv=/usr/local/bin/mas-gateway
--statute $STATUTE_DIR/.well-known/mas
pipe --app-url http://127.0.0.1:8024/mas-events --recipient \${recipient}
CF
mkdir -p /var/lib/mas && chown mas /var/lib/mas
# Replies stay in the outbox (no --smtp) until the subdomain has SPF/DKIM.
su -s /bin/sh mas -c "python3 /app/examples/ticket_service/ticket_service.py \
--db /var/lib/mas/tickets.sqlite3 --listen 127.0.0.1:8025" &
su -s /bin/sh mas -c "python3 /app/capture.py http://127.0.0.1:8025/mas-events \
/var/lib/mas/events.jsonl 127.0.0.1" &
exec postfix start-fg

View file

@ -0,0 +1,105 @@
# MAS live provider test (mail-address-semantics MAS-WP-0003).
# Founder go for A-D: 2026-10-08. Temporary: delete the namespace after the test.
# A namespace + MX pod B LoadBalancer :25 C statute-only ingress D no outbound mail
# IMAGE is replaced with the pushed digest at apply time.
apiVersion: v1
kind: Namespace
metadata:
name: mas
labels: {app.kubernetes.io/part-of: mail-address-semantics}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: mas-evidence, namespace: mas}
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-path
resources: {requests: {storage: 1Gi}}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mas-mx
namespace: mas
labels: {app.kubernetes.io/name: mas-mx}
spec:
replicas: 1
strategy: {type: Recreate}
selector: {matchLabels: {app.kubernetes.io/name: mas-mx}}
template:
metadata: {labels: {app.kubernetes.io/name: mas-mx}}
spec:
automountServiceAccountToken: false
containers:
- name: mx
image: IMAGE
args: [mx]
ports: [{name: smtp, containerPort: 25}]
volumeMounts: [{name: evidence, mountPath: /var/lib/mas}]
readinessProbe: {tcpSocket: {port: 25}, periodSeconds: 10}
resources:
requests: {cpu: 20m, memory: 64Mi}
limits: {memory: 256Mi}
volumes:
- name: evidence
persistentVolumeClaim: {claimName: mas-evidence}
---
apiVersion: v1
kind: Service
metadata: {name: mas-smtp, namespace: mas}
spec:
type: LoadBalancer
externalTrafficPolicy: Local # keep real client IPs in the Postfix log
selector: {app.kubernetes.io/name: mas-mx}
ports: [{name: smtp, port: 25, targetPort: 25, protocol: TCP}]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mas-statute
namespace: mas
labels: {app.kubernetes.io/name: mas-statute}
spec:
replicas: 1
selector: {matchLabels: {app.kubernetes.io/name: mas-statute}}
template:
metadata: {labels: {app.kubernetes.io/name: mas-statute}}
spec:
automountServiceAccountToken: false
securityContext: {runAsNonRoot: true, runAsUser: 65534}
containers:
- name: statute
image: IMAGE
args: [statute]
ports: [{name: http, containerPort: 8080}]
readinessProbe: {httpGet: {path: /.well-known/mas, port: 8080}}
resources:
requests: {cpu: 5m, memory: 32Mi}
limits: {memory: 64Mi}
---
apiVersion: v1
kind: Service
metadata: {name: mas-statute, namespace: mas}
spec:
selector: {app.kubernetes.io/name: mas-statute}
ports: [{name: http, port: 80, targetPort: 8080}]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mas-statute
namespace: mas
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
rules:
- host: mas.coulomb.social
http:
paths:
- path: /.well-known/mas
pathType: Exact
backend: {service: {name: mas-statute, port: {number: 80}}}
tls:
- hosts: [mas.coulomb.social]
secretName: mas-coulomb-social-tls

View file

@ -0,0 +1,35 @@
{
"mas_version": "0.1",
"namespace": "mas.coulomb.social",
"profiles": {
"p1": true,
"p2": true
},
"resource_types": {
"ticket": {
"enabled": true,
"description": "Customer-service ticket",
"identifier_policy": "non-guessable-preferred"
}
},
"interaction_modes": {
"callback": {
"enabled": true,
"description": "Continue by a voice callback"
}
},
"equivalence": {
"local_part_case": "exact",
"dot_folding": false
},
"addresses": {
"ticket": {
"kind": "function",
"description": "Ticket intake; '!MODE' requests a continuation mode"
},
"support": {
"kind": "function",
"description": "General support intake"
}
}
}

View file

@ -8,6 +8,8 @@ import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
UPSTREAM = sys.argv[1]
LOG = sys.argv[2] if len(sys.argv) > 2 else None # optional JSONL evidence file
BIND = sys.argv[3] if len(sys.argv) > 3 else "0.0.0.0"
EVENTS = []
@ -20,7 +22,11 @@ class H(BaseHTTPRequestHandler):
status, resp = r.status, r.read()
except urllib.error.HTTPError as e:
status, resp = e.code, e.read()
EVENTS.append({"event": json.loads(body), "app_status": status, "app": json.loads(resp or b"null")})
rec = {"event": json.loads(body), "app_status": status, "app": json.loads(resp or b"null")}
EVENTS.append(rec)
if LOG:
with open(LOG, "a") as f:
f.write(json.dumps(rec) + "\n")
self._send(status, resp)
def do_GET(self):
@ -37,4 +43,4 @@ class H(BaseHTTPRequestHandler):
pass
ThreadingHTTPServer(("0.0.0.0", 8024), H).serve_forever()
ThreadingHTTPServer((BIND, 8024), H).serve_forever()