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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ on:
dirctl_image_tag:
required: false
type: string
description: "Override the tag of the dir-ctl Docker image to use in the tests."
description: "Override the version (release tag) of dirctl used by the interoperability tests."

permissions:
contents: read
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/reusable-test-sdk.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on:
dirctl_image_tag:
required: false
type: string
description: "Override the tag of the dir-ctl Docker image to use in the tests."
description: "Override the version (release tag) of dirctl used by the interoperability tests."
sdk_ref:
required: false
type: string
Expand Down Expand Up @@ -70,7 +70,7 @@ jobs:
- name: Test Python SDK
env:
DIR_CHART_VERSION: ${{ inputs.dir_chart_version }}
DIRCTL_IMAGE_TAG: ${{ inputs.dirctl_image_tag }}
DIRCTL_VERSION: ${{ inputs.dirctl_image_tag }}
run: |
task test

Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
[agntcy/dir]: https://github.com/agntcy/dir
[agntcy/dir-sdk-python]: https://github.com/agntcy/dir-sdk-python

## Unreleased

### Changed

- **Breaking:** `client.sign` and `client.verify` now run natively in Python (cosign-compatible key signing, Sigstore keyless signing via the `sigstore` package, and local verification from the store referrers) instead of invoking `dirctl`. `Config.dirctl_path`, `Config.docker_config`, `DockerConfig` and the `DIRCTL_PATH`/`DIRCTL_IMAGE`/`DIRCTL_IMAGE_TAG` environment variables were removed.

## 1.8.0 (2026-10-16)

### Changed
Expand Down
12 changes: 3 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ The Directory Python SDK provides comprehensive access to all Directory APIs wit
- **Real-time Streaming**: Subscribe to Directory events with a streaming gRPC listener

