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.
@@ -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.5.0
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.5.0"
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
- tmp_dir = Path(tempfile.gettempdir())
279
- manifest_path = tmp_dir / f"agt-{agent_id}-manifest.json"
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")