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
4 changes: 4 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps php-example)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps mcp-fetcher)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps mcp-tests)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps py-dev-fetcher)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps py-analysis)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps composer-install)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps php-fetcher)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps php-analysis)",
Expand Down Expand Up @@ -75,6 +77,8 @@
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps php-example)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps mcp-fetcher)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps mcp-tests)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps py-dev-fetcher)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps py-analysis)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps composer-install)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps php-fetcher)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps php-analysis)",
Expand Down
11 changes: 9 additions & 2 deletions .claude/skills/check/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ docker compose -f docker/compose.yaml up -d worker frankenphp fault-worker hosti
docker compose -f docker/compose.yaml run --rm --no-deps php-integration
docker compose -f docker/compose.yaml run --rm --no-deps integration
docker compose -f docker/compose.yaml --profile test down
docker compose -f docker/compose.yaml --profile mcp --profile mcp-fetch --profile mcp-model --profile php-dev --profile php-frameworks --profile tls config
docker compose -f docker/compose.yaml --profile mcp --profile mcp-fetch --profile mcp-model --profile php-dev --profile py-dev --profile php-frameworks --profile tls config
docker compose -f docker/compose.yaml run --rm --no-deps mcp-fetcher
docker compose -f docker/compose.yaml run --rm --no-deps mcp-tests
docker compose -f docker/compose.yaml run --rm --no-deps py-dev-fetcher
docker compose -f docker/compose.yaml run --rm --no-deps py-analysis
docker compose -f docker/compose.yaml run --rm --no-deps composer-install
docker compose -f docker/compose.yaml run --rm --no-deps php-fetcher
docker compose -f docker/compose.yaml run --rm --no-deps php-frameworks-fetcher
Expand All @@ -33,7 +35,7 @@ docker compose -f docker/compose.yaml run --rm --no-deps laravel-tests
docker compose -f docker/compose.yaml run --rm --no-deps symfony-tests
docker compose -f docker/compose.yaml up -d tls-proxy
docker compose -f docker/compose.yaml run --rm --no-deps tls-check
docker compose -f docker/compose.yaml --profile mcp --profile mcp-fetch --profile php-dev --profile php-frameworks --profile tls down
docker compose -f docker/compose.yaml --profile mcp --profile mcp-fetch --profile php-dev --profile py-dev --profile php-frameworks --profile tls down
```

The `composer-install` step is the consumer-install check: `composer validate --strict` on the library, then a
Expand All @@ -42,6 +44,11 @@ repository in a git worktree whose git directory is outside the mount) with Pack
`vendor/`, and autoloads `PhpAiBridge\Client`; the archive is built from the committed `HEAD`, so an uncommitted change
is not what it tests, and it needs no network and no cache.

`py-dev-fetcher` downloads the mypy and ruff wheels `requirements-dev.lock` pins into `.cache/wheels`, with network,
and `py-analysis` installs them and the MCP server's wheels from `mcp-fetcher` into a tmpfs and runs, offline,
`ruff check` over `worker`, `mcp_server`, `scripts` and `tests` and `mypy --strict` over `worker` and `mcp_server`,
both configured in `pyproject.toml`. Fix a finding with annotations or a small guard, never a behaviour change.

`php-fetcher` installs the PHPStan and the PSR-7/17/18 interfaces that `composer.lock` pins into
`.cache/php-vendor`, with network, and `php-analysis` runs PHPStan offline at level max over `src`, `examples`
and `tests/php` with `phpstan.neon`; `phpstan-baseline.neon` holds the findings left on purpose, each with its
Expand Down
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
/mcp_server export-ignore
/phpstan-baseline.neon export-ignore
/phpstan.neon export-ignore
/pyproject.toml export-ignore
/requirements-dev.lock export-ignore
/requirements-dev.txt export-ignore
/requirements-mcp.lock export-ignore
/requirements-mcp.txt export-ignore
/requirements-model.lock export-ignore
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,16 @@ jobs:
- name: Prepare disposable cache
run: mkdir -p .cache && chmod 777 .cache
- name: Inspect sandbox configuration
run: docker compose -f docker/compose.yaml --profile mcp --profile mcp-fetch --profile mcp-model --profile php-dev --profile php-frameworks --profile tls config
run: docker compose -f docker/compose.yaml --profile mcp --profile mcp-fetch --profile mcp-model --profile php-dev --profile py-dev --profile php-frameworks --profile tls config
- name: Acquire hash-pinned MCP wheels
run: docker compose -f docker/compose.yaml run --rm --no-deps mcp-fetcher
- name: Offline MCP server tests
run: docker compose -f docker/compose.yaml run --rm --no-deps mcp-tests
- name: Acquire the pinned mypy and ruff
run: docker compose -f docker/compose.yaml run --rm --no-deps py-dev-fetcher
# After mcp-fetcher: mypy reads the MCP SDK's types from its wheels.
- name: mypy --strict and ruff over the Python code, offline
run: docker compose -f docker/compose.yaml run --rm --no-deps py-analysis
- name: Consumer Composer install of the library
run: docker compose -f docker/compose.yaml run --rm --no-deps -e BRIDGE_REQUIRE_ARCHIVE=1 composer-install
- name: Acquire the locked PHPStan
Expand All @@ -97,7 +102,7 @@ jobs:
docker compose -f docker/compose.yaml run --rm --no-deps tls-check
- name: Stop test containers
if: always()
run: docker compose -f docker/compose.yaml --profile mcp --profile mcp-fetch --profile php-dev --profile php-frameworks --profile tls down
run: docker compose -f docker/compose.yaml --profile mcp --profile mcp-fetch --profile php-dev --profile py-dev --profile php-frameworks --profile tls down

# The images publish.yml pushes, built for this runner's platform only and never pushed.
images:
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ docker compose -f docker/compose.yaml up -d worker frankenphp fault-worker hosti
docker compose -f docker/compose.yaml run --rm --no-deps php-integration
docker compose -f docker/compose.yaml run --rm --no-deps integration
docker compose -f docker/compose.yaml --profile test down
docker compose -f docker/compose.yaml --profile mcp --profile mcp-fetch --profile mcp-model --profile php-dev --profile php-frameworks --profile tls config
docker compose -f docker/compose.yaml --profile mcp --profile mcp-fetch --profile mcp-model --profile php-dev --profile py-dev --profile php-frameworks --profile tls config
docker compose -f docker/compose.yaml run --rm --no-deps mcp-fetcher # network once: wheels into .cache/
docker compose -f docker/compose.yaml run --rm --no-deps mcp-tests
docker compose -f docker/compose.yaml run --rm --no-deps py-dev-fetcher # network once: mypy and ruff into .cache/
docker compose -f docker/compose.yaml run --rm --no-deps py-analysis # offline mypy --strict and ruff check
docker compose -f docker/compose.yaml run --rm --no-deps composer-install # offline consumer install of the library
docker compose -f docker/compose.yaml run --rm --no-deps php-fetcher # network once: PHPStan and the PSR interfaces into .cache/
docker compose -f docker/compose.yaml run --rm --no-deps php-frameworks-fetcher # network once: Laravel and Symfony into .cache/
Expand All @@ -50,7 +52,7 @@ docker compose -f docker/compose.yaml run --rm --no-deps laravel-tests # offli
docker compose -f docker/compose.yaml run --rm --no-deps symfony-tests # offline: the bundle in a bare Symfony kernel
docker compose -f docker/compose.yaml up -d tls-proxy
docker compose -f docker/compose.yaml run --rm --no-deps tls-check # the client over HTTPS through Caddy, example CA only
docker compose -f docker/compose.yaml --profile mcp --profile mcp-fetch --profile php-dev --profile php-frameworks --profile tls down
docker compose -f docker/compose.yaml --profile mcp --profile mcp-fetch --profile php-dev --profile py-dev --profile php-frameworks --profile tls down
```

