agentmesh_runtime 3.4.0__tar.gz → 3.6.0__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.
- {agentmesh_runtime-3.4.0 → agentmesh_runtime-3.6.0}/.gitignore +1 -0
- {agentmesh_runtime-3.4.0 → agentmesh_runtime-3.6.0}/PKG-INFO +1 -1
- {agentmesh_runtime-3.4.0 → agentmesh_runtime-3.6.0}/pyproject.toml +1 -1
- {agentmesh_runtime-3.4.0 → agentmesh_runtime-3.6.0}/src/agent_runtime/deploy.py +50 -0
- {agentmesh_runtime-3.4.0 → agentmesh_runtime-3.6.0}/tests/test_deploy.py +109 -0
- {agentmesh_runtime-3.4.0 → agentmesh_runtime-3.6.0}/LICENSE +0 -0
- {agentmesh_runtime-3.4.0 → agentmesh_runtime-3.6.0}/README.md +0 -0
- {agentmesh_runtime-3.4.0 → agentmesh_runtime-3.6.0}/SECURITY.md +0 -0
- {agentmesh_runtime-3.4.0 → agentmesh_runtime-3.6.0}/src/agent_runtime/__init__.py +0 -0
- {agentmesh_runtime-3.4.0 → agentmesh_runtime-3.6.0}/src/agent_runtime/py.typed +0 -0
- {agentmesh_runtime-3.4.0 → agentmesh_runtime-3.6.0}/tests/test_runtime_imports.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentmesh_runtime
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.0
|
|
4
4
|
Summary: Public Preview — AgentMesh Runtime: Execution supervisor for multi-agent sessions with privilege rings, saga orchestration, and audit trails
|
|
5
5
|
Project-URL: Homepage, https://github.com/microsoft/agent-governance-toolkit
|
|
6
6
|
Project-URL: Repository, https://github.com/microsoft/agent-governance-toolkit
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentmesh_runtime"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.6.0"
|
|
8
8
|
description = "Public Preview — AgentMesh Runtime: Execution supervisor for multi-agent sessions with privilege rings, saga orchestration, and audit trails"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
13
|
import logging
|
|
14
|
+
import re
|
|
14
15
|
import shutil
|
|
15
16
|
import subprocess
|
|
16
17
|
import tempfile
|
|
@@ -22,6 +23,47 @@ from typing import Any, Protocol
|
|
|
22
23
|
logger = logging.getLogger(__name__)
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
# Conservative identifier shape that survives every downstream consumer this
|
|
27
|
+
# module hands `agent_id` to:
|
|
28
|
+
#
|
|
29
|
+
# - Docker container names accept `[a-zA-Z0-9][a-zA-Z0-9_.-]+`. Combined
|
|
30
|
+
# with the `agt-` prefix the deployer prepends, a leading dash or dot in
|
|
31
|
+
# `agent_id` cannot trigger Docker's "invalid name" path, but `agent_id`
|
|
32
|
+
# is also embedded raw into `--label agt.agent-id=<id>`, where a value
|
|
33
|
+
# starting with `-` would be reinterpreted as a CLI flag if the order
|
|
34
|
+
# ever changes.
|
|
35
|
+
# - Kubernetes pod/label names follow RFC-1123: `[a-z0-9]([-a-z0-9]*[a-z0-9])?`,
|
|
36
|
+
# 63 chars max. We allow uppercase here because the runtime lowercases
|
|
37
|
+
# when constructing the actual pod name (`agt-<id>`); upstream of that
|
|
38
|
+
# point the value is just an opaque tag.
|
|
39
|
+
#
|
|
40
|
+
# 63 chars matches Kubernetes' label-value length cap; the `agt-` prefix uses
|
|
41
|
+
# the remaining 4 chars.
|
|
42
|
+
_AGENT_ID_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _validate_agent_id(agent_id: str) -> str:
|
|
46
|
+
"""Reject ``agent_id`` values that would be unsafe to interpolate into
|
|
47
|
+
Docker / kubectl command-lines and label values.
|
|
48
|
+
|
|
49
|
+
Returns the validated id unchanged so call sites can write
|
|
50
|
+
``agent_id = _validate_agent_id(agent_id)`` as a single guard.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: if ``agent_id`` is empty, starts with a dash (would be
|
|
54
|
+
reparsed as a CLI flag), exceeds 63 characters, or contains any
|
|
55
|
+
character outside ``[a-zA-Z0-9_-]``.
|
|
56
|
+
"""
|
|
57
|
+
if not isinstance(agent_id, str) or not _AGENT_ID_PATTERN.match(agent_id):
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"invalid agent_id {agent_id!r}: must match "
|
|
60
|
+
f"^[a-zA-Z0-9][a-zA-Z0-9_-]{{0,62}}$ "
|
|
61
|
+
"(alphanumeric start, then alphanumerics/underscores/dashes, "
|
|
62
|
+
"max 63 chars)"
|
|
63
|
+
)
|
|
64
|
+
return agent_id
|
|
65
|
+
|
|
66
|
+
|
|
25
67
|
class DeploymentStatus(str, Enum):
|
|
26
68
|
PENDING = "pending"
|
|
27
69
|
DEPLOYING = "deploying"
|
|
@@ -91,6 +133,7 @@ class DockerDeployer:
|
|
|
91
133
|
|
|
92
134
|
def deploy(self, agent_id: str, image: str, config: GovernanceConfig,
|
|
93
135
|
port: int = 0, env: dict[str, str] | None = None, **kwargs: Any) -> DeploymentResult:
|
|
136
|
+
agent_id = _validate_agent_id(agent_id)
|
|
94
137
|
container_name = f"agt-{agent_id}"
|
|
95
138
|
cmd = [
|
|
96
139
|
"run", "-d",
|
|
@@ -141,6 +184,7 @@ class DockerDeployer:
|
|
|
141
184
|
)
|
|
142
185
|
|
|
143
186
|
def stop(self, agent_id: str) -> DeploymentResult:
|
|
187
|
+
agent_id = _validate_agent_id(agent_id)
|
|
144
188
|
container_name = f"agt-{agent_id}"
|
|
145
189
|
try:
|
|
146
190
|
self._run(["stop", container_name])
|
|
@@ -159,6 +203,7 @@ class DockerDeployer:
|
|
|
159
203
|
)
|
|
160
204
|
|
|
161
205
|
def status(self, agent_id: str) -> DeploymentResult:
|
|
206
|
+
agent_id = _validate_agent_id(agent_id)
|
|
162
207
|
container_name = f"agt-{agent_id}"
|
|
163
208
|
try:
|
|
164
209
|
result = self._run(["inspect", container_name, "--format", "{{.State.Status}}"])
|
|
@@ -178,6 +223,7 @@ class DockerDeployer:
|
|
|
178
223
|
)
|
|
179
224
|
|
|
180
225
|
def logs(self, agent_id: str, tail: int = 100) -> str:
|
|
226
|
+
agent_id = _validate_agent_id(agent_id)
|
|
181
227
|
try:
|
|
182
228
|
result = self._run(["logs", f"agt-{agent_id}", "--tail", str(tail)])
|
|
183
229
|
return result.stdout
|
|
@@ -270,6 +316,7 @@ class KubernetesDeployer:
|
|
|
270
316
|
}
|
|
271
317
|
|
|
272
318
|
def deploy(self, agent_id: str, image: str, config: GovernanceConfig, **kwargs: Any) -> DeploymentResult:
|
|
319
|
+
agent_id = _validate_agent_id(agent_id)
|
|
273
320
|
manifest = self._build_pod_manifest(agent_id, image, config)
|
|
274
321
|
manifest_json = json.dumps(manifest)
|
|
275
322
|
try:
|
|
@@ -297,6 +344,7 @@ class KubernetesDeployer:
|
|
|
297
344
|
)
|
|
298
345
|
|
|
299
346
|
def stop(self, agent_id: str) -> DeploymentResult:
|
|
347
|
+
agent_id = _validate_agent_id(agent_id)
|
|
300
348
|
try:
|
|
301
349
|
self._run(["delete", "pod", f"agt-{agent_id}", "-n", self._namespace])
|
|
302
350
|
return DeploymentResult(
|
|
@@ -313,6 +361,7 @@ class KubernetesDeployer:
|
|
|
313
361
|
)
|
|
314
362
|
|
|
315
363
|
def status(self, agent_id: str) -> DeploymentResult:
|
|
364
|
+
agent_id = _validate_agent_id(agent_id)
|
|
316
365
|
try:
|
|
317
366
|
result = self._run([
|
|
318
367
|
"get", "pod", f"agt-{agent_id}",
|
|
@@ -339,6 +388,7 @@ class KubernetesDeployer:
|
|
|
339
388
|
)
|
|
340
389
|
|
|
341
390
|
def logs(self, agent_id: str, tail: int = 100) -> str:
|
|
391
|
+
agent_id = _validate_agent_id(agent_id)
|
|
342
392
|
try:
|
|
343
393
|
result = self._run([
|
|
344
394
|
"logs", f"agt-{agent_id}",
|
|
@@ -14,6 +14,7 @@ from agent_runtime.deploy import (
|
|
|
14
14
|
DockerDeployer,
|
|
15
15
|
GovernanceConfig,
|
|
16
16
|
KubernetesDeployer,
|
|
17
|
+
_validate_agent_id,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
@@ -355,3 +356,111 @@ class TestKubernetesDeployer:
|
|
|
355
356
|
mock_run.return_value = MagicMock(stdout="k8s log line\n", stderr="", returncode=0)
|
|
356
357
|
deployer = KubernetesDeployer()
|
|
357
358
|
assert "k8s log line" in deployer.logs("agent-1")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
# agent_id validation
|
|
363
|
+
# ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
class TestValidateAgentId:
|
|
366
|
+
"""`_validate_agent_id` is the single guard for any string interpolated
|
|
367
|
+
into a Docker container name, kubectl pod name, or label value. The
|
|
368
|
+
regex pins the conservative shape that survives every downstream
|
|
369
|
+
consumer: alphanumeric start, alphanumerics + ``_-`` thereafter,
|
|
370
|
+
63 chars max.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
@pytest.mark.parametrize("agent_id", [
|
|
374
|
+
"a",
|
|
375
|
+
"analyst-001",
|
|
376
|
+
"agent_42",
|
|
377
|
+
"A",
|
|
378
|
+
"9-already-leading-digit",
|
|
379
|
+
"x" * 63, # max length
|
|
380
|
+
])
|
|
381
|
+
def test_accepts_well_formed_ids(self, agent_id: str) -> None:
|
|
382
|
+
assert _validate_agent_id(agent_id) == agent_id
|
|
383
|
+
|
|
384
|
+
@pytest.mark.parametrize("agent_id,reason", [
|
|
385
|
+
("", "empty"),
|
|
386
|
+
("-rm", "leading dash would reparse as docker CLI flag"),
|
|
387
|
+
("--privileged", "leading dash, looks like a flag"),
|
|
388
|
+
("a b", "whitespace"),
|
|
389
|
+
("a;rm -rf /", "shell metachars"),
|
|
390
|
+
("a$(id)", "command substitution"),
|
|
391
|
+
("a`id`", "backtick substitution"),
|
|
392
|
+
("a.b", "dot — RFC-1123 allows but conservative regex does not"),
|
|
393
|
+
("a/b", "path separator"),
|
|
394
|
+
("a:b", "colon — would break docker label syntax"),
|
|
395
|
+
("agent\nname", "newline injection"),
|
|
396
|
+
("agent\x00name", "NUL byte"),
|
|
397
|
+
("_leading_underscore", "underscore start — keep alphanumeric-only first char"),
|
|
398
|
+
("x" * 64, "one over the 63-char cap"),
|
|
399
|
+
])
|
|
400
|
+
def test_rejects_malformed_ids(self, agent_id: str, reason: str) -> None:
|
|
401
|
+
with pytest.raises(ValueError, match="invalid agent_id"):
|
|
402
|
+
_validate_agent_id(agent_id)
|
|
403
|
+
|
|
404
|
+
def test_rejects_non_str(self) -> None:
|
|
405
|
+
with pytest.raises(ValueError, match="invalid agent_id"):
|
|
406
|
+
_validate_agent_id(None) # type: ignore[arg-type]
|
|
407
|
+
with pytest.raises(ValueError, match="invalid agent_id"):
|
|
408
|
+
_validate_agent_id(42) # type: ignore[arg-type]
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class TestDockerDeployerAgentIdValidation:
|
|
412
|
+
"""Every `DockerDeployer` public entry point must validate `agent_id`
|
|
413
|
+
before interpolating it into a docker command-line. The previous
|
|
414
|
+
behaviour silently forwarded `agent_id="-rm"` etc., where the leading
|
|
415
|
+
dash would be reparsed as a CLI flag depending on Docker version.
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
@patch("agent_runtime.deploy.shutil.which", return_value="/usr/bin/docker")
|
|
419
|
+
def test_deploy_rejects_leading_dash(self, mock_which: MagicMock) -> None:
|
|
420
|
+
deployer = DockerDeployer()
|
|
421
|
+
with pytest.raises(ValueError, match="invalid agent_id"):
|
|
422
|
+
deployer.deploy("-rm", "img:latest", GovernanceConfig())
|
|
423
|
+
|
|
424
|
+
@patch("agent_runtime.deploy.shutil.which", return_value="/usr/bin/docker")
|
|
425
|
+
def test_stop_rejects_leading_dash(self, mock_which: MagicMock) -> None:
|
|
426
|
+
deployer = DockerDeployer()
|
|
427
|
+
with pytest.raises(ValueError, match="invalid agent_id"):
|
|
428
|
+
deployer.stop("-rm")
|
|
429
|
+
|
|
430
|
+
@patch("agent_runtime.deploy.shutil.which", return_value="/usr/bin/docker")
|
|
431
|
+
def test_status_rejects_shell_metachars(self, mock_which: MagicMock) -> None:
|
|
432
|
+
deployer = DockerDeployer()
|
|
433
|
+
with pytest.raises(ValueError, match="invalid agent_id"):
|
|
434
|
+
deployer.status("a;rm -rf /")
|
|
435
|
+
|
|
436
|
+
@patch("agent_runtime.deploy.shutil.which", return_value="/usr/bin/docker")
|
|
437
|
+
def test_logs_rejects_empty(self, mock_which: MagicMock) -> None:
|
|
438
|
+
deployer = DockerDeployer()
|
|
439
|
+
with pytest.raises(ValueError, match="invalid agent_id"):
|
|
440
|
+
deployer.logs("")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class TestKubernetesDeployerAgentIdValidation:
|
|
444
|
+
@patch("agent_runtime.deploy.shutil.which", return_value="/usr/bin/kubectl")
|
|
445
|
+
def test_deploy_rejects_overlong(self, mock_which: MagicMock) -> None:
|
|
446
|
+
deployer = KubernetesDeployer()
|
|
447
|
+
with pytest.raises(ValueError, match="invalid agent_id"):
|
|
448
|
+
deployer.deploy("x" * 64, "img:latest", GovernanceConfig())
|
|
449
|
+
|
|
450
|
+
@patch("agent_runtime.deploy.shutil.which", return_value="/usr/bin/kubectl")
|
|
451
|
+
def test_stop_rejects_leading_dash(self, mock_which: MagicMock) -> None:
|
|
452
|
+
deployer = KubernetesDeployer()
|
|
453
|
+
with pytest.raises(ValueError, match="invalid agent_id"):
|
|
454
|
+
deployer.stop("-rm")
|
|
455
|
+
|
|
456
|
+
@patch("agent_runtime.deploy.shutil.which", return_value="/usr/bin/kubectl")
|
|
457
|
+
def test_status_rejects_dot(self, mock_which: MagicMock) -> None:
|
|
458
|
+
deployer = KubernetesDeployer()
|
|
459
|
+
with pytest.raises(ValueError, match="invalid agent_id"):
|
|
460
|
+
deployer.status("a.b")
|
|
461
|
+
|
|
462
|
+
@patch("agent_runtime.deploy.shutil.which", return_value="/usr/bin/kubectl")
|
|
463
|
+
def test_logs_rejects_newline(self, mock_which: MagicMock) -> None:
|
|
464
|
+
deployer = KubernetesDeployer()
|
|
465
|
+
with pytest.raises(ValueError, match="invalid agent_id"):
|
|
466
|
+
deployer.logs("agent\nname")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|