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
56 changes: 56 additions & 0 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Publish atomic-agents-stack to PyPI.
#
# Triggered on tag pushes matching strict 3-component SemVer (e.g. v1.0.0, v1.1.0, v2.0.1).
# The pattern v[0-9]+.[0-9]+.[0-9]+ matches exactly these forms and rejects pre-release
# tags (v1.0.0-rc1, v1.0.0-beta) or partial-version tags (v1.0) that could trigger
# accidental publishes.
#
# IMPORTANT: The first publish to PyPI must be done manually (see
# docs/deployment/release-runbook.md ##TestPyPI smoke). This workflow
# automates v1.0.1 onward once the project exists on PyPI and the
# UV_PUBLISH_TOKEN secret has been added to the repository settings.
#
# Required GitHub Actions secret:
# UV_PUBLISH_TOKEN -- PyPI API token with upload scope for this project.
# Set at: Settings > Secrets and variables > Actions.
# Store the token value in Apple Passwords under
# "pypi-atomic-agents-stack" for recovery.

name: Publish to PyPI

on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"

jobs:
publish:
name: Build and publish to PyPI
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Check out source
uses: actions/checkout@v4

- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"

- name: Build wheel and sdist
run: uv build

- name: Validate metadata and README rendering
run: uv tool run twine check dist/*

- name: Publish to PyPI
env:
UV_PUBLISH_TOKEN: ${{ secrets.UV_PUBLISH_TOKEN }}
run: uv publish
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
# tests in test_judge_revise_jsonschema.py and the strict
# end-to-end tests in test_agent_judge_revise_dispatch.py
# exercise the real jsonschema dependency.
run: uv sync --extra dev --extra openai --extra validation --extra redis --python ${{ matrix.python-version }}
run: uv sync --extra dev --extra openai --extra validation --extra redis --extra http --python ${{ matrix.python-version }}

- name: Run tests
run: uv run --python ${{ matrix.python-version }} pytest -v --tb=short
Expand Down
34 changes: 34 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Run the full suite before pushing:
uv run pytest
```

2937 tests today; CI runs Python 3.11 + 3.12. New backend protocols add ~25 conformance tests + ~10 implementation-specific tests. New features ship with tests. Migration-shaped PRs need parameterized fixture tests across the backend protocol.
3270+ tests today; CI runs Python 3.11 + 3.12. New backend protocols add ~25 conformance tests + ~10 implementation-specific tests. New features ship with tests. Migration-shaped PRs need parameterized fixture tests across the backend protocol.

### Review

Expand Down
178 changes: 95 additions & 83 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion atomic_agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
ToolAlreadyInstalled,
)

__version__ = "0.13.0"
__version__ = "1.0.0"

__all__ = [
"AtomicAgent",
Expand Down
17 changes: 9 additions & 8 deletions atomic_agents/_locks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
but every public symbol here emits ``DeprecationWarning`` and delegates
to ``atomic_agents.locks.FilesystemLockBackend`` under the hood.

# SUNSET v1.0
# SUNSET v1.1
Per CLAUDE.md rule #14 ("Backward compatibility by default"), this shim
is planned for removal in the v1.0 release. New code MUST import from
is planned for removal in the v1.1 release (sunset deferred from v1.0
per the #201 PR 5 release decision). New code MUST import from
``atomic_agents.locks`` directly. Existing code should migrate via the
mechanical substitution:

Expand Down Expand Up @@ -40,15 +41,15 @@
from .locks.types import LockHandle


# SUNSET v1.0
# SUNSET v1.1
class AgentLock:
"""DEPRECATED: thin wrapper over ``FilesystemLockBackend.acquire("")``.

Preserves the legacy per-agent-flock construction signature
``AgentLock(agent_root, wait_seconds=...)`` while delegating the
actual acquire/release to the new Protocol-shaped backend.

Sunset planned for v1.0 — new code should construct
Sunset planned for v1.1; new code should construct
``FilesystemLockBackend(agent_root).acquire("", timeout=...)``
directly.
"""
Expand All @@ -62,7 +63,7 @@ def __init__(
warnings.warn(
"atomic_agents._locks.AgentLock is deprecated; use "
"atomic_agents.locks.FilesystemLockBackend instead. "
"See docs/spec/21-lock-backend.md. Sunset planned for v1.0.",
"See docs/spec/21-lock-backend.md. Sunset planned for v1.1.",
DeprecationWarning,
stacklevel=2,
)
Expand Down Expand Up @@ -110,7 +111,7 @@ def release(self) -> None:
self._handle = None


# SUNSET v1.0
# SUNSET v1.1
@contextlib.contextmanager
def acquire(agent_root: Path, wait_seconds: float = 0.0):
"""DEPRECATED: contextmanager wrapper for ``AgentLock``.
Expand All @@ -119,14 +120,14 @@ def acquire(agent_root: Path, wait_seconds: float = 0.0):
``acquire(agent_root, wait_seconds)`` shape. Emits a
``DeprecationWarning`` and delegates to ``AgentLock``.

Sunset planned for v1.0 — new code should use
Sunset planned for v1.1; new code should use
``with FilesystemLockBackend(agent_root).acquire("",
timeout=wait_seconds) as handle: ...`` directly.
"""
warnings.warn(
"atomic_agents._locks.acquire() is deprecated; use "
"atomic_agents.locks.FilesystemLockBackend instead. "
"Sunset planned for v1.0.",
"Sunset planned for v1.1.",
DeprecationWarning,
stacklevel=2,
)
Expand Down
6 changes: 6 additions & 0 deletions atomic_agents/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,12 @@ def _cmd_mcp_registry(args) -> int:
except MCPRegistryError as e:
print(f"Error: MCP registry error: {e}", file=sys.stderr)
return 1
except NotImplementedError as e:
print(
f"Error: operation not supported by this backend: {e}",
file=sys.stderr,
)
return 1
except ValueError as e:
print(f"Error: invalid server name: {e}", file=sys.stderr)
return 1
Expand Down
25 changes: 24 additions & 1 deletion atomic_agents/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def _is_path_shaped(arg: str) -> bool:
# Data classes


@dataclass
@dataclass(repr=False)
class MCPServerSpec:
"""Declaration of an MCP server an agent may connect to.

Expand All @@ -97,6 +97,10 @@ class MCPServerSpec:
env: extra env vars (resolved from agent's env at parse time)
transport: only "stdio" supported in v1
description: operator-readable note

Note on repr: ``env`` is redacted in the repr because it may contain
resolved secret values (API tokens, passwords) after ``load_mcp_server``
resolution. Use ``to_dict()`` if you need the full values for serialization.
"""

name: str
Expand All @@ -106,6 +110,25 @@ class MCPServerSpec:
transport: str = "stdio"
description: str = ""

def __repr__(self) -> str:
"""Return a repr that redacts env to prevent secret leakage in logs and error messages.

The ``env`` dict may contain resolved secret values after
``load_mcp_server()`` resolution. Including them in repr would leak
secrets into tracebacks, log lines, and operator-facing error messages.
The count of env entries is shown for debuggability without exposing values.
"""
return (
f"MCPServerSpec("
f"name={self.name!r}, "
f"command={self.command!r}, "
f"args={self.args!r}, "
f"env=<{len(self.env)} entries; redacted>, "
f"transport={self.transport!r}, "
f"description={self.description!r}"
f")"
)

def to_dict(self) -> dict:
"""Serialize to a JSON-safe plain dict.

Expand Down
Loading
Loading