diff --git a/.env.example b/.env.example index 0af5a660..d3e7a5b8 100644 --- a/.env.example +++ b/.env.example @@ -1,29 +1,49 @@ # ============================================================================= # Turnstone Environment Variables -# Copy to .env and adjust values for your deployment +# Copy to .env and adjust values for your deployment. +# +# Usage: +# Single node: docker compose --profile production up +# 10-node cluster: docker compose --profile cluster up # ============================================================================= # -- LLM Backend -------------------------------------------------------------- LLM_BASE_URL=http://host.docker.internal:8000/v1 -OPENAI_API_KEY=sk-... -# ANTHROPIC_API_KEY=sk-ant-... # Set instead for Anthropic provider -# TAVILY_API_KEY=tvly-... # For web search fallback (local models only) +OPENAI_API_KEY=dummy +# ANTHROPIC_API_KEY=sk-ant-...# Set instead of OPENAI_API_KEY for Anthropic +# TAVILY_API_KEY=tvly-... # Web search fallback (local models only) +# MODEL=# Override default model alias -# -- Database (production profile) -------------------------------------------- +# -- Authentication (required) ------------------------------------------------ +# Generate with: python -c "import secrets; print(secrets.token_hex(32))" +TURNSTONE_JWT_SECRET=changeme-to-32-bytes-of-hex + +# -- Database ------------------------------------------------------------------ +# Single-node default is SQLite (zero config). Set these for PostgreSQL: # DB_BACKEND=postgresql # POSTGRES_USER=turnstone # POSTGRES_PASSWORD=changeme # DATABASE_URL=postgresql+psycopg://turnstone:changeme@postgres:5432/turnstone -# -- Redis --------------------------------------------------------------------- -# REDIS_PASSWORD= -# REDIS_PORT=6379 - -# -- Authentication ------------------------------------------------------------ -# TURNSTONE_AUTH_ENABLED=true -# TURNSTONE_AUTH_TOKEN=your-secret-token -# TURNSTONE_JWT_SECRET=python -c "import secrets; print(secrets.token_hex(32))" - # -- Ports --------------------------------------------------------------------- # SERVER_PORT=8080 # CONSOLE_PORT=8090 + +# -- Workspace ----------------------------------------------------------------- +# Bind-mount a host directory into the container at /workspace. +# The model can read/write files here. Default: empty Docker volume. +# WORKSPACE_MOUNT=/path/to/your/project + +# -- Agent behavior ------------------------------------------------------------ +# SKIP_PERMISSIONS=true # Auto-approve all tool calls (dev only) +# MCP_CONFIG=/workspace/mcp.json# MCP server configuration file + +# -- Discord channel gateway --------------------------------------------------- +# TURNSTONE_DISCORD_TOKEN= +# TURNSTONE_DISCORD_GUILD=0 + +# -- Cluster (profile: cluster) ----------------------------------------------- +# These are set per-node in compose.yaml; only override for custom topologies. +# TURNSTONE_NODE_ID=node-1 +# TURNSTONE_ADVERTISE_URL=http://server-1:8080 + diff --git a/.github/renovate.json b/.github/renovate.json index 8b4e3147..6cee1175 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -83,7 +83,7 @@ { "description": "Infrastructure dependencies", "groupName": "Infrastructure", - "matchPackageNames": ["structlog", "redis", "croniter", "discord.py"], + "matchPackageNames": ["structlog", "croniter", "discord.py"], "schedule": ["before 9am on the first day of the month"], "automerge": true, "matchUpdateTypes": ["patch"] @@ -101,7 +101,6 @@ "matchPackageNames": [ "ruff", "mypy", - "types-redis", "pytest", "pytest-cov", "pre-commit" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e998c45e..551d4e61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,10 @@ name: CI on: push: - branches: [main] + branches: [main, "stable/*"] + tags: ["v*"] pull_request: - branches: [main] + branches: [main, "stable/*"] jobs: lint: @@ -25,8 +26,8 @@ jobs: - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: "3.14" - - run: pip install mypy types-redis - - run: pip install -e ".[mq]" + - run: pip install mypy + - run: pip install -e ".[all]" - run: mypy turnstone/ test: @@ -39,7 +40,7 @@ jobs: - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: ${{ matrix.python-version }} - - run: pip install -e ".[test,mq]" + - run: pip install -e ".[test]" - run: pytest tests/ -m "not live" --cov=turnstone --cov-report=term-missing --cov-report=xml -q - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 if: always() @@ -68,7 +69,7 @@ jobs: - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: "3.14" - - run: pip install -e ".[test,mq,postgres]" + - run: pip install -e ".[test,postgres]" - run: pytest tests/ -m "not live" --storage-backend=postgresql -q env: TURNSTONE_TEST_PG_URL: postgresql+psycopg://postgres:postgres@localhost:5432/turnstone_test diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..6f1eb5a8 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,75 @@ +name: Publish Docker Image + +on: + workflow_run: + workflows: ["CI"] + types: [completed] + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + docker: + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 0 + + - name: Resolve release tag + id: tag + run: | + TAG=$(git tag --points-at HEAD | grep '^v' | head -1) + if [ -z "$TAG" ]; then + echo "No v* tag at HEAD — skipping publish" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Log in to GHCR + if: steps.tag.outputs.skip == 'false' + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute Docker tags + if: steps.tag.outputs.skip == 'false' + id: tags + env: + REF: ${{ steps.tag.outputs.tag }} + run: | + VERSION="${REF#v}" + FULL="${REGISTRY}/${IMAGE_NAME}" + FULL="${FULL,,}" + + if echo "$VERSION" | grep -qE '(a|b|rc)[0-9]+$'; then + TAGS="${FULL}:${VERSION},${FULL}:experimental" + else + MINOR="${VERSION%.*}" + TAGS="${FULL}:${VERSION},${FULL}:${MINOR},${FULL}:stable,${FULL}:latest" + fi + echo "tags=${TAGS}" >> "$GITHUB_OUTPUT" + + - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 + if: steps.tag.outputs.skip == 'false' + + - name: Build and push + if: steps.tag.outputs.skip == 'false' + uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6 + with: + context: . + push: true + tags: ${{ steps.tags.outputs.tags }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/docker-scan.yml b/.github/workflows/docker-scan.yml index 4a1db50a..552b552b 100644 --- a/.github/workflows/docker-scan.yml +++ b/.github/workflows/docker-scan.yml @@ -2,7 +2,7 @@ name: Docker Security Scan on: push: - branches: [main] + branches: [main, "stable/*"] schedule: - cron: "0 6 * * 1" # Weekly Monday 06:00 UTC diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ce54cf00..30467dcf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,8 +1,9 @@ name: Publish to PyPI on: - push: - tags: ["v*"] + workflow_run: + workflows: ["CI"] + types: [completed] permissions: contents: write @@ -10,20 +11,43 @@ permissions: jobs: publish: + if: github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest environment: pypi steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 0 + + - name: Resolve release tag + id: tag + run: | + TAG=$(git tag --points-at HEAD | grep '^v' | head -1) + if [ -z "$TAG" ]; then + echo "No v* tag at HEAD — skipping publish" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + if: steps.tag.outputs.skip == 'false' with: python-version: "3.14" - run: pip install build + if: steps.tag.outputs.skip == 'false' - run: python -m build + if: steps.tag.outputs.skip == 'false' - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 + if: steps.tag.outputs.skip == 'false' - name: Create GitHub Release + if: steps.tag.outputs.skip == 'false' uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 with: + tag_name: ${{ steps.tag.outputs.tag }} generate_release_notes: true draft: false - prerelease: ${{ contains(github.ref, '-') }} + prerelease: ${{ contains(steps.tag.outputs.tag, 'a') || contains(steps.tag.outputs.tag, 'b') || contains(steps.tag.outputs.tag, 'rc') }} diff --git a/Dockerfile b/Dockerfile index cc8944b5..40a8c33f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,6 +55,9 @@ COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh WORKDIR /data RUN chown turnstone:turnstone /data +# Workspace mount point — bind-mount a host directory here +RUN mkdir -p /workspace && chown turnstone:turnstone /workspace + USER turnstone ENTRYPOINT ["entrypoint.sh"] diff --git a/README.md b/README.md index 50e37e35..696d6434 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,21 @@ Multi-node AI orchestration platform. Deploy tool-using AI agents across a cluster of servers with direct HTTP routing, interactive interfaces, and enterprise governance. -> **Beta — Use at your own risk.** APIs, configuration formats, and database schemas may change between versions without migration paths. +
+
+