agentmesh_runtime 3.5.0__tar.gz → 3.7.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.5.0 → agentmesh_runtime-3.7.0}/.gitignore +2 -0
- {agentmesh_runtime-3.5.0 → agentmesh_runtime-3.7.0}/PKG-INFO +1 -1
- {agentmesh_runtime-3.5.0 → agentmesh_runtime-3.7.0}/pyproject.toml +1 -1
- {agentmesh_runtime-3.5.0 → agentmesh_runtime-3.7.0}/src/agent_runtime/deploy.py +54 -2
- {agentmesh_runtime-3.5.0 → agentmesh_runtime-3.7.0}/tests/test_deploy.py +109 -0
- {agentmesh_runtime-3.5.0 → agentmesh_runtime-3.7.0}/LICENSE +0 -0
- {agentmesh_runtime-3.5.0 → agentmesh_runtime-3.7.0}/README.md +0 -0
- {agentmesh_runtime-3.5.0 → agentmesh_runtime-3.7.0}/SECURITY.md +0 -0
- {agentmesh_runtime-3.5.0 → agentmesh_runtime-3.7.0}/src/agent_runtime/__init__.py +0 -0
- {agentmesh_runtime-3.5.0 → agentmesh_runtime-3.7.0}/src/agent_runtime/py.typed +0 -0
- {agentmesh_runtime-3.5.0 → agentmesh_runtime-3.7.0}/tests/test_runtime_imports.py +0 -0
|
@@ -69,6 +69,7 @@ bld/
|
|
|
69
69
|
|
|
70
70
|
# Build results on 'Bin' directories
|
|
71
71
|
**/[Bb]in/*
|
|
72
|
+
!agent-governance-copilot-cli/bin/agt-copilot.mjs
|
|
72
73
|
# Uncomment if you have tasks that rely on *.refresh files to move binaries
|
|
73
74
|
# (https://github.com/github/gitignore/pull/3736)
|
|
74
75
|
#!**/[Bb]in/*.refresh
|
|
@@ -465,3 +466,4 @@ _site/
|
|
|
465
466
|
|
|
466
467
|
# Code Security Assessment artifacts
|
|
467
468
|
.security-assessment/
|
|
469
|
+
*.tgz
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentmesh_runtime
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.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.7.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,8 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
13
|
import logging
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
14
16
|
import shutil
|
|
15
17
|
import subprocess
|
|
16
18
|
import tempfile
|
|
@@ -22,6 +24,47 @@ from typing import Any, Protocol
|
|
|
22
24
|
logger = logging.getLogger(__name__)
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
# Conservative identifier shape that survives every downstream consumer this
|
|
28
|
+
# module hands `agent_id` to:
|
|
29
|
+
#
|
|
30
|
+
# - Docker container names accept `[a-zA-Z0-9][a-zA-Z0-9_.-]+`. Combined
|
|
31
|
+
# with the `agt-` prefix the deployer prepends, a leading dash or dot in
|
|
32
|
+
# `agent_id` cannot trigger Docker's "invalid name" path, but `agent_id`
|
|
33
|
+
# is also embedded raw into `--label agt.agent-id=<id>`, where a value
|
|
34
|
+
# starting with `-` would be reinterpreted as a CLI flag if the order
|
|
35
|
+
# ever changes.
|
|
36
|
+
# - Kubernetes pod/label names follow RFC-1123: `[a-z0-9]([-a-z0-9]*[a-z0-9])?`,
|
|
37
|
+
# 63 chars max. We allow uppercase here because the runtime lowercases
|
|
38
|
+
# when constructing the actual pod name (`agt-<id>`); upstream of that
|
|
39
|
+
# point the value is just an opaque tag.
|
|
40
|
+
#
|
|
41
|
+
# 63 chars matches Kubernetes' label-value length cap; the `agt-` prefix uses
|
|
42
|
+
# the remaining 4 chars.
|
|
43
|
+
_AGENT_ID_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}$")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _validate_agent_id(agent_id: str) -> str:
|
|
47
|
+
"""Reject ``agent_id`` values that would be unsafe to interpolate into
|
|
48
|
+
Docker / kubectl command-lines and label values.
|
|
49
|
+
|
|
50
|
+
Returns the validated id unchanged so call sites can write
|
|
51
|
+
``agent_id = _validate_agent_id(agent_id)`` as a single guard.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: if ``agent_id`` is empty, starts with a dash (would be
|
|
55
|
+
reparsed as a CLI flag), exceeds 63 characters, or contains any
|
|
56
|
+
character outside ``[a-zA-Z0-9_-]``.
|
|
57
|
+
"""
|
|
58
|
+
if not isinstance(agent_id, str) or not _AGENT_ID_PATTERN.match(agent_id):
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"invalid agent_id {agent_id!r}: must match "
|
|
61
|
+
f"^[a-zA-Z0-9][a-zA-Z0-9_-]{{0,62}}$ "
|
|
62
|
+
"(alphanumeric start, then alphanumerics/underscores/dashes, "
|
|
63
|
+
"max 63 chars)"
|
|
64
|
+
)
|
|
65
|
+
return agent_id
|
|
66
|
+
|
|
67
|
+
|
|
25
68
|
class DeploymentStatus(str, Enum):
|
|
26
69
|
PENDING = "pending"
|
|
27
70
|
DEPLOYING = "deploying"
|
|
@@ -91,6 +134,7 @@ class DockerDeployer:
|
|
|
91
134
|
|
|
92
135
|
def deploy(self, agent_id: str, image: str, config: GovernanceConfig,
|
|
93
136
|
port: int = 0, env: dict[str, str] | None = None, **kwargs: Any) -> DeploymentResult:
|
|
137
|
+
agent_id = _validate_agent_id(agent_id)
|
|
94
138
|
container_name = f"agt-{agent_id}"
|
|
95
139
|
cmd = [
|
|
96
140
|
"run", "-d",
|
|
@@ -141,6 +185,7 @@ class DockerDeployer:
|
|
|
141
185
|
)
|
|
142
186
|
|
|
143
187
|
def stop(self, agent_id: str) -> DeploymentResult:
|
|
188
|
+
agent_id = _validate_agent_id(agent_id)
|
|
144
189
|
container_name = f"agt-{agent_id}"
|
|
145
190
|
try:
|
|
146
191
|
self._run(["stop", container_name])
|
|
@@ -159,6 +204,7 @@ class DockerDeployer:
|
|
|
159
204
|
)
|
|
160
205
|
|
|
161
206
|
def status(self, agent_id: str) -> DeploymentResult:
|
|
207
|
+
agent_id = _validate_agent_id(agent_id)
|
|
162
208
|
container_name = f"agt-{agent_id}"
|
|
163
209
|
try:
|
|
164
210
|
result = self._run(["inspect", container_name, "--format", "{{.State.Status}}"])
|
|
@@ -178,6 +224,7 @@ class DockerDeployer:
|
|
|
178
224
|
)
|
|
179
225
|
|
|
180
226
|
def logs(self, agent_id: str, tail: int = 100) -> str:
|
|
227
|
+
agent_id = _validate_agent_id(agent_id)
|
|
181
228
|
try:
|
|
182
229
|
result = self._run(["logs", f"agt-{agent_id}", "--tail", str(tail)])
|
|
183
230
|
return result.stdout
|
|
@@ -270,14 +317,16 @@ class KubernetesDeployer:
|
|
|
270
317
|
}
|
|
271
318
|
|
|
272
319
|
def deploy(self, agent_id: str, image: str, config: GovernanceConfig, **kwargs: Any) -> DeploymentResult:
|
|
320
|
+
agent_id = _validate_agent_id(agent_id)
|
|
273
321
|
manifest = self._build_pod_manifest(agent_id, image, config)
|
|
274
322
|
manifest_json = json.dumps(manifest)
|
|
275
323
|
try:
|
|
276
324
|
# Ensure namespace
|
|
277
325
|
self._run(["create", "namespace", self._namespace], check=False)
|
|
278
|
-
|
|
279
|
-
manifest_path =
|
|
326
|
+
fd, tmp_path = tempfile.mkstemp(prefix=f"agt-{agent_id}-", suffix=".json")
|
|
327
|
+
manifest_path = Path(tmp_path)
|
|
280
328
|
try:
|
|
329
|
+
os.close(fd)
|
|
281
330
|
manifest_path.write_text(manifest_json, encoding="utf-8")
|
|
282
331
|
self._run(["apply", "-f", str(manifest_path)])
|
|
283
332
|
finally:
|
|
@@ -297,6 +346,7 @@ class KubernetesDeployer:
|
|
|
297
346
|
)
|
|
298
347
|
|
|
299
348
|
def stop(self, agent_id: str) -> DeploymentResult:
|
|
349
|
+
agent_id = _validate_agent_id(agent_id)
|
|
300
350
|
try:
|
|
301
351
|
self._run(["delete", "pod", f"agt-{agent_id}", "-n", self._namespace])
|
|
302
352
|
return DeploymentResult(
|
|
@@ -313,6 +363,7 @@ class KubernetesDeployer:
|
|
|
313
363
|
)
|
|
314
364
|
|
|
315
365
|
def status(self, agent_id: str) -> DeploymentResult:
|
|
366
|
+
agent_id = _validate_agent_id(agent_id)
|
|
316
367
|
try:
|
|
317
368
|
result = self._run([
|
|
318
369
|
"get", "pod", f"agt-{agent_id}",
|
|
@@ -339,6 +390,7 @@ class KubernetesDeployer:
|
|
|
339
390
|
)
|
|
340
391
|
|
|
341
392
|
def logs(self, agent_id: str, tail: int = 100) -> str:
|
|
393
|
+
agent_id = _validate_agent_id(agent_id)
|
|
342
394
|
try:
|
|
343
395
|
result = self._run([
|
|
344
396
|
"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
|