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:
parent
edfbb49022
commit
e517f2b81a
5 changed files with 216 additions and 2 deletions
15
deploy/railiance/Dockerfile
Normal file
15
deploy/railiance/Dockerfile
Normal 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"]
|
||||
53
deploy/railiance/entrypoint.sh
Normal file
53
deploy/railiance/entrypoint.sh
Normal 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
|
||||
105
deploy/railiance/manifests.yaml
Normal file
105
deploy/railiance/manifests.yaml
Normal 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
|
||||
35
examples/statute/mas.coulomb.social/.well-known/mas
Normal file
35
examples/statute/mas.coulomb.social/.well-known/mas
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue