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
8 changes: 7 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Nixpacks/Docker build context — do NOT ignore .mutiny (Docker cannot
# Docker build context — do NOT ignore .mutiny (Docker cannot
# re-include files under an excluded parent dir). Sample adapter must ship.
.env
.env.*
!.env.example
**/.env
**/.env.*
__pycache__/
*.py[cod]
*$py.class
Expand All @@ -26,5 +28,9 @@ node_modules/
.worktrees/
.vscode/
.idea/
.git/
.codex/
.agents/
.uv-cache/
apps/web/node_modules/
apps/web/.next/
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,57 @@ jobs:
- name: Build
run: npm run build

container-smoke:
name: container-smoke
runs-on: ubuntu-latest
timeout-minutes: 15
env:
MUTINY_API_TOKEN: container-ci-dummy-token
COMPOSE_PROJECT_NAME: mutiny-ci
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Build and start production images
run: docker compose up --build -d

- name: Check readiness and container boundaries
run: |
set -euo pipefail
for attempt in $(seq 1 30); do
if curl --fail --silent http://127.0.0.1:8000/api/health >/dev/null &&
curl --fail --silent http://127.0.0.1:3000 >/dev/null; then
break
fi
sleep 2
done
curl --fail http://127.0.0.1:8000/api/health
curl --fail --silent http://127.0.0.1:3000 >/dev/null
# Same-origin proxy must reach the API and inject the runtime token.
curl --fail http://127.0.0.1:3000/api/campaigns
docker compose exec -T api sh -c 'test "$(id -u)" = 10001 && test -w /app/data && test ! -w /app/.venv && test ! -w /app/examples'
docker compose exec -T web sh -c 'test "$(id -u)" != 0'
# The named volume is the only API mount; web has no host mounts.
test "$(docker inspect --format '{{len .Mounts}}' "$(docker compose ps -q api)")" = 1
test "$(docker inspect --format '{{(index .Mounts 0).Type}}' "$(docker compose ps -q api)")" = volume
test "$(docker inspect --format '{{len .Mounts}}' "$(docker compose ps -q web)")" = 0

- name: Confirm missing token fails before listen
run: |
set +e
timeout 15 docker compose run --rm --no-deps -e MUTINY_API_TOKEN= api
status=$?
test "$status" = 2

- name: Container logs
if: failure()
run: docker compose logs --no-color

- name: Clean up test containers and data
if: always()
run: docker compose down -v

package-build:
name: package-build
runs-on: ubuntu-latest
Expand Down
48 changes: 48 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# syntax=docker/dockerfile:1
FROM python:3.12-slim-bookworm AS api-build
COPY --from=ghcr.io/astral-sh/uv:0.12.23 /uv /usr/local/bin/uv
WORKDIR /app
ENV UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never
COPY pyproject.toml uv.lock ./
COPY packages ./packages
COPY apps/api ./apps/api
COPY apps/demo_agent ./apps/demo_agent
RUN uv sync --locked --no-dev --no-editable --package mutiny-api

FROM node:22-bookworm-slim AS web-build
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY apps/web/package.json apps/web/package-lock.json ./
RUN npm ci
COPY apps/web ./
# Next.js rewrites are baked into the build. Never pass the API token here.
ARG MUTINY_API_URL=http://api:8000
ENV MUTINY_API_URL=$MUTINY_API_URL
RUN npm run build

FROM node:22-bookworm-slim AS web
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 HOSTNAME=0.0.0.0 PORT=3000
COPY --from=web-build --chown=node:node /app/.next/standalone ./
COPY --from=web-build --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"]

# Keep API last: Railway builds this default target from the repository root.
FROM python:3.12-slim-bookworm AS api
WORKDIR /app
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 \
MUTINY_DB_PATH=/app/data/mutiny.sqlite
RUN groupadd --gid 10001 mutiny && useradd --uid 10001 --gid mutiny --no-create-home mutiny \
&& mkdir /app/data && chown mutiny:mutiny /app/data
COPY --from=api-build /app/.venv /app/.venv
COPY examples ./examples
COPY config ./config
COPY scripts/ensure_sample_mutiny.py ./scripts/ensure_sample_mutiny.py
# Preserve the Railway sample-ensure behavior, but prepare files during build.
RUN python scripts/ensure_sample_mutiny.py
USER 10001:10001
EXPOSE 8000
CMD ["python", "-m", "mutiny_api", "--host", "0.0.0.0", "--port", "8000"]
28 changes: 28 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,34 @@ mutiny db restore --from /secure/backups/mutiny-YYYYMMDD.sqlite --db /path/to/mu

**Not provided:** automated off-site replication, PostgreSQL, or Web `POST /api/db/*` endpoints.

## Container deployment (MUT-012 / #63)

Compose builds the API and production Next.js UI from the root Dockerfile.
Python dependencies are installed with `uv sync --locked` and JavaScript
dependencies with `npm ci` during image build. Restarts use the built virtual
environment / standalone server; no package installation occurs at startup.
Rebuild images to apply dependency or base-image updates. Lockfiles constrain
application dependencies; image tags and build tooling still require trusted
registries and a trusted build environment.

The API runs as UID/GID 10001 and the UI as the image's `node` user. Source and
dependencies in the API image are root-owned. Compose mounts only the named
`mutiny-data` volume at `/app/data`, with no host checkout bind mount. Both host
ports bind to `127.0.0.1`. The API still listens on `0.0.0.0` inside its container
and requires `MUTINY_API_TOKEN` through `python -m mutiny_api` before listening.

The UI proxies that shared operator token for its visitors. Keep it on a trusted
local machine: exposing port 3000 through a public proxy requires separate
access control. Non-root execution and restricted mounts reduce host exposure;
containers still share the host kernel and are not a sandbox for hostile agents.

Railway builds the default API image and uses its installed Python environment.
Its startup keeps the existing sample-ensure check; customer execution remains
removed. Configure a persistent volume at `/app/data` writable by UID/GID 10001
if durable storage is needed. New Docker named volumes inherit image directory
ownership; existing or externally provisioned volumes need permissions set by
the operator. See [COLD_START](./docs/COLD_START.md) for commands and backup notes.

## Reporting a vulnerability

Please **do not** open a public GitHub issue for security-sensitive reports.
Expand Down
1 change: 1 addition & 0 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { NextConfig } from "next";
const API = process.env.MUTINY_API_URL || "http://127.0.0.1:8000";

const nextConfig: NextConfig = {
output: "standalone",
async rewrites() {
return [
{
Expand Down
93 changes: 93 additions & 0 deletions apps/web/src/app/fonts/IBM-Plex-Mono-LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ThinItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLight.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Light.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-LightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Regular.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Italic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Medium.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-MediumItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Bold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-BoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved.

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
14 changes: 14 additions & 0 deletions apps/web/src/app/fonts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Bundled UI fonts

The UI loads Latin Rubik (variable, weights 400–800) and IBM Plex Mono
(weights 400, 500, 600) through `next/font/local`. Builds do not fetch Google
Fonts CSS or font files.

Unmodified WOFF2 files were extracted from these npm packages, with the archive
SHA-512 checked against the registry's `dist.integrity`:

- `@fontsource-variable/rubik@5.3.0`: `files/rubik-latin-wght-normal.woff2`
- `@fontsource/ibm-plex-mono@5.3.0`: `files/ibm-plex-mono-latin-{400,500,600}-normal.woff2`

Both fonts use the SIL Open Font License 1.1. Their copyright notices and
licenses are included beside the font files. Preserve these when updating fonts.
93 changes: 93 additions & 0 deletions apps/web/src/app/fonts/Rubik-LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik) Rubik-Italic[wght].ttf: Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Loading