Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.git
.venv
venv
data
tests
*.key
.env
__pycache__
.pytest_cache
.mypy_cache
.ruff_cache
docs
examples
deploy
docker-compose.yml
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,35 @@ jobs:
run: .venv/bin/mypy src
- name: Pytest
run: .venv/bin/pytest -q

docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t poke-interconnect:ci .
- name: Validate compose
run: docker compose config -q
- name: Run container
run: >-
docker run -d --name pip-ci
-e PIP_ALLOW_INSECURE_KEY_PERMS=1
-p 127.0.0.1:8642:8642
poke-interconnect:ci
- name: Wait for health
run: |
for i in $(seq 1 30); do
curl -sf http://127.0.0.1:8642/healthz && break || sleep 1
done
for i in $(seq 1 30); do
s=$(docker inspect --format '{{.State.Health.Status}}' pip-ci)
echo "health: $s"; [ "$s" = healthy ] && exit 0; sleep 1
done
docker logs pip-ci; exit 1
- name: Check handshake
run: >-
curl -sf http://127.0.0.1:8642/.well-known/pip |
python3 -c 'import json,sys; d=json.load(sys.stdin); assert d["type"]=="handshake"'
- name: Cleanup
if: always()
run: docker logs pip-ci; docker rm -f pip-ci
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.env
config/policy.yaml
.venv/
venv/
data/
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 1.1.0

- Added `PIP_MCP_PORT` (default 8643) — MCP streamable-http now binds its own port.
- MCP streamable-http gains a `/healthz` endpoint exempt from the bearer gate.
- Production deployment: multi-stage `Dockerfile` (non-root, healthcheck),
`docker-compose.yml` (loopback-only ports, read-only fs, cap_drop),
`docker/entrypoint.sh`, `deploy/` (deploy.sh, Caddyfile, systemd units),
`docs/DEPLOYMENT.md`.
- `SqliteStore` sets a 5s busy timeout for multi-process WAL access.

## 1.0.0

Initial release of the Poke Interconnect Protocol (PIP) v1.
Expand Down
31 changes: 31 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# syntax=docker/dockerfile:1

FROM python:3.12-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
WORKDIR /app
COPY pyproject.toml README.md LICENSE ./
COPY src ./src
RUN uv venv /opt/venv && /opt/venv/bin/python -V \
&& uv pip install --python /opt/venv/bin/python .

FROM python:3.12-slim AS runtime
RUN groupadd -g 10001 pip && useradd -u 10001 -g pip -m pip
COPY --from=builder /opt/venv /opt/venv
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh \
&& mkdir -p /data /config && chown pip:pip /data
ENV PATH=/opt/venv/bin:$PATH \
PYTHONUNBUFFERED=1 \
PIP_PRIVATE_KEY_FILE=/data/instance.key \
PIP_POLICY_FILE=/config/policy.yaml \
PIP_STORE_URL=sqlite:////data/pip.db \
PIP_HTTP_HOST=0.0.0.0 \
PIP_HTTP_PORT=8642 \
PIP_MCP_PORT=8643
VOLUME ["/data"]
USER pip
EXPOSE 8642 8643
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["python", "-c", "import urllib.request,os,sys;sys.exit(0 if urllib.request.urlopen(f\"http://127.0.0.1:{os.environ.get('PIP_HEALTH_PORT','8642')}/healthz\",timeout=2).status==200 else 1)"]
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["serve-http"]
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,20 @@ Prompts: `pip_compose_message(to, subject, intent)`,
| `PIP_POLICY_FILE` | `./config/policy.yaml` | YAML or JSON |
| `PIP_STORE_URL` | `memory://` | or `sqlite:///data/pip.db` |
| `PIP_HTTP_HOST` / `PIP_HTTP_PORT` | `127.0.0.1` / `8642` | loopback by default |
| `PIP_MCP_PORT` | `8643` | streamable-http MCP port |
| `PIP_PUBLIC_HTTP_URL` / `PIP_PUBLIC_MCP_URL` | unset | advertised endpoints |
| `PIP_HTTP_BEARER_TOKEN` | unset | optional transport gate |
| `PIP_MAX_CLOCK_SKEW_SECONDS` | `300` | |
| `PIP_IDEMPOTENCY_TTL_SECONDS` | `86400` | |
| `PIP_LOG_LEVEL` | `INFO` | JSON structured logs |

## Deployment

Container image, compose stack, TLS proxy examples, and a systemd unit live in
`Dockerfile`, `docker-compose.yml`, `docker/`, and `deploy/`. See
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md); quick path:
`./deploy/deploy.sh up`.

## Security model (summary)

