dispatch_agents 0.10.1__tar.gz → 0.12.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/PKG-INFO +1 -1
- dispatch_agents-0.12.2/RELEASE_NOTES.md +1 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/config.py +114 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/integrations/github/__init__.py +17 -0
- dispatch_agents-0.12.2/dispatch_agents/integrations/github/client.py +121 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/llm.py +11 -1
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/proxy/server.py +2 -2
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/hello_world/agent.py +50 -1
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/hello_world/uv.lock +48 -48
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/pyproject.toml +2 -2
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/uv.lock +11 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/pyproject.toml +1 -1
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_config.py +207 -0
- dispatch_agents-0.12.2/tests/test_github_client.py +233 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_proxy_server.py +8 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/uv.lock +7 -7
- dispatch_agents-0.10.1/RELEASE_NOTES.md +0 -21
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/.claude-plugin/marketplace.json +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/.github/scripts/change_scope.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/.github/scripts/ci_git.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/.github/scripts/version_policy.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/.github/tests/test_change_scope.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/.github/tests/test_ci_git.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/.github/tests/test_version_policy.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/.github/workflows/ci-reusable.yml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/.github/workflows/feature-branch.yml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/.github/workflows/release.yml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/.github/workflows/version-policy-reusable.yml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/.gitignore +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/CONTRIBUTING.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/LICENSE +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/LICENSE-3rdparty.csv +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/NOTICE +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/README.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/agentservice/__init__.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/agentservice/py.typed +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/agentservice/v1/__init__.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/agentservice/v1/message_pb2.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/agentservice/v1/message_pb2.pyi +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/agentservice/v1/message_pb2_grpc.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/agentservice/v1/request_response_pb2.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/agentservice/v1/request_response_pb2.pyi +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/agentservice/v1/request_response_pb2_grpc.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/agentservice/v1/service_pb2.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/agentservice/v1/service_pb2.pyi +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/agentservice/v1/service_pb2_grpc.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/__init__.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/agent_service.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/contrib/__init__.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/contrib/claude/__init__.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/contrib/openai/__init__.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/events.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/grpc_server.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/instrument.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/integrations/__init__.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/integrations/github/README.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/invocation.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/llm_langchain.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/logging_config.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/mcp.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/memory.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/models.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/proxy/__init__.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/proxy/sse_utils.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/py.typed +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/resources.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/version.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/README.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/company-researcher/.gitignore +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/company-researcher/AGENTS.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/company-researcher/README.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/company-researcher/agent.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/company-researcher/dispatch.yaml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/company-researcher/pyproject.toml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/company-researcher/uv.lock +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/conversational-agent/README.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/conversational-agent/agent.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/conversational-agent/dispatch.yaml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/conversational-agent/pyproject.toml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/daily-digest/README.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/daily-digest/agent.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/daily-digest/dispatch.yaml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/daily-digest/pyproject.toml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/deep-research/README.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/deep-research/agent.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/deep-research/configuration.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/deep-research/deep_researcher.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/deep-research/dispatch.yaml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/deep-research/prompts.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/deep-research/pyproject.toml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/deep-research/state.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/deep-research/tools.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/hello_world/.gitignore +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/hello_world/AGENTS.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/hello_world/dispatch.yaml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/hello_world/pyproject.toml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/hello_world/test_agent.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/knowledge-base-query/.env.example +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/knowledge-base-query/.gitignore +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/knowledge-base-query/AGENTS.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/knowledge-base-query/agent.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/knowledge-base-query/dispatch.yaml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/knowledge-base-query/pyproject.toml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/multi-framework/README.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/multi-framework/agent.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/multi-framework/dispatch.yaml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/multi-framework/pyproject.toml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/weather-assistant/.gitignore +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/weather-assistant/AGENTS.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/weather-assistant/agent.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/weather-assistant/dispatch.yaml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/weather-assistant/pyproject.toml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/weather-assistant/uv.lock +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/weather-service/.gitignore +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/weather-service/AGENTS.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/weather-service/agent.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/weather-service/dispatch.yaml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/weather-service/pyproject.toml +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/examples/weather-service/uv.lock +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/internal/py.typed +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/plugins/README.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/__init__.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/e2e_claude_mcp_proxy.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/schemas/README.md +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/schemas/octokit-webhooks.json +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_agent_service.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_agent_uid.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_contrib_claude.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_contrib_openai.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_dev_mode_isolation.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_extra_headers.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_fn_decorator.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_github_integration.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_github_schema_compliance.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_grpc_server.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_init.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_instrument.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_llm_langchain.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_llm_logging.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_logging_config.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_mcp.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_memory.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_proxy_e2e.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_resources.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_sse_utils.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_trace_context.py +0 -0
- {dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/tests/test_typed_events.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
BREAKING CHANGE: Compatibility changes to remove LiteLLM from backend.
|
|
@@ -5,6 +5,7 @@ shared between CLI and backend.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
8
9
|
from enum import StrEnum
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
@@ -25,6 +26,13 @@ RESERVED_ENV_VARS: frozenset[str] = frozenset(
|
|
|
25
26
|
}
|
|
26
27
|
)
|
|
27
28
|
|
|
29
|
+
# Domain validation pattern for egress allow list.
|
|
30
|
+
# Accepts exact FQDNs (api.openai.com) and wildcard prefixes (*.github.com).
|
|
31
|
+
# Rejects URLs with schemes/ports/paths, IP addresses, and bare wildcards.
|
|
32
|
+
_DOMAIN_PATTERN = re.compile(
|
|
33
|
+
r"^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$"
|
|
34
|
+
)
|
|
35
|
+
|
|
28
36
|
|
|
29
37
|
class VolumeMode(StrEnum):
|
|
30
38
|
"""Volume access mode for persistent storage.
|
|
@@ -322,6 +330,98 @@ class ResourceConfig(BaseModel):
|
|
|
322
330
|
)
|
|
323
331
|
|
|
324
332
|
|
|
333
|
+
class DomainSelector(BaseModel):
|
|
334
|
+
"""A single domain selector -- exactly one of match_name or match_pattern.
|
|
335
|
+
|
|
336
|
+
match_name is an exact FQDN (e.g. api.openai.com).
|
|
337
|
+
match_pattern is a wildcard prefix (e.g. *.github.com).
|
|
338
|
+
|
|
339
|
+
Serialises with camelCase aliases (matchName / matchPattern) to match the
|
|
340
|
+
downstream Cilium FQDN selector API.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
match_name: str | None = Field(
|
|
344
|
+
default=None,
|
|
345
|
+
description="Exact FQDN to allow. Must match the entire domain name exactly "
|
|
346
|
+
"(e.g. 'api.openai.com' matches only 'api.openai.com').",
|
|
347
|
+
)
|
|
348
|
+
match_pattern: str | None = Field(
|
|
349
|
+
default=None,
|
|
350
|
+
description="Wildcard pattern to allow. Uses '*.domain.com' syntax to match "
|
|
351
|
+
"any subdomain of the specified domain (e.g. '*.github.com' matches "
|
|
352
|
+
"'api.github.com' and 'raw.github.com' but not 'github.com' itself).",
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
@model_validator(mode="after")
|
|
356
|
+
def _validate_exactly_one_field(self) -> "DomainSelector":
|
|
357
|
+
if self.match_name and self.match_pattern:
|
|
358
|
+
raise ValueError(
|
|
359
|
+
"Exactly one of match_name or match_pattern must be set, not both"
|
|
360
|
+
)
|
|
361
|
+
if not self.match_name and not self.match_pattern:
|
|
362
|
+
raise ValueError("Exactly one of match_name or match_pattern must be set")
|
|
363
|
+
domain = self.match_name or self.match_pattern or ""
|
|
364
|
+
if not _DOMAIN_PATTERN.match(domain):
|
|
365
|
+
raise ValueError(
|
|
366
|
+
f"Invalid domain '{domain}'. "
|
|
367
|
+
"Must be an exact FQDN (e.g., api.openai.com) "
|
|
368
|
+
"or wildcard prefix (e.g., *.github.com). "
|
|
369
|
+
"URL schemes, ports, paths, IP addresses, "
|
|
370
|
+
"and bare wildcards are not allowed."
|
|
371
|
+
)
|
|
372
|
+
return self
|
|
373
|
+
|
|
374
|
+
model_config = {"extra": "forbid"}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class EgressConfig(BaseModel):
|
|
378
|
+
"""Configuration for network egress allow list.
|
|
379
|
+
|
|
380
|
+
Domains are specified as objects with either matchName (exact FQDN)
|
|
381
|
+
or matchPattern (wildcard prefix). This is a subset of the
|
|
382
|
+
downstream Cilium FQDN selector API.
|
|
383
|
+
|
|
384
|
+
Example:
|
|
385
|
+
network:
|
|
386
|
+
egress:
|
|
387
|
+
allow_domains:
|
|
388
|
+
- match_name: api.openai.com
|
|
389
|
+
- match_pattern: "*.github.com"
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
allow_domains: list[DomainSelector] = Field(
|
|
393
|
+
default_factory=list,
|
|
394
|
+
description="Domains allowed for egress as Cilium FQDN selectors.",
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
@field_validator("allow_domains")
|
|
398
|
+
@classmethod
|
|
399
|
+
def _validate_allow_domains(cls, v: list[DomainSelector]) -> list[DomainSelector]:
|
|
400
|
+
if len(v) > 50:
|
|
401
|
+
raise ValueError(
|
|
402
|
+
f"allow_domains cannot have more than 50 entries, got {len(v)}"
|
|
403
|
+
)
|
|
404
|
+
return v
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class NetworkConfig(BaseModel):
|
|
408
|
+
"""Network configuration for an agent.
|
|
409
|
+
|
|
410
|
+
When present in dispatch.yaml, CiliumNetworkPolicies are created to
|
|
411
|
+
restrict the agent's outbound traffic to platform services and any
|
|
412
|
+
listed allow_domains. When absent, all egress is unrestricted.
|
|
413
|
+
|
|
414
|
+
Example:
|
|
415
|
+
network:
|
|
416
|
+
egress:
|
|
417
|
+
allow_domains:
|
|
418
|
+
- match_name: api.openai.com
|
|
419
|
+
- match_pattern: "*.github.com"
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
egress: EgressConfig = Field(default_factory=EgressConfig)
|
|
423
|
+
|
|
424
|
+
|
|
325
425
|
class DispatchConfig(BaseModel):
|
|
326
426
|
"""Configuration model for dispatch.yaml files.
|
|
327
427
|
|
|
@@ -416,6 +516,10 @@ class DispatchConfig(BaseModel):
|
|
|
416
516
|
default_factory=ResourceConfig,
|
|
417
517
|
description="Container resource limits (CPU and memory)",
|
|
418
518
|
)
|
|
519
|
+
network: NetworkConfig | None = Field(
|
|
520
|
+
default=None,
|
|
521
|
+
description="Network egress restrictions. When set, CiliumNetworkPolicies restrict outbound traffic.",
|
|
522
|
+
)
|
|
419
523
|
|
|
420
524
|
@field_validator("env")
|
|
421
525
|
@classmethod
|
|
@@ -485,6 +589,16 @@ class DispatchConfig(BaseModel):
|
|
|
485
589
|
}
|
|
486
590
|
result["resources"] = {"limits": limits_dict}
|
|
487
591
|
|
|
592
|
+
if self.network is not None:
|
|
593
|
+
result["network"] = {
|
|
594
|
+
"egress": {
|
|
595
|
+
"allow_domains": [
|
|
596
|
+
d.model_dump(exclude_none=True)
|
|
597
|
+
for d in self.network.egress.allow_domains
|
|
598
|
+
],
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
488
602
|
return result
|
|
489
603
|
|
|
490
604
|
model_config = {"populate_by_name": True}
|
{dispatch_agents-0.10.1 → dispatch_agents-0.12.2}/dispatch_agents/integrations/github/__init__.py
RENAMED
|
@@ -88,6 +88,10 @@ from typing import Any, ClassVar, Literal
|
|
|
88
88
|
from pydantic import ConfigDict, Field
|
|
89
89
|
|
|
90
90
|
from dispatch_agents.events import BasePayload
|
|
91
|
+
from dispatch_agents.integrations.github.client import GitHubAppToken
|
|
92
|
+
from dispatch_agents.integrations.github.client import (
|
|
93
|
+
get_github_app_token as _get_github_app_token,
|
|
94
|
+
)
|
|
91
95
|
from dispatch_agents.models import StrictBaseModel
|
|
92
96
|
|
|
93
97
|
# =============================================================================
|
|
@@ -98,6 +102,16 @@ from dispatch_agents.models import StrictBaseModel
|
|
|
98
102
|
GITHUB_TOPIC_PREFIX = "github."
|
|
99
103
|
|
|
100
104
|
|
|
105
|
+
async def get_github_app_token() -> GitHubAppToken:
|
|
106
|
+
"""Return a GitHub App installation token.
|
|
107
|
+
|
|
108
|
+
This package-level wrapper is the canonical public import path for GitHub
|
|
109
|
+
token retrieval and delegates to the lower-level client implementation.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
return await _get_github_app_token()
|
|
113
|
+
|
|
114
|
+
|
|
101
115
|
# =============================================================================
|
|
102
116
|
# GitHubEvent Enum
|
|
103
117
|
# =============================================================================
|
|
@@ -4056,6 +4070,9 @@ __all__ = [
|
|
|
4056
4070
|
"GitHubIssuePullRequest",
|
|
4057
4071
|
"GitHubChangeValue",
|
|
4058
4072
|
"GitHubChanges",
|
|
4073
|
+
# GitHub client
|
|
4074
|
+
"get_github_app_token",
|
|
4075
|
+
"GitHubAppToken",
|
|
4059
4076
|
# Watch events
|
|
4060
4077
|
"WatchBase",
|
|
4061
4078
|
"WatchStarted",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import UTC, datetime, timedelta
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from dispatch_agents.events import _get_auth_headers, _get_router_url
|
|
10
|
+
|
|
11
|
+
# Module-level cache for the current agent process.
|
|
12
|
+
# The SDK assumes a single agent run does not switch orgs or API keys in-process;
|
|
13
|
+
# multi-org access is enforced by the backend, not modeled in this client cache.
|
|
14
|
+
_cached_token: tuple[GitHubAppToken, datetime] | None = None
|
|
15
|
+
|
|
16
|
+
_TOKEN_BUFFER_MINUTES = 5
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class GitHubAppToken:
|
|
21
|
+
token: str
|
|
22
|
+
expires_at: datetime
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def get_github_app_token() -> GitHubAppToken:
|
|
26
|
+
"""Return a GitHub App installation token for use with any HTTP client.
|
|
27
|
+
|
|
28
|
+
Fetches a GitHub App installation token from the Dispatch backend. The token
|
|
29
|
+
is cached transparently; subsequent calls return the cached token until it is
|
|
30
|
+
near expiry (< 5 min), at which point a fresh token is fetched automatically.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
GitHubAppToken: Dataclass with ``token`` (str) and
|
|
34
|
+
``expires_at`` (timezone-aware datetime) fields. Pass ``token`` as a
|
|
35
|
+
Bearer credential to any GitHub API client of your choice.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
RuntimeError: If DISPATCH_API_KEY environment variable is not set.
|
|
39
|
+
RuntimeError: If no GitHub installation is configured for this org (404).
|
|
40
|
+
RuntimeError: If authentication fails — check DISPATCH_API_KEY (401).
|
|
41
|
+
RuntimeError: If access is forbidden (403).
|
|
42
|
+
RuntimeError: If the backend returns an unexpected HTTP status code.
|
|
43
|
+
|
|
44
|
+
Note:
|
|
45
|
+
Refresh is lazy and call-time: a new token is fetched on the NEXT call
|
|
46
|
+
after the cached token nears expiry (<5 min remaining). Call
|
|
47
|
+
get_github_app_token() at the start of each handler invocation
|
|
48
|
+
rather than once at module load.
|
|
49
|
+
|
|
50
|
+
Network errors (httpx.ConnectError, httpx.TimeoutException) propagate
|
|
51
|
+
as-is and are not caught.
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
Using PyGithub::
|
|
55
|
+
|
|
56
|
+
from github import Auth, Github
|
|
57
|
+
from dispatch_agents.integrations.github import get_github_app_token
|
|
58
|
+
|
|
59
|
+
tok = await get_github_app_token()
|
|
60
|
+
gh = Github(auth=Auth.Token(tok.token))
|
|
61
|
+
repo = gh.get_repo("my-org/my-repo")
|
|
62
|
+
|
|
63
|
+
Using httpx directly::
|
|
64
|
+
|
|
65
|
+
import httpx
|
|
66
|
+
from dispatch_agents.integrations.github import get_github_app_token
|
|
67
|
+
|
|
68
|
+
tok = await get_github_app_token()
|
|
69
|
+
async with httpx.AsyncClient() as client:
|
|
70
|
+
resp = await client.get(
|
|
71
|
+
"https://api.github.com/repos/my-org/my-repo",
|
|
72
|
+
headers={"Authorization": f"Bearer {tok.token}"},
|
|
73
|
+
)
|
|
74
|
+
"""
|
|
75
|
+
global _cached_token
|
|
76
|
+
|
|
77
|
+
if not os.getenv("DISPATCH_API_KEY"):
|
|
78
|
+
raise RuntimeError(
|
|
79
|
+
"DISPATCH_API_KEY environment variable is not set. "
|
|
80
|
+
"GitHub installation token requires an authenticated Dispatch agent."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if _cached_token is not None:
|
|
84
|
+
cached, expires_at = _cached_token
|
|
85
|
+
if expires_at >= datetime.now(UTC) + timedelta(minutes=_TOKEN_BUFFER_MINUTES):
|
|
86
|
+
return cached
|
|
87
|
+
|
|
88
|
+
url = _get_router_url() + "/api/unstable/integrations/github/installation-token"
|
|
89
|
+
|
|
90
|
+
async with httpx.AsyncClient() as http_client:
|
|
91
|
+
response = await http_client.post(
|
|
92
|
+
url, headers=_get_auth_headers(), timeout=10.0
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if response.status_code == 401:
|
|
96
|
+
raise RuntimeError(
|
|
97
|
+
"GitHub installation token request failed: unauthorized. "
|
|
98
|
+
"Check that DISPATCH_API_KEY is valid."
|
|
99
|
+
)
|
|
100
|
+
if response.status_code == 403:
|
|
101
|
+
raise RuntimeError("GitHub installation token request failed: forbidden.")
|
|
102
|
+
if response.status_code == 404:
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
"No GitHub installation found for this organization. "
|
|
105
|
+
"Ensure the GitHub App is installed and configured in Dispatch."
|
|
106
|
+
)
|
|
107
|
+
if response.status_code != 200:
|
|
108
|
+
raise RuntimeError(
|
|
109
|
+
f"Failed to fetch GitHub installation token: "
|
|
110
|
+
f"backend returned HTTP {response.status_code}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
data = response.json()
|
|
114
|
+
token: str = data["token"]
|
|
115
|
+
expires_at = datetime.fromisoformat(data["expires_at"])
|
|
116
|
+
if expires_at.tzinfo is None:
|
|
117
|
+
expires_at = expires_at.replace(tzinfo=UTC)
|
|
118
|
+
|
|
119
|
+
result = GitHubAppToken(token=token, expires_at=expires_at)
|
|
120
|
+
_cached_token = (result, expires_at)
|
|
121
|
+
return result
|
|
@@ -470,7 +470,17 @@ class LLMClient:
|
|
|
470
470
|
headers=auth_headers,
|
|
471
471
|
timeout=600.0, # 10min — matches ALB idle timeout for long-context LLM calls
|
|
472
472
|
)
|
|
473
|
-
response.
|
|
473
|
+
if response.status_code >= 400:
|
|
474
|
+
try:
|
|
475
|
+
error_body = response.json()
|
|
476
|
+
detail = error_body.get("detail", response.text)
|
|
477
|
+
except Exception:
|
|
478
|
+
detail = response.text
|
|
479
|
+
raise httpx.HTTPStatusError(
|
|
480
|
+
f"LLM inference failed ({response.status_code}): {detail}",
|
|
481
|
+
request=response.request,
|
|
482
|
+
response=response,
|
|
483
|
+
)
|
|
474
484
|
data = response.json()
|
|
475
485
|
|
|
476
486
|
# Parse tool calls if present
|
|
@@ -764,7 +764,7 @@ async def _call_provider_directly_streaming(
|
|
|
764
764
|
) -> Response:
|
|
765
765
|
"""Call the LLM provider directly with streaming (fallback when backend has no config).
|
|
766
766
|
|
|
767
|
-
Uses raw httpx streaming
|
|
767
|
+
Uses raw httpx streaming.
|
|
768
768
|
Buffers SSE events for usage extraction and logs to /llm/log after stream completes.
|
|
769
769
|
"""
|
|
770
770
|
|
|
@@ -947,10 +947,10 @@ async def _proxy_passthrough(request: Request, provider_format: str) -> Response
|
|
|
947
947
|
)
|
|
948
948
|
|
|
949
949
|
# Build passthrough request for backend.
|
|
950
|
-
# provider_format is no longer sent; the backend derives it from path.
|
|
951
950
|
backend_payload: dict[str, Any] = {
|
|
952
951
|
"path": path,
|
|
953
952
|
"method": method,
|
|
953
|
+
"provider_format": provider_format,
|
|
954
954
|
}
|
|
955
955
|
if body_dict is not None:
|
|
956
956
|
backend_payload["body"] = body_dict
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
|
|
5
|
+
import aiohttp
|
|
5
6
|
import dispatch_agents
|
|
6
7
|
from dispatch_agents import BasePayload, fn, on
|
|
7
8
|
from dispatch_agents.integrations.github import PullRequestReviewCommentCreated
|
|
@@ -11,7 +12,7 @@ from pydantic import Field, PositiveInt
|
|
|
11
12
|
class GreetingPayload(BasePayload):
|
|
12
13
|
"""Input payload for greeting requests."""
|
|
13
14
|
|
|
14
|
-
subject: str = Field(description="The name or subject to greet")
|
|
15
|
+
subject: str = Field(default="World", description="The name or subject to greet")
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class GreetingResponse(BasePayload):
|
|
@@ -110,3 +111,51 @@ async def reverse(payload: ReverseRequest) -> ReverseResponse:
|
|
|
110
111
|
"""Reverse the provided text string."""
|
|
111
112
|
print(f"Reversing: {payload.text!r}")
|
|
112
113
|
return ReverseResponse(reversed_text=payload.text[::-1])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class EgressTestRequest(BasePayload):
|
|
117
|
+
"""Input for the egress test function."""
|
|
118
|
+
|
|
119
|
+
url: str = Field(
|
|
120
|
+
default="https://jsonplaceholder.typicode.com/todos/1",
|
|
121
|
+
description="URL to attempt to fetch",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class EgressTestResponse(BasePayload):
|
|
126
|
+
"""Output of the egress test function."""
|
|
127
|
+
|
|
128
|
+
success: bool = Field(description="Whether the request succeeded")
|
|
129
|
+
status_code: int | None = Field(
|
|
130
|
+
default=None, description="HTTP status code if successful"
|
|
131
|
+
)
|
|
132
|
+
body: str = Field(default="", description="Response body or error message")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@fn()
|
|
136
|
+
async def test_egress(payload: EgressTestRequest) -> EgressTestResponse:
|
|
137
|
+
"""Test outbound HTTP connectivity by fetching a URL.
|
|
138
|
+
|
|
139
|
+
Useful for verifying network egress controls. When network.egress is
|
|
140
|
+
configured, this request will be blocked unless the target domain is
|
|
141
|
+
in allow_domains.
|
|
142
|
+
"""
|
|
143
|
+
print(f"Testing egress to: {payload.url}")
|
|
144
|
+
try:
|
|
145
|
+
async with aiohttp.ClientSession(
|
|
146
|
+
timeout=aiohttp.ClientTimeout(total=10)
|
|
147
|
+
) as session:
|
|
148
|
+
async with session.get(payload.url) as resp:
|
|
149
|
+
body = await resp.text()
|
|
150
|
+
print(f"Response: {resp.status} ({len(body)} bytes)")
|
|
151
|
+
return EgressTestResponse(
|
|
152
|
+
success=True,
|
|
153
|
+
status_code=resp.status,
|
|
154
|
+
body=body[:1000],
|
|
155
|
+
)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
print(f"Request failed: {type(e).__name__}: {e}")
|
|
158
|
+
return EgressTestResponse(
|
|
159
|
+
success=False,
|
|
160
|
+
body=f"{type(e).__name__}: {e}",
|
|
161
|
+
)
|