Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7ac3398
apply dependabot insights
alain-sv May 13, 2026
22d50aa
minor
alain-sv May 13, 2026
2a89660
chore(ci): SHA-pin actions/checkout and actions/setup-python (#43)
alain-sv May 13, 2026
297d98e
docs(changelog): add unreleased dependency security refresh entry (#44)
alain-sv May 13, 2026
1c41f54
chore(ci): add uv cache-suffix per Python version + update AGENTS.md …
alain-sv May 13, 2026
00aad5e
Merge branch 'main' into develop
alain-sv May 13, 2026
1f5c176
supervaizer-v2-mvp-contracts (#47)
alain-sv May 17, 2026
d7c98b9
Minor
alain-sv May 17, 2026
b192229
Merge branch 'main' into develop
alain-sv May 17, 2026
789b0c6
chore: update changelog for Supervaizer v2 enhancements and API key v…
alain-sv May 17, 2026
da4b00d
Merge branch 'main' into develop
alain-sv May 17, 2026
c977b6e
workspace-agent-grants (#50)
alain-sv May 19, 2026
a12b628
minor
alain-sv May 19, 2026
a761421
Merge branch 'main' into develop
alain-sv May 20, 2026
1f20b22
feat(logging): implement structured logging for Cloud Logging compati…
alain-sv May 20, 2026
746a04f
Merge branch 'main' into develop
alain-sv May 24, 2026
75a3a0d
Minor
alain-sv May 24, 2026
612001f
Merge branch 'main' into develop
alain-sv May 26, 2026
3324db5
codex/supervaizer-lifespan-cleanup (#54)
alain-sv May 26, 2026
e7834a2
codex/refactor-server-modules (#55)
alain-sv May 26, 2026
4fed877
minor
alain-sv May 26, 2026
afaf602
codex/agent-interviewer-workspace-jobs-refresh (#56)
alain-sv May 26, 2026
ceede13
Refactor pre-commit configuration and enhance agent validation
alain-sv May 26, 2026
62f299d
Merge branch 'main' into develop
alain-sv May 27, 2026
ea6ce6c
chore(deps): bump uv from 0.11.14 to 0.11.15 (#58)
dependabot[bot] May 30, 2026
770a668
chore(deps): bump starlette from 0.50.0 to 1.0.1 (#60)
dependabot[bot] Jun 6, 2026
15e586e
chore(deps): bump trufflesecurity/trufflehog from 3.95.3 to 3.95.5 (#61)
dependabot[bot] Jun 9, 2026
4d5703a
feat(.agents): add skill symlinks from runwaize skills cookbook
alain-sv Jun 17, 2026
f197480
chore(deps): bump astral-sh/setup-uv from 8.1.0 to 8.2.0 (#62)
dependabot[bot] Jun 20, 2026
b50b3e9
chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#63)
dependabot[bot] Jun 20, 2026
4461b3e
chore(deps): bump starlette from 1.0.1 to 1.3.1 (#66)
dependabot[bot] Jun 20, 2026
287b498
chore(deps): bump cryptography from 48.0.0 to 48.0.1 (#67)
dependabot[bot] Jun 20, 2026
d3dcc08
chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#68)
dependabot[bot] Jul 2, 2026
5712dfd
chore(deps): bump softprops/action-gh-release from 3.0.0 to 3.0.1 (#69)
dependabot[bot] Jul 2, 2026
b64d982
chore(deps): bump trufflesecurity/trufflehog from 3.95.5 to 3.95.6 (#70)
dependabot[bot] Jul 2, 2026
dbd4e3f
codex/managed-context-sdk (#65)
alain-sv Jul 2, 2026
665ef50
✨ feat(contracts): add V2ContextAssignment for context.assign (#71)
alain-sv Jul 2, 2026
ec535d1
feat: bump version to1.3.0 and update deps; iterate nested (#73)
alain-sv Jul 2, 2026
4e8fc0f
changelog
alain-sv Jul 2, 2026
a09ef1a
chore(deps): bump actions/setup-python from 6.2.0 to 6.3.0 (#75)
dependabot[bot] Jul 7, 2026
6a53535
chore(deps): bump trufflesecurity/trufflehog from 3.95.6 to 3.95.7 (#76)
dependabot[bot] Jul 7, 2026
9bf38b0
Merge branch 'main' into develop
alain-sv Jul 7, 2026
fe1d11d
security: review summary (redacted) + safe P0/P1 hardening (#77)
alain-sv Jul 7, 2026
5fe47c6
[MINOR] chore: merge develop to main
alain-sv Jul 7, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.13"

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.13"

Expand Down
75 changes: 73 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
# Documentation-only diffs (.md, .txt, docs/) skip unit tests but still run pre-commit.

name: Python package

Expand All @@ -11,11 +12,73 @@ on:
workflow_call:

jobs:
change_scope:
runs-on: ubuntu-latest
outputs:
skip_tests: ${{ steps.detect.outputs.skip_tests }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0

- name: Detect documentation-only changes
id: detect
run: |
is_documentation_only_path() {
case "$1" in
*.md|*.MD|*.txt|*.TXT) return 0 ;;
docs/*|Documentation/*|documentation/*) return 0 ;;
changelog/*) return 0 ;;
*) return 1 ;;
esac
}

# Reusable workflows and unknown events always run the full suite.
if [ "${{ github.event_name }}" = "workflow_call" ]; then
echo "skip_tests=false" >> "$GITHUB_OUTPUT"
exit 0
fi

if [ "${{ github.event_name }}" = "pull_request" ]; then
git fetch --depth=1 origin "${{ github.event.pull_request.base.sha }}"
mapfile -t changed < <(git diff --name-only "${{ github.event.pull_request.base.sha }}" HEAD)
elif [ "${{ github.event_name }}" = "push" ]; then
before="${{ github.event.before }}"
if [ -z "$before" ] || [ "$before" = "0000000000000000000000000000000000000000" ]; then
echo "skip_tests=false" >> "$GITHUB_OUTPUT"
exit 0
fi
mapfile -t changed < <(git diff --name-only "$before" HEAD)
else
echo "skip_tests=false" >> "$GITHUB_OUTPUT"
exit 0
fi

if [ "${#changed[@]}" -eq 0 ]; then
echo "skip_tests=false" >> "$GITHUB_OUTPUT"
exit 0
fi

doc_only=true
for path in "${changed[@]}"; do
if ! is_documentation_only_path "$path"; then
doc_only=false
break
fi
done

if [ "$doc_only" = true ]; then
echo "skip_tests=true" >> "$GITHUB_OUTPUT"
echo "Documentation-only changes detected — unit tests will be skipped."
else
echo "skip_tests=false" >> "$GITHUB_OUTPUT"
fi

pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.13"
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
Expand All @@ -35,6 +98,7 @@ jobs:
.github/workflows

build:
needs: change_scope
runs-on: ubuntu-latest

strategy:
Expand All @@ -45,7 +109,7 @@ jobs:
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
Expand All @@ -54,6 +118,13 @@ jobs:
# Avoid parallel matrix jobs racing on the same Actions cache reservation
cache-suffix: py-${{ matrix.python-version }}
- name: Install dependencies
if: needs.change_scope.outputs.skip_tests != 'true'
run: uv sync --frozen --extra dev --extra deploy
- name: Documentation-only changes — unit tests skipped
if: needs.change_scope.outputs.skip_tests == 'true'
run: |
echo "Only .md, .txt, or docs/ paths changed."
echo "Skipping pytest matrix."
- name: Test with pytest
if: needs.change_scope.outputs.skip_tests != 'true'
run: uv run pytest --no-cov
2 changes: 1 addition & 1 deletion .github/workflows/trufflehog-full-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ jobs:

- name: Full Repository Scan
if: inputs.scan_type == 'full-repo'
uses: trufflesecurity/trufflehog@30d5bb91af1a771378349dbbb0c82129392acf70 # v3.95.6
uses: trufflesecurity/trufflehog@f446421baf832d6356c42c1743d99abff52ff334 # v3.95.7
with:
extra_args: --results=verified,unknown
2 changes: 1 addition & 1 deletion .github/workflows/trufflehog-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ jobs:
fetch-depth: 0 # Required for TruffleHog to scan git history

- name: Secret Scanning
uses: trufflesecurity/trufflehog@30d5bb91af1a771378349dbbb0c82129392acf70 # v3.95.6
uses: trufflesecurity/trufflehog@f446421baf832d6356c42c1743d99abff52ff334 # v3.95.7
with:
extra_args: --results=verified,unknown
86 changes: 86 additions & 0 deletions docs/2026_07_SECURITY_REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Supervaizer — Security & Performance Review (Summary)

> **Date:** 2026-07-07
> **Scope:** Full (non-diff) review of the entire `supervaizer` SDK source tree (~20k LOC).
> **Type:** Security review + performance/scalability review.
>
> **⚠️ Disclosure note:** Per [`SECURITY.md`](../SECURITY.md), detailed vulnerability
> findings — attack scenarios, exact code locations, and remediation specifics — are **not**
> published here. This page is a non-actionable high-level summary only. The complete
> findings are handled through the project's private vulnerability channel (GitHub Security
> Advisories) so that unpatched issues are not operationalized in a public artifact.

---

## Methodology

A multi-agent audit: per-component security finders reviewed the full source; every
high-impact candidate was independently re-checked by an adversarial verifier (instructed
to refute it and confirm real reachability), which recalibrated several severities; a
completeness critic then swept for missed classes; and a separate pass covered async
performance and scalability. This was a static review — no exploit was executed.

## Overall posture

The **core authorization primitives are sound.** Verified during the review:

- Signed workspace authorization uses EdDSA with the algorithm pinned (no `alg:none` or
algorithm-confusion), full claim binding (issuer/audience/expiry/subject/workspace), and
keys sourced only from configured trust material.
- Privileged protocol actions fail **closed** when authorization is not configured.
- No SQL/NoSQL/command injection, no server-side template injection, and no unsafe
deserialization (`pickle`/`yaml.load`/`eval`) were found.
- No CORS misconfiguration — none is configured, so the safe same-origin default applies.
- Jinja autoescaping is enabled.

The material risk is concentrated in **credential handling and the trust model of the
administrative surface**, not in the request-validation core. Themes (no specifics here):

- Development/quick-start defaults that are unsafe if exposed on an untrusted network.
- An administrative surface whose trust boundary can be weakened under certain
reverse-proxy configurations.
- Credential material that is more exposed at rest / in transit than it should be.
- A symmetric-encryption construction that should be migrated to an authenticated (AEAD)
scheme.
- Deployment tooling that handles secrets less defensively than the runtime does.

## Supply-chain posture (already in place)

The repository already implements a strong supply-chain baseline, documented in
[`SECURITY.md`](../SECURITY.md): a committed `uv.lock` with `uv sync --frozen` enforced in
CI, Dependabot security updates, OSV-Scanner on every PR, secret scanning with push
protection, SHA-pinned third-party Actions, and OIDC Trusted Publishing. Pinned dependency
floors were reviewed and are modern (no known-vulnerable pins). **No supply-chain action is
recommended beyond what already exists.**

## Findings summary (counts only)

| Domain | Critical | High | Medium | Low |
|--------|----------|------|--------|-----|
| Security (post-verification) | 0 | 6 | 16 | 18 |
| Performance / scalability | — | 9 | 10 | 2 |

No finding survived verification at **Critical**.

### Performance themes

The performance findings cluster into two areas: **unbounded in-memory growth** (long-lived
registries that do not evict completed work) and **event-loop blocking** (synchronous I/O
and whole-file persistence operations executed on the async loop). Neither is a correctness
bug today; each degrades under sustained load or data growth. The highest-leverage
mitigations are caching/offloading the persistence layer, evicting terminal entities, and
using the already-present async code paths for request-time verification.

## Remediation approach

Detailed, prioritized remediation guidance (P0–P3) accompanies the private report. At a
high level: address credential-handling and admin-trust items first, migrate the symmetric
encryption to an AEAD construction, then harden response headers, request limits, and
object-level authorization, and finally apply the performance mitigations. The core
architecture does not require redesign.

---

*For the complete findings and remediation detail, maintainers should refer to the private
security advisory. Report any deviation from the documented security posture via the private
channel in [`SECURITY.md`](../SECURITY.md).*
23 changes: 23 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,27 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- **Security & performance review summary** — Added `docs/2026_07_SECURITY_REVIEW.md`, a non-actionable high-level summary of a full-source security and performance/scalability review (posture, verified-sound controls, severity counts, and remediation themes). Per `SECURITY.md`, detailed findings (locations, attack scenarios, remediation specifics) are handled through the private vulnerability channel and are intentionally omitted from the public repository.

### Fixed

- **Hardened API-key checks** — API keys are compared in constant time.
- **Safer local test mode** — `supervaizer start --local` binds to loopback (`127.0.0.1`) by default instead of all interfaces; pass an explicit `--host` to override.
- **Baseline security response headers** — Responses now set `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, and `Strict-Transport-Security` (streaming/SSE-safe).
- **Reduced sensitive data in logs** — Agent parameter values are no longer written to logs during parameter validation.
- **Scheduled-step execution hardening** — The scheduler only runs methods declared by the agent that owns the step's job.

### Tests

| Status | Count |
| ---------- | ----- |
| ✅ Passed | 683 |
| 🤔 Skipped | 0 |
| 🔴 Failed | 0 |
| ⏱️ in | 83s |

## [1.3.1] - 2026-07-02

### Changed
Expand All @@ -22,6 +43,8 @@ All notable changes to this project will be documented in this file.

- **A2A event scope test** — `tests/test_a2a.py` now walks FastAPI included-router wrappers when locating `/a2a/events`, preserving the read-scope assertion under FastAPI `0.139.0`.

### Tests

| Status | Count |
| ---------- | ----- |
| ✅ Passed | 683 |
Expand Down
1 change: 0 additions & 1 deletion src/supervaizer/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@
# If a copy of the MPL was not distributed with this file, you can obtain one at
# https://mozilla.org/MPL/2.0/.


VERSION = "1.3.1"
__version__ = VERSION
8 changes: 7 additions & 1 deletion src/supervaizer/access/api_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import annotations

import hmac
import os
from collections.abc import Callable
from typing import Annotated
Expand Down Expand Up @@ -61,7 +62,12 @@ def require_api_key( # <-- ADDED
live_server = getattr(getattr(request, "app", None), "state", None)
live_server = getattr(live_server, "server", None) if live_server else None
live_key = getattr(live_server, "api_key", None) if live_server else None
if live_key and x_api_key == live_key:
# Constant-time comparison to avoid a timing side channel on the key.
# Compare bytes so non-ASCII keys fail closed instead of raising
# TypeError (hmac.compare_digest rejects non-ASCII str inputs).
if live_key and hmac.compare_digest(
x_api_key.encode("utf-8"), live_key.encode("utf-8")
):
return {"scope": "write"} # live server key always has full access
log_access_denied_api(x_api_key, path, "invalid key")
raise HTTPException(status_code=401, detail="Invalid or missing API key")
Expand Down
18 changes: 16 additions & 2 deletions src/supervaizer/admin/workbench_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from supervaizer.contracts import API_VERSION
from supervaizer.job import Job, JobContext, JobResponse, Jobs
from supervaizer.lifecycle import EntityStatus
from supervaizer.scheduled_steps import _execute_scheduled_method

templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))

Expand Down Expand Up @@ -531,13 +532,25 @@ async def workbench_execute_step(
request: Request, slug: str, job_id: str, case_id: str, step_index: int
) -> Response:
"""Execute a scheduled step immediately."""
from supervaizer.server import _execute_scheduled_method
agent = get_agent_by_slug(request, slug)

get_agent_by_slug(request, slug)
# Verify the job and case are both owned by this agent before using
# the agent's method allow-list.
job = Jobs().get_job(job_id, agent_name=agent.name)
if job is None:
raise HTTPException(
status_code=404,
detail=f"Job '{job_id}' not found for agent '{slug}'",
)

case = Cases().get_case(case_id, job_id=job_id)
if not case:
raise HTTPException(status_code=404, detail=f"Case '{case_id}' not found")
if case.id not in job.case_ids:
raise HTTPException(
status_code=404,
detail=f"Case '{case_id}' not found for agent '{slug}'",
)

if step_index < 0 or step_index >= len(case.updates):
raise HTTPException(status_code=404, detail="Step not found")
Expand All @@ -557,6 +570,7 @@ async def workbench_execute_step(
_execute_scheduled_method(
update.scheduled_method,
update.scheduled_params or {},
allowed_methods=agent._declared_method_paths(),
)
object.__setattr__(update, "scheduled_status", "completed")
return JSONResponse({
Expand Down
6 changes: 6 additions & 0 deletions src/supervaizer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@ def start(

if local:
os.environ["SUPERVAIZER_LOCAL_MODE"] = "true"
# Security: local test mode uses a well-known default API key
# ("local-dev"). Never expose that on all network interfaces — bind
# loopback unless the user chose a specific non-wildcard host.
if host in ("0.0.0.0", "::", ""):
host = "127.0.0.1"
os.environ["SUPERVAIZER_HOST"] = host
Comment on lines +194 to +196

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve explicit local host overrides

When a local dev server needs to be reachable outside the host, e.g. supervaizer start --local --host 0.0.0.0 in Docker, this branch rewrites that explicit value to 127.0.0.1 before constructing or overriding the Server, so uvicorn still binds only loopback even though the release note says an explicit --host can override the safer default. Track whether the option was actually omitted, and only rewrite the default wildcard binding.

Useful? React with 👍 / 👎.

# In local mode, force public_url to localhost unless the user
# explicitly passed --public-url on the CLI.
if not user_provided_public_url:
Expand Down
20 changes: 16 additions & 4 deletions src/supervaizer/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,12 +623,19 @@ async def validate_agent_parameters(
"encrypted_agent_parameters": f"Decryption failed: {e!s}"
},
}
log.info(f"📤 Agent {agent.name}: Decryption failed → {result}")
# Do not log the result payload: it can echo parameter data.
log.info(f"📤 Agent {agent.name}: Decryption failed")
return result

# Log the incoming request details
# Log the incoming request details.
# Never log decrypted parameter values (secrets); log only presence/count.
_param_count = (
len(agent_parameters) if isinstance(agent_parameters, dict) else 0
)
log.info(
f"🔍 Agent {agent.name}: Incoming request - encrypted_params: {bool(encrypted_agent_parameters)}, parsed_params: {agent_parameters}"
f"🔍 Agent {agent.name}: Incoming request - "
f"encrypted_params: {bool(encrypted_agent_parameters)}, "
f"param_count: {_param_count}"
)

# Validate agent parameters
Expand All @@ -643,7 +650,12 @@ async def validate_agent_parameters(
"invalid_parameters": validation_result["invalid_parameters"],
}

log.info(f"📤 Agent {agent.name}: Validation result → {result}")
# Log only the outcome, not the result payload (may contain values).
log.info(
f"📤 Agent {agent.name}: Validation "
f"{'passed' if validation_result['valid'] else 'failed'} "
f"({len(validation_result['errors'])} error(s))"
)
return result

@router.post(
Expand Down
Loading
Loading