Ed25519 signatures over canonical JSON (`"PIPv1\n" + canonical_json(env-sig)`);
Expand Down
5 changes: 4 additions & 1 deletion config/.env.example
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# Example PIP environment configuration (spec §12). No real secrets here.
# Copy to .env (for docker compose) or export individually.
PIP_PRIVATE_KEY_FILE=./data/instance.key
PIP_DISPLAY_NAME=poke
PIP_POLICY_FILE=./config/policy.yaml
PIP_STORE_URL=memory://
# PIP_STORE_URL=sqlite:///data/pip.db
PIP_HTTP_HOST=127.0.0.1
PIP_HTTP_PORT=8642
PIP_MCP_PORT=8643
# PIP_PUBLIC_HTTP_URL=https://poke.example.com
# PIP_PUBLIC_MCP_URL=
# PIP_HTTP_BEARER_TOKEN=
# optional defense-in-depth transport gate; generate with: openssl rand -hex 32
PIP_HTTP_BEARER_TOKEN=
PIP_MAX_CLOCK_SKEW_SECONDS=300
PIP_IDEMPOTENCY_TTL_SECONDS=86400
PIP_LOG_LEVEL=INFO
Expand Down
11 changes: 11 additions & 0 deletions deploy/Caddyfile.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Example Caddyfile: TLS reverse proxy in front of PIP.
# PIP binds its ports to loopback only; terminate TLS here.
# Set PIP_PUBLIC_HTTP_URL / PIP_PUBLIC_MCP_URL to the public https URLs below.

poke.example.com {
reverse_proxy 127.0.0.1:8642
}

poke-mcp.example.com {
reverse_proxy 127.0.0.1:8643
}
54 changes: 54 additions & 0 deletions deploy/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Deploy helper for the PIP docker compose stack.
# Usage: deploy.sh [up|down|status|logs|keygen|identity]
set -euo pipefail

cd "$(dirname "$0")/.."
COMPOSE="docker compose"

wait_health() {
local port="$1" name="$2" i
for i in $(seq 1 30); do
if curl -sf "http://127.0.0.1:${port}/healthz" >/dev/null 2>&1; then
echo "${name}: healthy on :${port}"
return 0
fi
sleep 1
done
echo "${name}: NOT healthy on :${port} after 30s" >&2
return 1
}

case "${1:-up}" in
up)
if [ ! -f .env ]; then
cp config/.env.example .env
echo "warning: created .env from config/.env.example — EDIT IT before exposing publicly" >&2
fi
if [ ! -f config/policy.yaml ]; then
cp config/policy.example.yaml config/policy.yaml
echo "warning: created config/policy.yaml from the example — peers are placeholders" >&2
fi
$COMPOSE build
$COMPOSE up -d
wait_health 8642 pip-http
wait_health 8643 pip-mcp
;;
down)
$COMPOSE down
;;
status)
$COMPOSE ps
;;
logs)
shift || true
$COMPOSE logs -f "$@"
;;
keygen|identity)
$COMPOSE run --rm pip-http "$1"
;;
*)
echo "usage: $0 [up|down|status|logs|keygen|identity]" >&2
exit 2
;;
esac
18 changes: 18 additions & 0 deletions deploy/systemd/pip-mcp.service.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# /etc/systemd/system/pip-mcp.service — PIP MCP transport (streamable-http)
[Unit]
Description=PIP node (MCP streamable-http transport)
After=network.target

[Service]
User=pip
Group=pip
EnvironmentFile=/etc/pip-node/env
ExecStart=/opt/pip-node/.venv/bin/pip-node serve-mcp --transport streamable-http
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/lib/pip-node
PrivateTmp=true

[Install]
WantedBy=multi-user.target
18 changes: 18 additions & 0 deletions deploy/systemd/pip-node.service.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# /etc/systemd/system/pip-node.service — PIP HTTP transport
[Unit]
Description=PIP node (HTTP transport)
After=network.target

[Service]
User=pip
Group=pip
EnvironmentFile=/etc/pip-node/env
ExecStart=/opt/pip-node/.venv/bin/pip-node serve-http
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/lib/pip-node
PrivateTmp=true

[Install]
WantedBy=multi-user.target
54 changes: 54 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
services:
pip-http:
build: .
command: ["serve-http"]
ports:
- "127.0.0.1:8642:8642"
env_file:
- path: .env
required: false
environment:
# these must beat env_file defaults inside a container
PIP_HEALTH_PORT: "8642"
PIP_HTTP_HOST: "0.0.0.0"
PIP_STORE_URL: "sqlite:////data/pip.db"
volumes:
- pip-data:/data
- ./config/policy.yaml:/config/policy.yaml:ro
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL

pip-mcp:
build: .
command: ["serve-mcp"]
ports:
- "127.0.0.1:8643:8643"
env_file:
- path: .env
required: false
environment:
PIP_HEALTH_PORT: "8643"
PIP_HTTP_HOST: "0.0.0.0"
PIP_STORE_URL: "sqlite:////data/pip.db"
volumes:
# same volume → same identity key; sqlite WAL + busy timeout handle
# concurrent access from both processes
- pip-data:/data
- ./config/policy.yaml:/config/policy.yaml:ro
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL

volumes:
pip-data:
23 changes: 23 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# PIP container entrypoint. First arg selects the subcommand.
# Never echo env values — they may carry the bearer token.
set -euo pipefail

if [ ! -f "${PIP_POLICY_FILE:-/config/policy.yaml}" ]; then
echo "warning: ${PIP_POLICY_FILE:-/config/policy.yaml} not found; starting with empty policy" >&2
fi

cmd="${1:-serve-http}"
case "$cmd" in
serve-http)
exec pip-node serve-http
;;
serve-mcp)
# HEALTHCHECK reads PIP_HEALTH_PORT from the container env — set it
# per-service in compose (or -e PIP_HEALTH_PORT=8643 for docker run).
exec pip-node serve-mcp --transport streamable-http
;;
*)
exec pip-node "$@"
;;
esac
Loading
Loading