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
53 changes: 53 additions & 0 deletions .claude/hooks/repo-rules.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/bin/sh
# After an edit: what CONTRIBUTING.md requires always to hold in the file just edited.
# PHP under src/, examples/ or tests/php declares strict types and uses spaces; a worker module
# (worker/ai_bridge) imports no model dependency at module level, since that path is standard library only.
# Exit 2 reports the problem to the agent; the edit itself has already happened.
cd "${CLAUDE_PROJECT_DIR:-.}" || exit 0
input=$(cat)
file=$(printf '%s' "$input" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)
[ -n "$file" ] || exit 0
# The path arrives absolute, on Windows with backslashes (JSON-escaped as \\) and a drive letter of either
# case; make it repository-relative.
file=$(printf '%s' "$file" | sed 's#\\\\#/#g; s#\\#/#g')
root=$(pwd -W 2>/dev/null || pwd)
upper() { printf '%s' "$1" | awk '{ print toupper(substr($0, 1, 1)) substr($0, 2) }'; }
case $file in
[A-Za-z]:/*) file=$(upper "$file"); root=$(upper "$root") ;;
esac
case $file in
"$root"/*) file=${file#"$root"/} ;;
"$PWD"/*) file=${file#"$PWD"/} ;;
esac
[ -f "$file" ] || exit 0
msg=""
case $file in
src/*.php|examples/*.php|tests/php/*.php)
if ! head -n 10 "$file" | grep -q 'declare(strict_types=1);'; then
msg="$file: add declare(strict_types=1); right after the opening tag (CONTRIBUTING.md)."
fi
if grep -q "$(printf '\t')" "$file"; then
msg="${msg:+$msg
}$file: indent with four spaces, not tabs (CONTRIBUTING.md)."
fi
;;
worker/ai_bridge/*.py)
# Any import statement naming a model dependency outside a function body: at column zero, or
# indented under try/if at module level. Imports inside functions are indented too, so an indented
# match is only reported when no def precedes it.
deps='(numpy|onnxruntime|tokenizers|torch|transformers)'
if awk -v re="^[[:space:]]*(import[[:space:]]+([A-Za-z_.]+[[:space:]]*,[[:space:]]*)*${deps}([[:space:].,]|$)|from[[:space:]]+${deps}([[:space:].]|$))" '
/^[[:space:]]*(def|async[[:space:]]+def|class)[[:space:]]/ { infn = 1 }
/^[^[:space:]#]/ && !/^(def|async[[:space:]]+def|class)[[:space:]]/ { infn = 0 }
$0 ~ re && !infn { found = 1 }
END { exit found ? 0 : 1 }' "$file"; then
msg="$file: a model dependency is imported at module level; import it inside the ONNX function only (the worker path is standard library only)."
fi
;;
*) exit 0 ;;
esac
if [ -n "$msg" ]; then
printf '%s\n' "$msg" >&2
exit 2
fi
exit 0
23 changes: 23 additions & 0 deletions .claude/rules/mcp-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
paths:
- "mcp_server/**"
- "tests/mcp/**"
- "scripts/mcp_*"
- "requirements-mcp.*"
---
# The MCP server

- The tool list is a literal in `tools.py`, never read from the worker: test-only tasks are never tools,
whatever `BRIDGE_TEST_TASKS` says, and the suite runs with it set to prove that.
- `BACKENDS` names what the worker reports, per backend and task, and must equal
`ai_bridge.tasks.MODEL_NAMES` (`tests/mcp/test_models.py`); this package cannot import the worker.
Every timing claim in a tool description is a measured median from `docs/testing.md` (a test checks),
every path parameter says where it resolves, and every position in a result says what it counts.
- Paths resolve under `BRIDGE_MCP_ROOT` and must stay inside it after following symlinks; the server
writes nothing, and the index builder is the only writer. A worker's free text and the token are never
reflected; only its fixed error code is.
- A call times out after `BRIDGE_MCP_TIMEOUT_MS` and then cancels the job; a full worker is waited out
within the call's own budget. The MCP server never shares a worker with an application.
- Dependencies are `requirements-mcp.txt` with a hash lock; `mcp-fetcher` downloads the wheels once and
`mcp-tests` installs them offline into a tmpfs. Pin changes are reviewed, regenerated deliberately and
tested before they land.
24 changes: 24 additions & 0 deletions .claude/rules/php-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
paths:
- "src/**"
- "examples/**"
- "tests/php/**"
---
# The PHP client

- Every file starts with `declare(strict_types=1)`; four-space indentation; PHP `>=8.2 <8.6` with
`ext-curl` and `ext-json` only. No `ext-mbstring`: `Client::codePoints()` counts code points as bytes
minus UTF-8 continuation bytes, and new text limits go through it.
- Warnings are failures. `tests/php/run.php` promotes every warning and deprecation to an exception, so a
call deprecated on PHP 8.5 (as `curl_close()` was) fails the suite: release handles by dropping them.
CI runs the suite on 8.2, 8.3, 8.4 and 8.5; to reproduce one locally, set `BRIDGE_PHP_IMAGE` to that
job's pinned image from `.github/workflows/tests.yml` and rerun the same compose commands.
- Tests are `check()` and `rejects()` calls: `tests/php/run.php` runs offline (contract checks and one
transport error), and `tests/php/integration.php` loads it and then drives the client against the real
`worker`, `fault-worker` and `hostile` peers over HTTP. The path through FrankenPHP is
`tests/integration.py`. A new behaviour gets a success case, invalid-input cases and, where the worker
is involved, an integration case. Syntax is checked with `php -l` over `src`, `examples` and `tests/php`.
- The client never retries a `POST` on its own (a transport failure leaves the submission uncertain), and
`wait()` never cancels; the MCP server cancels on its own deadline on purpose. Keep both as
`docs/protocol.md` describes, and change the doc with the contract.
- The example routes have no end-user authentication and publish no ports; don't add either to the examples.
25 changes: 25 additions & 0 deletions .claude/rules/worker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
paths:
- "worker/**"
- "tests/python/**"
- "scripts/fetch_model.py"
---
# The Python worker

- The worker path is standard library only. `numpy`, `onnxruntime` and `tokenizers` are imported inside
the ONNX functions of `tasks.py`, never at module top, so the demo backend and every test run without
them; the post-edit hook reports a top-level import of a model dependency. Model dependencies belong
in `requirements-model.txt` and its hash lock, downloaded only by the `fetcher` service; the model
worker installs them offline from `.cache/wheels` into a tmpfs when it starts.
- Tasks are an allowlist in `tasks.py`, each with `validate()` before `execute()` and each run in its own
process. Clients get fixed error codes, never exception text, and the token is never logged or returned.
Test-only tasks exist only with `BRIDGE_TEST_TASKS=1` (compose's `fault-worker`).
- `MODEL_NAMES` is what every result reports as `model` and what the MCP server's `BACKENDS` table must
equal (`tests/mcp/test_models.py`); change both together.
- Limits live in three places: concurrency, capacity and retention are `Settings` in `jobs.py` (defaults
from the environment in `server.main`), the body size is `MAX_BODY` in `server.py`, and the field,
count and character limits are the `MAX_*` constants in `tasks.py`; the deadline range is checked in
`JobStore.submit`. `docs/protocol.md` documents them; change the doc with the limit.
- Tests run offline with `python -m unittest discover -s tests/python` inside `python-tests`
(`network_mode: none`). A change to the model path belongs on `model-smoke.yml`'s `paths:` list, so
the real-model smoke tests run for it; keep that list current when a file moves.
201 changes: 201 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"attribution": {
"commit": "",
"pr": "",
"sessionUrl": false
},
"permissions": {
"allow": [
"Bash(docker compose -f docker/compose.yaml config)",
"Bash(docker compose -f docker/compose.yaml --profile * config)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps python-tests)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps php-tests)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps php-tests sh -ec *)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps php-integration)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps integration)",
"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 up -d worker frankenphp fault-worker hostile)",
"Bash(docker compose -f docker/compose.yaml up -d worker frankenphp)",
"Bash(docker compose -f docker/compose.yaml down)",
"Bash(docker compose -f docker/compose.yaml down *)",
"Bash(docker compose -f docker/compose.yaml --profile * down)",
"Bash(docker compose -f docker/compose.yaml --profile * down *)",
"Bash(docker compose -f docker/compose.yaml logs *)",
"Bash(docker compose -f docker/compose.yaml --profile * logs *)",
"Bash(docker compose -f docker/compose.yaml ps *)",
"Bash(docker compose -f docker/compose.yaml --profile * ps *)",
"Bash(docker compose version)",
"Bash(docker version)",
"Bash(docker ps *)",
"Bash(git status *)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(git show *)",
"Bash(git grep *)",
"Bash(git blame *)",
"Bash(git worktree list *)",
"Bash(git branch)",
"Bash(git branch --show-current)",
"Bash(git branch --list *)",
"Bash(git branch -a)",
"Bash(git branch -r)",
"Bash(git branch -v)",
"Bash(git branch -vv)",
"Bash(gh pr view *)",
"Bash(gh pr list *)",
"Bash(gh pr checks *)",
"Bash(gh pr diff *)",
"Bash(gh issue view *)",
"Bash(gh issue list *)",
"Bash(gh run view *)",
"Bash(gh run list *)",
"Bash(gh workflow view *)",
"Bash(gh workflow list *)",
"PowerShell(docker compose -f docker/compose.yaml config)",
"PowerShell(docker compose -f docker/compose.yaml --profile * config)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps python-tests)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps php-tests)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps php-tests sh -ec *)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps php-integration)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps integration)",
"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 up -d worker frankenphp fault-worker hostile)",
"PowerShell(docker compose -f docker/compose.yaml up -d worker frankenphp)",
"PowerShell(docker compose -f docker/compose.yaml down)",
"PowerShell(docker compose -f docker/compose.yaml down *)",
"PowerShell(docker compose -f docker/compose.yaml --profile * down)",
"PowerShell(docker compose -f docker/compose.yaml --profile * down *)",
"PowerShell(docker compose -f docker/compose.yaml logs *)",
"PowerShell(docker compose -f docker/compose.yaml --profile * logs *)",
"PowerShell(docker compose -f docker/compose.yaml ps *)",
"PowerShell(docker compose -f docker/compose.yaml --profile * ps *)",
"PowerShell(docker compose version)",
"PowerShell(docker version)",
"PowerShell(docker ps *)",
"PowerShell(git status *)",
"PowerShell(git diff *)",
"PowerShell(git log *)",
"PowerShell(git show *)",
"PowerShell(git grep *)",
"PowerShell(git blame *)",
"PowerShell(git worktree list *)",
"PowerShell(git branch)",
"PowerShell(git branch --show-current)",
"PowerShell(git branch --list *)",
"PowerShell(git branch -a)",
"PowerShell(git branch -r)",
"PowerShell(git branch -v)",
"PowerShell(git branch -vv)",
"PowerShell(gh pr view *)",
"PowerShell(gh pr list *)",
"PowerShell(gh pr checks *)",
"PowerShell(gh pr diff *)",
"PowerShell(gh issue view *)",
"PowerShell(gh issue list *)",
"PowerShell(gh run view *)",
"PowerShell(gh run list *)",
"PowerShell(gh workflow view *)",
"PowerShell(gh workflow list *)"
],
"ask": [
"Bash(gh workflow run *)",
"Bash(gh pr create *)",
"Bash(gh pr merge *)",
"Bash(gh api -f*)",
"Bash(gh api -F*)",
"Bash(gh api * -f*)",
"Bash(gh api * -F*)",
"Bash(gh api *--field*)",
"Bash(gh api *--raw-field*)",
"Bash(gh api *--input*)",
"Bash(docker compose -f docker/compose.yaml run --rm --no-deps fetcher)",
"PowerShell(gh workflow run *)",
"PowerShell(gh pr create *)",
"PowerShell(gh pr merge *)",
"PowerShell(gh api -f*)",
"PowerShell(gh api -F*)",
"PowerShell(gh api * -f*)",
"PowerShell(gh api * -F*)",
"PowerShell(gh api *--field*)",
"PowerShell(gh api *--raw-field*)",
"PowerShell(gh api *--input*)",
"PowerShell(docker compose -f docker/compose.yaml run --rm --no-deps fetcher)"
],
"deny": [
"Read(**/.env)",
"Read(**/.env.*)",
"Read(**/*.key)",
"Read(**/*.pem)",
"Read(~/.claude/.credentials.json)",
"Bash(gh release *)",
"Bash(gh pr merge *--auto*)",
"Bash(gh pr merge *--admin*)",
"Bash(gh api *-X*)",
"Bash(gh api *--method*)",
"Bash(git push --force *)",
"Bash(git push -f *)",
"Bash(git push *--force*)",
"Bash(git push * -f)",
"Bash(git push * -f *)",
"Bash(git push * +*)",
"Bash(git push * main)",
"Bash(git push * *:main)",
"Bash(git push *--tags*)",
"Bash(git push *--follow-tags*)",
"Bash(git push *refs/tags/*)",
"Bash(git push *--mirror*)",
"Bash(git push *--delete*)",
"Bash(git tag *)",
"Bash(git grep *-O*)",
"Bash(git grep *--open-files-in-pager*)",
"Bash(git * --output*)",
"Bash(git * --output=*)",
"Bash(git diff *--no-index*)",
"Bash(git push *refs/heads/main*)",
"PowerShell(gh release *)",
"PowerShell(gh pr merge *--auto*)",
"PowerShell(gh pr merge *--admin*)",
"PowerShell(gh api *-X*)",
"PowerShell(gh api *--method*)",
"PowerShell(git push --force *)",
"PowerShell(git push -f *)",
"PowerShell(git push *--force*)",
"PowerShell(git push * -f)",
"PowerShell(git push * -f *)",
"PowerShell(git push * +*)",
"PowerShell(git push * main)",
"PowerShell(git push * *:main)",
"PowerShell(git push *--tags*)",
"PowerShell(git push *--follow-tags*)",
"PowerShell(git push *refs/tags/*)",
"PowerShell(git push *--mirror*)",
"PowerShell(git push *--delete*)",
"PowerShell(git tag *)",
"PowerShell(git grep *-O*)",
"PowerShell(git grep *--open-files-in-pager*)",
"PowerShell(git * --output*)",
"PowerShell(git * --output=*)",
"PowerShell(git diff *--no-index*)",
"PowerShell(git push *refs/heads/main*)"
]
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "sh \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/repo-rules.sh",
"timeout": 20
}
]
}
]
}
}
39 changes: 39 additions & 0 deletions .claude/skills/check/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
name: check
description: Run what CI's deterministic gate runs (compose python, PHP, syntax, integration and MCP suites) before a PR
---
Run the steps in order from the repository root and stop at the first failure. Report each step's
result in one line; on a failure, show the error text and fix it before re-running.