CI runs the PHP steps on each of PHP 8.2, 8.3, 8.4 and 8.5 with `BRIDGE_PHP_IMAGE` set to the matrix's
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All notable changes to this project are documented in this file. The format foll
- `GET /readyz`, behind any configured token like `/healthz`: 200 when the worker is not draining and a child is idle with its backend's models loaded, otherwise 503 with a reason (`draining`, `no_idle_child`, `models_loading`). A child now loads its backend's models as it starts instead of on its first job. Compose's MCP workers report healthy only once ready.
- Optional structured logging: `BRIDGE_LOG=json` writes one JSON object per line on stderr, best effort (a full queue or the exit can drop lines), a `request` line after each reply (method, route template, status, duration, the token's name), a `job` line when a job ends (job ID, task, owner, terminal status, queue and run times), and `start`, `drain` and `stop` lines. A fixed schema checked value by value keeps inputs, results, headers, query strings, tokens and exception text out; a stderr that blocks or fails never holds a request. `off`, the default, writes nothing, and any other value stops the worker at start naming `BRIDGE_LOG`.
- `BRIDGE_OWNER_CAPACITY`, a cap on one token's queued, running and retained jobs (1 to the capacity; unset, the whole capacity, so nothing changes without it). A submission over it is the same 429 `capacity_exceeded` as a full worker, with a `Retry-After` from that token's own first expiry; a repeated idempotency key is still answered, and the total capacity still applies. The `start` log line carries `owner_capacity`.
- Static analysis of the Python code in the required `deterministic` check: `py-analysis` runs `mypy --strict` over the worker and the MCP server and `ruff check` over them, the scripts and the tests, offline, with mypy 2.4.0 and ruff 0.17.2 hash-pinned in `requirements-dev.lock` and configured in `pyproject.toml`; neither is installed at runtime or shipped in the archive.
- A full connection budget (32) is answered 503 `service_busy` with `Retry-After: 1` and `Connection: close` instead of a closed socket, and a request line over 8 KiB or a header block over 16 KiB is refused with 431 `headers_too_large`.

### Changed
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ docker compose -f docker/compose.yaml run --rm --no-deps integration
docker compose -f docker/compose.yaml run --rm --no-deps mcp-fetcher
docker compose -f docker/compose.yaml run --rm --no-deps mcp-tests

# mypy --strict and ruff over the Python code, after acquiring the pinned wheels once (network);
# py-analysis also installs the MCP server's wheels from mcp-fetcher, so it runs after that:
docker compose -f docker/compose.yaml run --rm --no-deps py-dev-fetcher
docker compose -f docker/compose.yaml run --rm --no-deps py-analysis

# PHP syntax validation inside the container:
docker compose -f docker/compose.yaml run --rm --no-deps php-tests sh -ec \
'find src examples tests/php -name "*.php" -print0 | xargs -0 -n1 php -l'
Expand All @@ -198,7 +203,7 @@ docker compose -f docker/compose.yaml run --rm --no-deps php-analysis

Coverage includes invalid contracts, duplicate/non-finite JSON, authentication, oversized bodies, queue deadlines, queued/running cancellation, hard process crashes, retention, malformed/oversized HTTP responses, redirect refusal, and reused-worker request isolation. Fault tasks are enabled only in the test service with `BRIDGE_TEST_TASKS=1`.

CI runs deterministic tests for pushes to main and for pull requests across PHP 8.2–8.5, treating PHP warnings and deprecations as test failures. Every job runs on GitHub's hosted runners (`ubuntu-latest`), because the repository is public: GitHub does not meter Actions minutes for public repositories, and a fork's pull request runs its own code on a disposable runner of GitHub's, not on a machine of the maintainer's; see [the runner notes](docs/testing.md#ci-runners) and [security](docs/security.md). The stable `deterministic` check succeeds only if every PHP matrix job and the MCP server job, which runs the offline MCP suite, the consumer install and PHPStan, succeed. A separate workflow performs model acquisition and the real-model smoke tests on the default PHP 8.5 image: weekly, on pull requests that touch the model path, and on demand. It is not a required check, it does not use paid APIs or GPUs, and it also prints an informational latency table: its jobs must succeed, but its times never gate the run.
CI runs deterministic tests for pushes to main and for pull requests across PHP 8.2–8.5, treating PHP warnings and deprecations as test failures. Every job runs on GitHub's hosted runners (`ubuntu-latest`), because the repository is public: GitHub does not meter Actions minutes for public repositories, and a fork's pull request runs its own code on a disposable runner of GitHub's, not on a machine of the maintainer's; see [the runner notes](docs/testing.md#ci-runners) and [security](docs/security.md). The stable `deterministic` check succeeds only if every PHP matrix job and the MCP server job, which runs the offline MCP suite, mypy and ruff, the consumer install and PHPStan, succeed. A separate workflow performs model acquisition and the real-model smoke tests on the default PHP 8.5 image: weekly, on pull requests that touch the model path, and on demand. It is not a required check, it does not use paid APIs or GPUs, and it also prints an informational latency table: its jobs must succeed, but its times never gate the run.

## Important limits

Expand Down
42 changes: 42 additions & 0 deletions docker/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,48 @@ services:
- -ec
- 'python -m pip install -q --require-hashes --no-compile --no-index --find-links=/cache/wheels --only-binary=:all: --target /deps -r requirements-mcp.lock && python -m unittest discover -s tests/mcp -v'

# The pinned mypy and ruff wheels into .cache: the only static-analysis step with network.
py-dev-fetcher:
<<: *python
profiles: [py-dev]
network_mode: bridge
volumes:
- ..:/work:ro
- ../.cache:/cache
environment:
PYTHONDONTWRITEBYTECODE: "1"
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_CACHE_DIR: "1"
command:
- sh
- -ec
- 'python -m pip download --require-hashes --only-binary=:all: --dest /cache/wheels -r requirements-dev.lock'

# mypy --strict and ruff, offline. The MCP server's wheels (from mcp-fetcher) are installed beside the tools so
# mypy reads the SDK's own types; both tools keep their caches in the tmpfs, since the checkout is read-only.
py-analysis:
<<: *python
profiles: [py-dev]
network_mode: none
mem_limit: 768m
tmpfs:
- /tmp:rw,noexec,nosuid,size=128m,uid=65532,gid=65532
- /deps:rw,exec,nosuid,size=384m,uid=65532,gid=65532
volumes:
- ..:/work:ro
- ../.cache:/cache:ro
environment:
PYTHONDONTWRITEBYTECODE: "1"
PYTHONPATH: /deps/tools:/deps/mcp
MYPY_CACHE_DIR: /tmp/mypy
RUFF_CACHE_DIR: /tmp/ruff
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_CACHE_DIR: "1"
command:
- sh
- -ec
- 'python -m pip install -q --require-hashes --no-compile --no-index --find-links=/cache/wheels --only-binary=:all: --target /deps/tools -r requirements-dev.lock && python -m pip install -q --require-hashes --no-compile --no-index --find-links=/cache/wheels --only-binary=:all: --target /deps/mcp -r requirements-mcp.lock && python -m ruff check worker mcp_server scripts tests && python -m mypy'

networks:
isolated:
internal: true
Expand Down
Loading
Loading