### **Signing and Verification**
- **Local Signing**: Sign records locally using private keys or OIDC-based authentication.
Requires [dirctl](https://github.com/agntcy/dir/releases) binary to perform signing.
- **Remote Verification**: Verify record signatures using the Directory gRPC API
- **Native Signing**: Sign records in-process using cosign-compatible private keys or OIDC-based keyless signing (Sigstore). No external binary is required.
- **Local and Remote Verification**: Verify record signatures locally (key, OIDC identity, or any signer) or fetch the server's cached result with `from_server=True`

### **Developer Experience**
- **Type Safety**: Full type hints for better IDE support and fewer runtime errors
Expand Down Expand Up @@ -71,7 +70,6 @@ The SDK can be configured via environment variables or direct instantiation:
```python
# Environment variables (insecure mode, default)
export DIRECTORY_CLIENT_SERVER_ADDRESS="localhost:8888"
export DIRCTL_PATH="/path/to/dirctl"

# Environment variables (X.509 authentication)
export DIRECTORY_CLIENT_SERVER_ADDRESS="localhost:8888"
Expand All @@ -89,15 +87,13 @@ from agntcy.dir_sdk.client import Config, Client

# Insecure mode (default, for development only)
config = Config(
server_address="localhost:8888",
dirctl_path="/usr/local/bin/dirctl"
server_address="localhost:8888"
)
client = Client(config)

# X.509 authentication with SPIRE
x509_config = Config(
server_address="localhost:8888",
dirctl_path="/usr/local/bin/dirctl",
spiffe_socket_path="/tmp/agent.sock",
auth_mode="x509"
)
Expand All @@ -106,7 +102,6 @@ x509_client = Client(x509_config)
# JWT authentication with SPIRE
jwt_config = Config(
server_address="localhost:8888",
dirctl_path="/usr/local/bin/dirctl",
spiffe_socket_path="/tmp/agent.sock",
auth_mode="jwt",
jwt_audience="spiffe://example.org/dir-server"
Expand Down Expand Up @@ -235,7 +230,6 @@ Common gRPC status codes:

- Python 3.10 or higher
- [uv](https://github.com/astral-sh/uv) - Package manager
- [dirctl](https://github.com/agntcy/dir/releases) - Directory CLI binary
- Directory server instance (see setup below)

### 1. Server Setup
Expand Down
2 changes: 1 addition & 1 deletion RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Create a branch for the new release:
2. Update the dependencies if necessary:
- [agntcy-dir-grpc-python](https://buf.build/agntcy/dir/sdks/v1.8.0%3Agrpc/python) (Buf SDK)
- [agntcy-dir-protocolbuffers-python](https://buf.build/agntcy/dir/sdks/v1.8.0%3Aprotocolbuffers/python) (Buf SDK)
- `.github/workflows/ci.yaml` (dir & dir-ctl version)
- `.github/workflows/ci.yaml` (dir version)
3. Add an entry to `CHANGELOG.md`

## 2. Create and push tags
Expand Down
30 changes: 17 additions & 13 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ vars:
DIR_CHART_VERSION: '{{ .DIR_CHART_VERSION | default "v1.8.0" }}'
# renovate: datasource=github-releases depName=agntcy/dir versioning=semver
DIR_IMAGE_TAG: '{{ .DIR_IMAGE_TAG | default "v1.8.0" }}'
# renovate: datasource=github-releases depName=agntcy/dir versioning=semver
DIRCTL_IMAGE_TAG: '{{ .DIRCTL_IMAGE_TAG | default "v1.8.0" }}'

# renovate: datasource=github-releases depName=renovatebot/renovate versioning=semver
RENOVATE_VERSION: "44.141.0"
Expand Down Expand Up @@ -37,6 +35,10 @@ vars:
COSIGN_VERSION: "3.1.3"
COSIGN_BIN: "{{ .BIN_DIR }}/cosign-{{.COSIGN_VERSION}}"

# renovate: datasource=github-releases depName=agntcy/dir versioning=semver
DIRCTL_VERSION: '{{ .DIRCTL_VERSION | default "v1.8.0" }}'
DIRCTL_BIN: "{{ .BIN_DIR }}/dirctl-{{.DIRCTL_VERSION}}"

KIND_CLUSTER_NAME: "sdk-py-test"

tasks:
Expand Down Expand Up @@ -72,12 +74,12 @@ tasks:
- defer: { task: test-env:delete }
- task: test-env:create
- |
export DIRCTL_IMAGE_TAG="{{ .DIRCTL_IMAGE_TAG }}"
export COSIGN_PATH="$(printf "%s" "${COSIGN_PATH:-{{ .COSIGN_BIN }}}")"
export DIRCTL_PATH="$(printf "%s" "${DIRCTL_PATH:-{{ .DIRCTL_BIN }}}")"

# Run SDK tests
'{{ .UV_BIN }}' run pytest \
--cov=agntcy.dir_sdk \
--cov=dir-sdk-python/agntcy/dir_sdk \
--cov-report=xml:coverage.xml \
--cov-report=term-missing

Expand All @@ -91,7 +93,7 @@ tasks:
deps:
- task: deps:uv
- task: deps:cosign
- task: deps:dir-ctl
- task: deps:dirctl
cmds:
- "{{.UV_BIN}} sync --all-packages"

Expand Down Expand Up @@ -393,22 +395,24 @@ tasks:
deps:cosign:
desc: Install sigstore cosign
internal: true
deps:
- deps:bin-dir
cmds:
- curl -sfL https://github.com/sigstore/cosign/releases/download/v{{.COSIGN_VERSION}}/cosign-{{OS}}-{{ARCH}} -o {{.BIN_DIR}}/cosign-{{.COSIGN_VERSION}}
- chmod +x {{.BIN_DIR}}/cosign-{{.COSIGN_VERSION}}
status:
- test -x {{.COSIGN_BIN}}

deps:dir-ctl:
desc: Install dir-ctl
deps:dirctl:
desc: Install dirctl (only used by the SDK interoperability tests)
internal: true
run: once
vars:
DIRCTL_IMAGE_TAG: "{{ .DIRCTL_IMAGE_TAG }}"
preconditions:
- which docker
deps:
- deps:bin-dir
cmds:
- docker image pull ghcr.io/agntcy/dir-ctl:{{ .DIRCTL_IMAGE_TAG }}
- curl -sfL https://github.com/agntcy/dir/releases/download/{{.DIRCTL_VERSION}}/dirctl-{{OS}}-{{ARCH}} -o {{.DIRCTL_BIN}}
- chmod +x {{.DIRCTL_BIN}}
status:
- test -x {{.DIRCTL_BIN}}

deps:renovate:
desc: Install Renovate
Expand Down
2 changes: 1 addition & 1 deletion dir-sdk-python/agntcy/dir_sdk/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def __init__(self, config: Config | None = None) -> None:
self.routing_service = RoutingService(self.routing_client, logger)
self.publication_service = PublicationService(self.publication_client, logger)
self.search_service = SearchService(self.search_client, logger)
self.sign_service = SignService(self.config, self.sign_client, logger)
self.sign_service = SignService(self.sign_client, self.store_service, logger)
self.sync_service = SyncService(self.sync_client, logger)
self.event_service = EventService(self.event_client, logger)
self.identity_service = IdentityService(self.identity_client, logger)
Expand Down
89 changes: 0 additions & 89 deletions dir-sdk-python/agntcy/dir_sdk/client/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,69 +31,8 @@ def _parse_comma_scopes(value: str | list[str] | None, default: list[str]) -> li
return [s.strip() for s in value.split(",") if s.strip()]


class DockerConfig:
"""
A class for using dirctl via the Docker image.
"""

DEFAULT_DIRCTL_IMAGE = "ghcr.io/agntcy/dir-ctl"
DEFAULT_DIRCTL_IMAGE_TAG = "latest"

def __init__(
self,
dirctl_image: str = DEFAULT_DIRCTL_IMAGE,
dirctl_image_tag: str = DEFAULT_DIRCTL_IMAGE_TAG,
envs: dict[str, str] | None = None,
mounts: list[str] | None = None,
user: str | None = None,
) -> None:
if envs is None:
envs = {}
if mounts is None:
mounts = []

self.dirctl_image = dirctl_image
self.dirctl_image_tag = dirctl_image_tag
self.envs: dict[str, str] = envs
self.mounts: list[str] = mounts
self.user = user

def get_commands(self) -> list[str]:
self.prune_mounts()
commands = [
"docker",
"container",
"run",
"--name=dir-ctl",
"--rm",
"--network",
"host",
]
if self.user:
commands.extend(["--user", self.user])
for key, val in self.envs.items():
commands.append("--env")
commands.append(f"{key}={val}")
for mount in self.mounts:
commands.append("--mount")
commands.append(mount)
commands.append(f"{self.dirctl_image}:{self.dirctl_image_tag}")
return commands

def prune_mounts(self) -> None:
mounts = []
for mount in self.mounts:
if mount.startswith("type=bind"):
_type, src, _dst = mount.split(",")
_, src = src.split("=")
if os.path.isfile(src):
mounts.append(mount)
self.mounts = mounts


class Config:
DEFAULT_SERVER_ADDRESS = "127.0.0.1:8888"
DEFAULT_DIRCTL_PATH = "dirctl"
DEFAULT_SPIFFE_SOCKET_PATH = ""
DEFAULT_AUTH_MODE = ""
DEFAULT_AUTH_TOKEN: str = ""
Expand All @@ -115,7 +54,6 @@ class Config:
def __init__(
self,
server_address: str = DEFAULT_SERVER_ADDRESS,
dirctl_path: str | None = DEFAULT_DIRCTL_PATH,
spiffe_socket_path: str = DEFAULT_SPIFFE_SOCKET_PATH,
auth_mode: str = DEFAULT_AUTH_MODE,
auth_token: str = DEFAULT_AUTH_TOKEN,
Expand All @@ -133,10 +71,8 @@ def __init__(
oidc_auth_timeout: float = DEFAULT_OIDC_AUTH_TIMEOUT,
oidc_scopes: list[str] | None = None,
oidc_access_token: str | None = None,
docker_config: DockerConfig | None = None,
) -> None:
self.server_address = server_address
self.dirctl_path = dirctl_path
self.spiffe_socket_path = spiffe_socket_path
resolved_auth_token = auth_token or oidc_access_token or ""
self.auth_token = resolved_auth_token
Expand All @@ -160,33 +96,10 @@ def __init__(
if oidc_scopes is not None
else list(Config.DEFAULT_OIDC_SCOPES)
)
self.docker_config = docker_config
if dirctl_path and docker_config:
raise ValueError("You cannot specify both dirctl_path and docker_config.")

def get_dirctl(self) -> list[str]:
if self.dirctl_path:
return [self.dirctl_path]
if self.docker_config is None:
msg = "Either dirctl_path or docker_config must be configured"
raise ValueError(msg)
return self.docker_config.get_commands()

@staticmethod
def load_from_env(env_prefix: str = "DIRECTORY_CLIENT_") -> "Config":
"""Load configuration from environment variables."""
dirctl_path = os.environ.get("DIRCTL_PATH")
dirctl_image = os.environ.get("DIRCTL_IMAGE")
dirctl_image_tag = os.environ.get("DIRCTL_IMAGE_TAG")

docker_config = None
if dirctl_image or dirctl_image_tag:
docker_config = DockerConfig(
dirctl_image or DockerConfig.DEFAULT_DIRCTL_IMAGE,
dirctl_image_tag or DockerConfig.DEFAULT_DIRCTL_IMAGE_TAG,
user="0:0",
)

server_address = os.environ.get(
f"{env_prefix}SERVER_ADDRESS",
Config.DEFAULT_SERVER_ADDRESS,
Expand Down Expand Up @@ -258,7 +171,6 @@ def load_from_env(env_prefix: str = "DIRECTORY_CLIENT_") -> "Config":

return Config(
server_address=server_address,
dirctl_path=dirctl_path,
spiffe_socket_path=spiffe_socket_path,
auth_mode=auth_mode,
auth_token=auth_token,
Expand All @@ -275,5 +187,4 @@ def load_from_env(env_prefix: str = "DIRECTORY_CLIENT_") -> "Config":
oidc_callback_port=oidc_callback_port,
oidc_auth_timeout=oidc_auth_timeout,
oidc_scopes=oidc_scopes,
docker_config=docker_config,
)
9 changes: 0 additions & 9 deletions dir-sdk-python/agntcy/dir_sdk/client/dirctl/__init__.py

This file was deleted.

Loading
Loading