`BRIDGE_TOKEN` must be set to a disposable random string of 32+ characters first; every compose command
refuses to start without it, and the `config` steps print it, so never run them with a real token in the
shell. The MCP step needs `.cache/wheels` from one network-enabled `mcp-fetcher`
run; it is then offline. On Linux, `mkdir -p .cache && chmod 777 .cache` first so container UID 65532
can write there.

```bash
docker compose -f docker/compose.yaml --profile test config
docker compose -f docker/compose.yaml run --rm --no-deps python-tests
docker compose -f docker/compose.yaml run --rm --no-deps php-tests
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'
docker compose -f docker/compose.yaml up -d worker frankenphp fault-worker hostile
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 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 --profile mcp --profile mcp-fetch down
```

This runs the PHP steps on compose's default image, PHP 8.5. CI runs them on 8.2, 8.3, 8.4 and 8.5
and first checks that the image's PHP minor is the one the matrix expects; to reproduce another branch,
set `BRIDGE_PHP_IMAGE` to its pinned image from `.github/workflows/tests.yml` and rerun the PHP and
integration steps. Report the other three as "left to CI". If the integration step fails, read
`docker compose -f docker/compose.yaml logs --tail 100` before `down`.

The real-model smoke tests (`model-smoke.yml`) are not part of this: they download about 330 MB and run
only for a change on that workflow's `paths:` list, through `fetcher`, `model-test` and
`model-integration` as README describes.

This skill does not merge: merging is through the PR, under AGENTS.md's conditions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
* text=auto eol=lf
# Hook scripts run under sh, which breaks on CRLF line endings.
.claude/hooks/* text eol=lf
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ __pycache__/
.DS_Store
.idea/
.vscode/

# Personal AI notes and settings
CLAUDE.local.md
.claude/settings.local.json
Loading
Loading