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.
@@ -465,3 +465,4 @@ _site/
465
465
 
466
466
  # Code Security Assessment artifacts
467
467
  .security-assessment/
468
+ *.tgz
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentmesh_runtime
3
- Version: 3.4.0
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.4.0"
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")