kaos-cli 0.7.7__tar.gz → 0.8.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.
Files changed (74) hide show
  1. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/PKG-INFO +1 -1
  2. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/__init__.py +1 -1
  3. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/install/__init__.py +40 -5
  4. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/install/auth.py +306 -33
  5. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/install/infra.py +26 -0
  6. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli.egg-info/PKG-INFO +1 -1
  7. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/pyproject.toml +1 -1
  8. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/tests/test_cli_integration.py +164 -22
  9. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/README.md +0 -0
  10. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/agent/__init__.py +0 -0
  11. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/agent/a2a.py +0 -0
  12. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/agent/build.py +0 -0
  13. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/agent/deploy.py +0 -0
  14. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/agent/init.py +0 -0
  15. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/agent/invoke.py +0 -0
  16. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/agent/memory.py +0 -0
  17. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/agent/run.py +0 -0
  18. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/agent/status.py +0 -0
  19. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/agent/tools.py +0 -0
  20. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/auth/__init__.py +0 -0
  21. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/auth/consent.py +0 -0
  22. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/auth/grant.py +0 -0
  23. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/auth/login.py +0 -0
  24. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/cluster_http.py +0 -0
  25. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/config.py +0 -0
  26. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/config_cli.py +0 -0
  27. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/install/observability.py +0 -0
  28. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/main.py +0 -0
  29. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/mcp/__init__.py +0 -0
  30. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/mcp/build.py +0 -0
  31. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/mcp/crud.py +0 -0
  32. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/mcp/deploy.py +0 -0
  33. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/mcp/init.py +0 -0
  34. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/mcp/invoke.py +0 -0
  35. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/mcp/run.py +0 -0
  36. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/memory/__init__.py +0 -0
  37. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/memorystore/__init__.py +0 -0
  38. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/memorystore/create.py +0 -0
  39. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/modelapi/__init__.py +0 -0
  40. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/modelapi/deploy.py +0 -0
  41. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/modelapi/invoke.py +0 -0
  42. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/proxy.py +0 -0
  43. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/samples/__init__.py +0 -0
  44. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/samples/data/1-simple-echo-agent.yaml +0 -0
  45. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/samples/data/2-multi-agent-mcp.yaml +0 -0
  46. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/samples/data/3-hierarchical-agents.yaml +0 -0
  47. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/samples/data/4-dev-ollama-proxy-agent.yaml +0 -0
  48. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/samples/data/5-proxy-external-api.yaml +0 -0
  49. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/samples/data/6-autonomous-monitor.yaml +0 -0
  50. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/samples/data/7-memory-agent.yaml +0 -0
  51. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/samples/data/8-authorization-walkthrough.yaml +0 -0
  52. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/system/__init__.py +0 -0
  53. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/system/create_rbac.py +0 -0
  54. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/system/install.py +0 -0
  55. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/system/runtimes.py +0 -0
  56. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/system/status.py +0 -0
  57. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/ui.py +0 -0
  58. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/utils/__init__.py +0 -0
  59. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/utils/crud.py +0 -0
  60. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/utils/port_forward.py +0 -0
  61. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli/utils/wait.py +0 -0
  62. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli.egg-info/SOURCES.txt +0 -0
  63. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli.egg-info/dependency_links.txt +0 -0
  64. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli.egg-info/entry_points.txt +0 -0
  65. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli.egg-info/requires.txt +0 -0
  66. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/kaos_cli.egg-info/top_level.txt +0 -0
  67. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/setup.cfg +0 -0
  68. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/tests/test_agent_invoke.py +0 -0
  69. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/tests/test_agent_tools.py +0 -0
  70. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/tests/test_auth_cli.py +0 -0
  71. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/tests/test_config_cli.py +0 -0
  72. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/tests/test_deploy_wait.py +0 -0
  73. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/tests/test_install_surface.py +0 -0
  74. {kaos_cli-0.7.7 → kaos_cli-0.8.0}/tests/test_memory_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kaos-cli
3
- Version: 0.7.7
3
+ Version: 0.8.0
4
4
  Summary: CLI for KAOS (K8s Agent Orchestration System)
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -5,4 +5,4 @@ try:
5
5
 
6
6
  __version__ = version("kaos-cli")
7
7
  except Exception:
8
- __version__ = "0.7.5.dev0"
8
+ __version__ = "0.7.8.dev0"
@@ -38,6 +38,9 @@ DEFAULT_CREDENTIAL_SECRET_PREFIX = "kaos-aib"
38
38
  AUTH_ENDUSER_PORT = 8000
39
39
  AUTH_ADMIN_PORT = 14000
40
40
  AUTH_EXTPROC_PORT = 50051
41
+ DEFAULT_AIB_EXTPROC_IMAGE = (
42
+ "ghcr.io/zalando-incubator/agentic-identity-broker-extproc:v0.1.8"
43
+ )
41
44
 
42
45
 
43
46
  # Independent agent and user identity modes selected by the install flags.
@@ -121,8 +124,8 @@ from .auth import (
121
124
  _build_aib_broker_public_url_args, _build_auth_operator_args,
122
125
  _build_token_exchange_aib_args, _default_auth_admin_url,
123
126
  _default_auth_issuer, _default_user_auth_issuer, _install_aib,
124
- _install_keycloak, _keycloak_dev_manifests, _keycloak_realm_configmap_name,
125
- _keycloak_realm_json,
127
+ _install_aib_extproc, _install_keycloak, _keycloak_dev_manifests,
128
+ _keycloak_realm_configmap_name, _keycloak_realm_json,
126
129
  )
127
130
 
128
131
 
@@ -248,8 +251,23 @@ def install_command(
248
251
  keycloak_namespace, keycloak_release
249
252
  )
250
253
 
251
- # Install the identity broker from a local chart when provided (it is
252
- # unpublished, so a chart path is required to install it here).
254
+ # In exchange mode the public broker and extProc both contact Keycloak
255
+ # during startup, so apply Keycloak first.
256
+ if token_exchange_enabled:
257
+ if not _install_keycloak(
258
+ keycloak_namespace,
259
+ keycloak_release,
260
+ DEFAULT_USER_AUTH_REALM,
261
+ user_auth_audience,
262
+ keycloak_chart_path,
263
+ wait,
264
+ token_exchange_enabled,
265
+ ):
266
+ typer.echo(
267
+ "Warning: Keycloak installation failed, continuing...", err=True
268
+ )
269
+
270
+ # Install the broker from the public chart checkout when provided.
253
271
  if (identity_provider == "aib" or token_exchange_enabled) and aib_chart_path:
254
272
  if token_exchange_enabled:
255
273
  keycloak_issuer = user_auth_issuer or _default_user_auth_issuer(
@@ -269,6 +287,7 @@ def install_command(
269
287
  aib_values_path,
270
288
  wait,
271
289
  extra_set=aib_extra_set,
290
+ seed_identity_bootstrap=identity_provider == "aib",
272
291
  ):
273
292
  typer.echo(
274
293
  "Warning: identity broker installation failed, continuing...",
@@ -285,7 +304,7 @@ def install_command(
285
304
 
286
305
  # Keycloak backs the user plane and the OIDC DCR agent mode. Install it
287
306
  # when either plane selects it.
288
- if user_auth or identity_provider == "oidc":
307
+ if (user_auth or identity_provider == "oidc") and not token_exchange_enabled:
289
308
  if not _install_keycloak(
290
309
  keycloak_namespace,
291
310
  keycloak_release,
@@ -299,6 +318,17 @@ def install_command(
299
318
  "Warning: Keycloak installation failed, continuing...", err=True
300
319
  )
301
320
 
321
+ if token_exchange_enabled and not _install_aib_extproc(
322
+ auth_namespace,
323
+ auth_release,
324
+ user_auth_issuer
325
+ or _default_user_auth_issuer(keycloak_namespace, keycloak_release),
326
+ wait,
327
+ ):
328
+ typer.echo(
329
+ "Warning: AIB extProc installation failed, continuing...", err=True
330
+ )
331
+
302
332
  # Phase 2: Wait for infra that the operator depends on
303
333
  if gateway_enabled:
304
334
  _wait_for_gateway_class()
@@ -432,6 +462,11 @@ def install_command(
432
462
  policy_rego_override=policy_rego_override,
433
463
  policy_configmap_name=policy_configmap_name or "",
434
464
  policy_configmap_namespace=policy_configmap_namespace or "",
465
+ default_permission_set=(
466
+ "kaos-identity"
467
+ if identity_provider == "aib" and aib_chart_path
468
+ else ""
469
+ ),
435
470
  )
436
471
  )
437
472
  if token_exchange_enabled:
@@ -1,11 +1,16 @@
1
1
  """Authentication provider installation and Helm argument helpers."""
2
2
 
3
3
  import json
4
+ from urllib.parse import urlsplit
4
5
 
6
+ import httpx
5
7
  import typer
6
8
 
9
+ from kaos_cli.cluster_http import local_service_url
10
+
7
11
  from . import (
8
- AUTH_ADMIN_PORT, AUTH_ENDUSER_PORT, DEFAULT_AGENT_AUTH_AUDIENCE,
12
+ AUTH_ADMIN_PORT, AUTH_ENDUSER_PORT, AUTH_EXTPROC_PORT,
13
+ DEFAULT_AGENT_AUTH_AUDIENCE, DEFAULT_AIB_EXTPROC_IMAGE,
9
14
  DEFAULT_KEYCLOAK_ADMIN_PASSWORD, DEFAULT_KEYCLOAK_ADMIN_USER,
10
15
  DEFAULT_KEYCLOAK_IMAGE, DEFAULT_OIDC_REGISTRATION_SECRET_KEY,
11
16
  DEFAULT_OIDC_REGISTRATION_SECRET_NAME, DEFAULT_TOKEN_EXCHANGE_AUDIENCE,
@@ -64,6 +69,7 @@ def _build_auth_operator_args(
64
69
  policy_rego_override: bool = False,
65
70
  policy_configmap_name: str = "",
66
71
  policy_configmap_namespace: str = "",
72
+ default_permission_set: str = "",
67
73
  ) -> list[str]:
68
74
  """Build the operator Helm --set arguments that enable agent-auth wiring.
69
75
 
@@ -101,6 +107,14 @@ def _build_auth_operator_args(
101
107
  )
102
108
  if identity_provider == "aib" and admin_url:
103
109
  args.extend(["--set", f"security.agentAuth.adminUrl={admin_url}"])
110
+ if default_permission_set:
111
+ args.extend(
112
+ [
113
+ "--set",
114
+ "security.agentAuth.defaultPermissionSet="
115
+ f"{default_permission_set}",
116
+ ]
117
+ )
104
118
  if identity_provider == "oidc" and oidc_registration_secret_name:
105
119
  args.extend(
106
120
  [
@@ -179,8 +193,9 @@ def _install_aib(
179
193
  values_path: str | None,
180
194
  wait: bool,
181
195
  extra_set: list[str] | None = None,
196
+ seed_identity_bootstrap: bool = True,
182
197
  ) -> bool:
183
- """Install the identity broker from a local chart (unpublished/dev path)."""
198
+ """Install the public identity-broker chart from a local v0.1.8 checkout."""
184
199
  typer.echo("Installing identity broker...")
185
200
  helm_args = [
186
201
  "upgrade",
@@ -203,9 +218,111 @@ def _install_aib(
203
218
  typer.echo(f"Error installing identity broker: {result.stderr}", err=True)
204
219
  return False
205
220
 
221
+ if seed_identity_bootstrap and not _seed_aib_identity_bootstrap(
222
+ namespace, release
223
+ ):
224
+ return False
225
+
206
226
  typer.echo(f"✅ Identity broker installed in '{namespace}' namespace")
207
227
  return True
208
228
 
229
+
230
+ AIB_BOOTSTRAP_SERVICE = "kaos-identity-placeholder"
231
+ AIB_BOOTSTRAP_PERMISSION_SET = "kaos-identity"
232
+ AIB_BOOTSTRAP_DESCRIPTION = (
233
+ "KAOS identity-only bootstrap; remove once AIB allows agents without permission sets"
234
+ )
235
+
236
+
237
+ def _items(response: httpx.Response) -> list[dict]:
238
+ response.raise_for_status()
239
+ payload = response.json()
240
+ return payload if isinstance(payload, list) else payload.get("items", [])
241
+
242
+
243
+ def _seed_aib_identity_bootstrap(namespace: str, release: str) -> bool:
244
+ """Idempotently seed the public AIB permission-set registration bridge."""
245
+ rollout = _root()._run_kubectl(
246
+ [
247
+ "rollout",
248
+ "status",
249
+ f"deployment/{_auth_broker_fullname(release)}",
250
+ "-n",
251
+ namespace,
252
+ "--timeout=120s",
253
+ ],
254
+ check=False,
255
+ )
256
+ if rollout.returncode != 0:
257
+ typer.echo(f"Error waiting for identity broker: {rollout.stderr}", err=True)
258
+ return False
259
+
260
+ admin_url = _default_auth_admin_url(namespace, release)
261
+ headers = {
262
+ "Host": urlsplit(admin_url).netloc,
263
+ "X-Remote-User": "kaos-operator",
264
+ }
265
+ try:
266
+ with local_service_url(admin_url) as local_url:
267
+ base = local_url.rstrip("/")
268
+ with httpx.Client(headers=headers, timeout=30.0) as client:
269
+ services = _items(client.get(f"{base}/services"))
270
+ service = next(
271
+ (
272
+ item
273
+ for item in services
274
+ if item.get("canonical_id") == AIB_BOOTSTRAP_SERVICE
275
+ or item.get("display_name") == AIB_BOOTSTRAP_SERVICE
276
+ ),
277
+ None,
278
+ )
279
+ if service is None:
280
+ response = client.post(
281
+ f"{base}/services",
282
+ json={
283
+ "canonical_id": AIB_BOOTSTRAP_SERVICE,
284
+ # AIB services have no description field, so the
285
+ # descriptive marker lives in display_name.
286
+ "display_name": AIB_BOOTSTRAP_DESCRIPTION,
287
+ "client_id": AIB_BOOTSTRAP_SERVICE,
288
+ "token_endpoint_auth_method": "none",
289
+ "issuer_uri": "https://kaos-identity-placeholder.invalid",
290
+ "discovery": {"enable_discovery": False},
291
+ "endpoints": {
292
+ "authorize_endpoint": "https://kaos-identity-placeholder.invalid/oauth2/authorize",
293
+ "token_endpoint": "https://kaos-identity-placeholder.invalid/oauth2/token",
294
+ },
295
+ "scopes": [],
296
+ },
297
+ )
298
+ response.raise_for_status()
299
+ service = response.json()
300
+
301
+ permission_sets = _items(client.get(f"{base}/permission-sets"))
302
+ if not any(
303
+ item.get("name") == AIB_BOOTSTRAP_PERMISSION_SET
304
+ for item in permission_sets
305
+ ):
306
+ response = client.post(
307
+ f"{base}/permission-sets",
308
+ json={
309
+ "canonical_id": AIB_BOOTSTRAP_PERMISSION_SET,
310
+ "name": AIB_BOOTSTRAP_PERMISSION_SET,
311
+ "description": AIB_BOOTSTRAP_DESCRIPTION,
312
+ "service_scopes": [
313
+ {"service_id": service["id"], "scopes": []}
314
+ ],
315
+ },
316
+ )
317
+ response.raise_for_status()
318
+ except (httpx.HTTPError, KeyError, RuntimeError, ValueError) as exc:
319
+ typer.echo(f"Error seeding identity broker: {exc}", err=True)
320
+ return False
321
+
322
+ typer.echo("✅ Seeded AIB permission set 'kaos-identity'")
323
+ return True
324
+
325
+
209
326
  def _build_aib_broker_public_url_args(public_url: str) -> list[str]:
210
327
  """Set the broker enduser ``public_url`` to its in-cluster service URL.
211
328
 
@@ -219,25 +336,17 @@ def _build_aib_broker_public_url_args(public_url: str) -> list[str]:
219
336
  """
220
337
  return ["--set", f"broker.server.enduser.publicUrl={public_url}"]
221
338
 
339
+
222
340
  def _build_token_exchange_aib_args(
223
341
  auth_namespace: str,
224
342
  auth_release: str,
225
343
  keycloak_issuer: str,
226
344
  ) -> list[str]:
227
- """Configure the self-managed AIB release for Keycloak-backed exchange."""
345
+ """Configure public AIB v0.1.8 for Keycloak-backed token exchange."""
228
346
  aib_issuer = _default_auth_issuer(auth_namespace, auth_release)
229
347
  keycloak_token_endpoint = f"{keycloak_issuer}/protocol/openid-connect/token"
230
348
  keycloak_authorize_endpoint = f"{keycloak_issuer}/protocol/openid-connect/auth"
231
- extra_env = [
232
- {"name": "EXTPROC_OAUTH2_ISSUER", "value": keycloak_issuer},
233
- {"name": "EXTPROC_OAUTH2_CLIENT_ID", "value": DEFAULT_USER_AUTH_CLIENT_ID},
234
- {
235
- "name": "EXTPROC_OAUTH2_CLIENT_SECRET",
236
- "value": DEFAULT_USER_AUTH_CLIENT_SECRET,
237
- },
238
- {"name": "EXTPROC_OAUTH2_CLIENT_ASSERTION_TYPE", "value": "access_token"},
239
- ]
240
- return [
349
+ args = [
241
350
  "--set",
242
351
  f"broker.server.enduser.publicUrl={aib_issuer}",
243
352
  "--set",
@@ -248,7 +357,8 @@ def _build_token_exchange_aib_args(
248
357
  "broker.oauth2AuthorizationServer.proxy.upstreamAuthorizeEndpoint="
249
358
  f"{keycloak_authorize_endpoint}",
250
359
  "--set",
251
- f"broker.oauth2AuthorizationServer.proxy.upstreamTokenEndpoint={keycloak_token_endpoint}",
360
+ "broker.oauth2AuthorizationServer.proxy.upstreamTokenEndpoint="
361
+ f"{keycloak_token_endpoint}",
252
362
  "--set",
253
363
  f"broker.tokenExchange.expectedAudience={DEFAULT_TOKEN_EXCHANGE_AUDIENCE}",
254
364
  "--set",
@@ -261,13 +371,153 @@ def _build_token_exchange_aib_args(
261
371
  "--set",
262
372
  "broker.tokenExchange.authorization.cel.expression="
263
373
  f'client_assertion.azp == "{DEFAULT_USER_AUTH_CLIENT_ID}"',
264
- "--set",
265
- "extProc.enabled=true",
266
- "--set",
267
- f"extProc.oauth2.clientCredentialsEndpoint={keycloak_token_endpoint}",
268
- "--set-json",
269
- f"extProc.extraEnv={json.dumps(extra_env, separators=(',', ':'))}",
270
374
  ]
375
+ if keycloak_issuer.startswith("http://"):
376
+ args.extend(
377
+ ["--set", "broker.security.skipThirdpartyHttpsValidation=true"]
378
+ )
379
+ return args
380
+
381
+
382
+ def _auth_extproc_name(auth_release: str) -> str:
383
+ return f"{auth_release}-agentic-identity-broker-extproc"
384
+
385
+
386
+ def _aib_extproc_manifests(
387
+ namespace: str, release: str, keycloak_issuer: str
388
+ ) -> list[dict]:
389
+ """Build the public extProc workload using its v0.1.8 env contract."""
390
+ name = _auth_extproc_name(release)
391
+ labels = {"app.kubernetes.io/name": name}
392
+ secret_name = f"{name}-credentials"
393
+ token_endpoint = f"{_default_auth_issuer(namespace, release)}/oauth2/token"
394
+ client_credentials_endpoint = (
395
+ f"{keycloak_issuer}/protocol/openid-connect/token"
396
+ )
397
+ secret = {
398
+ "apiVersion": "v1",
399
+ "kind": "Secret",
400
+ "metadata": {"name": secret_name, "namespace": namespace},
401
+ "type": "Opaque",
402
+ "stringData": {"client-secret": DEFAULT_USER_AUTH_CLIENT_SECRET},
403
+ }
404
+ deployment = {
405
+ "apiVersion": "apps/v1",
406
+ "kind": "Deployment",
407
+ "metadata": {"name": name, "namespace": namespace, "labels": labels},
408
+ "spec": {
409
+ "replicas": 1,
410
+ "selector": {"matchLabels": labels},
411
+ "template": {
412
+ "metadata": {"labels": labels},
413
+ "spec": {
414
+ "containers": [
415
+ {
416
+ "name": "extproc",
417
+ "image": DEFAULT_AIB_EXTPROC_IMAGE,
418
+ "imagePullPolicy": "IfNotPresent",
419
+ "ports": [
420
+ {"name": "grpc", "containerPort": AUTH_EXTPROC_PORT}
421
+ ],
422
+ "env": [
423
+ {
424
+ "name": "EXTPROC_OAUTH2_TOKEN_ENDPOINT",
425
+ "value": token_endpoint,
426
+ },
427
+ {
428
+ "name": "EXTPROC_OAUTH2_ISSUER",
429
+ "value": keycloak_issuer,
430
+ },
431
+ {
432
+ "name": "EXTPROC_OAUTH2_CLIENT_ID",
433
+ "value": DEFAULT_USER_AUTH_CLIENT_ID,
434
+ },
435
+ {
436
+ "name": "EXTPROC_OAUTH2_CLIENT_SECRET",
437
+ "valueFrom": {
438
+ "secretKeyRef": {
439
+ "name": secret_name,
440
+ "key": "client-secret",
441
+ }
442
+ },
443
+ },
444
+ {
445
+ "name": (
446
+ "EXTPROC_OAUTH2_CLIENT_CREDENTIALS_ENDPOINT"
447
+ ),
448
+ "value": client_credentials_endpoint,
449
+ },
450
+ {
451
+ "name": "EXTPROC_OAUTH2_CLIENT_ASSERTION_TYPE",
452
+ "value": "access_token",
453
+ },
454
+ {
455
+ "name": "EXTPROC_OAUTH2_TLS_ALLOW_HTTP",
456
+ "value": "true",
457
+ },
458
+ {
459
+ "name": "EXTPROC_GRPC_PORT",
460
+ "value": str(AUTH_EXTPROC_PORT),
461
+ },
462
+ ],
463
+ "readinessProbe": {
464
+ "tcpSocket": {"port": "grpc"},
465
+ "periodSeconds": 2,
466
+ },
467
+ }
468
+ ]
469
+ },
470
+ },
471
+ },
472
+ }
473
+ service = {
474
+ "apiVersion": "v1",
475
+ "kind": "Service",
476
+ "metadata": {"name": name, "namespace": namespace, "labels": labels},
477
+ "spec": {
478
+ "selector": labels,
479
+ "ports": [
480
+ {
481
+ "name": "grpc",
482
+ "port": AUTH_EXTPROC_PORT,
483
+ "targetPort": "grpc",
484
+ }
485
+ ],
486
+ },
487
+ }
488
+ return [secret, deployment, service]
489
+
490
+
491
+ def _install_aib_extproc(
492
+ namespace: str, release: str, keycloak_issuer: str, wait: bool
493
+ ) -> bool:
494
+ """Apply KAOS-owned extProc resources for the public AIB release."""
495
+ typer.echo("Installing AIB token-exchange extProc...")
496
+ for manifest in _aib_extproc_manifests(namespace, release, keycloak_issuer):
497
+ result = _root()._run_kubectl(
498
+ ["apply", "-f", "-"], check=False, input=json.dumps(manifest)
499
+ )
500
+ if result.returncode != 0:
501
+ typer.echo(f"Error installing AIB extProc: {result.stderr}", err=True)
502
+ return False
503
+ if wait:
504
+ result = _root()._run_kubectl(
505
+ [
506
+ "rollout",
507
+ "status",
508
+ f"deployment/{_auth_extproc_name(release)}",
509
+ "-n",
510
+ namespace,
511
+ "--timeout=120s",
512
+ ],
513
+ check=False,
514
+ )
515
+ if result.returncode != 0:
516
+ typer.echo(f"Error waiting for AIB extProc: {result.stderr}", err=True)
517
+ return False
518
+ typer.echo(f"✅ AIB extProc installed in '{namespace}' namespace")
519
+ return True
520
+
271
521
 
272
522
  def _keycloak_realm_json(
273
523
  realm: str,
@@ -276,6 +526,7 @@ def _keycloak_realm_json(
276
526
  audience: str,
277
527
  username: str,
278
528
  password: str,
529
+ token_exchange_enabled: bool = False,
279
530
  ) -> dict:
280
531
  """Build a minimal Keycloak realm definition for non-interactive validation.
281
532
 
@@ -285,6 +536,32 @@ def _keycloak_realm_json(
285
536
  and group-based AccessGrants require.
286
537
  """
287
538
 
539
+ agent_audience_mappers = [
540
+ {
541
+ "name": "kaos-agent-audience",
542
+ "protocol": "openid-connect",
543
+ "protocolMapper": "oidc-audience-mapper",
544
+ "config": {
545
+ "included.custom.audience": DEFAULT_AGENT_AUTH_AUDIENCE,
546
+ "id.token.claim": "false",
547
+ "access.token.claim": "true",
548
+ },
549
+ }
550
+ ]
551
+ if token_exchange_enabled:
552
+ agent_audience_mappers.append(
553
+ {
554
+ "name": "token-exchange-audience",
555
+ "protocol": "openid-connect",
556
+ "protocolMapper": "oidc-audience-mapper",
557
+ "config": {
558
+ "included.custom.audience": DEFAULT_TOKEN_EXCHANGE_AUDIENCE,
559
+ "id.token.claim": "false",
560
+ "access.token.claim": "true",
561
+ },
562
+ }
563
+ )
564
+
288
565
  return {
289
566
  "realm": realm,
290
567
  "enabled": True,
@@ -293,18 +570,7 @@ def _keycloak_realm_json(
293
570
  "name": "kaos-agent-audience",
294
571
  "protocol": "openid-connect",
295
572
  "attributes": {"include.in.token.scope": "false"},
296
- "protocolMappers": [
297
- {
298
- "name": "kaos-agent-audience",
299
- "protocol": "openid-connect",
300
- "protocolMapper": "oidc-audience-mapper",
301
- "config": {
302
- "included.custom.audience": DEFAULT_AGENT_AUTH_AUDIENCE,
303
- "id.token.claim": "false",
304
- "access.token.claim": "true",
305
- },
306
- }
307
- ],
573
+ "protocolMappers": agent_audience_mappers,
308
574
  }
309
575
  ],
310
576
  "defaultDefaultClientScopes": ["kaos-agent-audience"],
@@ -385,15 +651,18 @@ def _keycloak_realm_json(
385
651
  ],
386
652
  }
387
653
 
654
+
388
655
  def _keycloak_realm_configmap_name(release: str) -> str:
389
656
  """Name of the ConfigMap holding the imported realm definition."""
390
657
  return f"{release}-realm-import"
391
658
 
659
+
392
660
  def _bootstrap_keycloak_realm(
393
661
  namespace: str,
394
662
  release: str,
395
663
  realm: str,
396
664
  audience: str,
665
+ token_exchange_enabled: bool = False,
397
666
  ) -> bool:
398
667
  """Create the realm-import ConfigMap consumed by Keycloak's --import-realm.
399
668
 
@@ -409,6 +678,7 @@ def _bootstrap_keycloak_realm(
409
678
  audience,
410
679
  DEFAULT_USER_AUTH_TEST_USER,
411
680
  DEFAULT_USER_AUTH_TEST_PASSWORD,
681
+ token_exchange_enabled,
412
682
  ),
413
683
  indent=2,
414
684
  )
@@ -521,6 +791,7 @@ def _keycloak_dev_manifests(
521
791
  }
522
792
  return [deployment, service]
523
793
 
794
+
524
795
  def _install_keycloak(
525
796
  namespace: str,
526
797
  release: str,
@@ -537,7 +808,9 @@ def _install_keycloak(
537
808
  dev path); otherwise a self-contained dev deployment is applied via kubectl.
538
809
  """
539
810
  typer.echo("Installing user identity provider (Keycloak)...")
540
- if not _bootstrap_keycloak_realm(namespace, release, realm, audience):
811
+ if not _bootstrap_keycloak_realm(
812
+ namespace, release, realm, audience, token_exchange_enabled
813
+ ):
541
814
  return False
542
815
 
543
816
  if chart_path:
@@ -90,6 +90,32 @@ def _install_gateway_api(enable_backend: bool = False) -> bool:
90
90
  typer.echo(f"Error installing Envoy Gateway: {result.stderr}", err=True)
91
91
  return False
92
92
 
93
+ if enable_backend:
94
+ for args in (
95
+ [
96
+ "rollout",
97
+ "restart",
98
+ "deployment/envoy-gateway",
99
+ "-n",
100
+ "envoy-gateway-system",
101
+ ],
102
+ [
103
+ "rollout",
104
+ "status",
105
+ "deployment/envoy-gateway",
106
+ "-n",
107
+ "envoy-gateway-system",
108
+ "--timeout=180s",
109
+ ],
110
+ ):
111
+ rollout = _root()._run_kubectl(args, check=False)
112
+ if rollout.returncode != 0:
113
+ typer.echo(
114
+ f"Error reloading Envoy Gateway Backend support: {rollout.stderr}",
115
+ err=True,
116
+ )
117
+ return False
118
+
93
119
  typer.echo("✅ Envoy Gateway installed")
94
120
  return True
95
121
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kaos-cli
3
- Version: 0.7.7
3
+ Version: 0.8.0
4
4
  Summary: CLI for KAOS (K8s Agent Orchestration System)
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kaos-cli"
7
- version = "0.7.7"
7
+ version = "0.8.0"
8
8
  description = "CLI for KAOS (K8s Agent Orchestration System)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1,5 +1,6 @@
1
1
  """CLI integration tests using --dry-run to validate YAML generation."""
2
2
 
3
+ import json
3
4
  import re
4
5
  import yaml
5
6
  import pytest
@@ -1137,16 +1138,40 @@ def test_token_exchange_gateway_install_enables_backend_api():
1137
1138
  from kaos_cli.install import _install_gateway_api
1138
1139
 
1139
1140
  calls = []
1141
+ kubectl_calls = []
1140
1142
 
1141
1143
  def fake_helm(args, check=False):
1142
1144
  calls.append(args)
1143
1145
  return SimpleNamespace(returncode=0, stdout="", stderr="")
1144
1146
 
1145
- with patch("kaos_cli.install.run_helm_command", side_effect=fake_helm):
1147
+ def fake_kubectl(args, check=False, input=None):
1148
+ kubectl_calls.append(args)
1149
+ return SimpleNamespace(returncode=0, stdout="", stderr="")
1150
+
1151
+ with patch("kaos_cli.install.run_helm_command", side_effect=fake_helm), patch(
1152
+ "kaos_cli.install._run_kubectl", side_effect=fake_kubectl
1153
+ ):
1146
1154
  assert _install_gateway_api(enable_backend=True)
1147
1155
 
1148
1156
  install_args = calls[-1]
1149
1157
  assert "config.envoyGateway.extensionApis.enableBackend=true" in install_args
1158
+ assert kubectl_calls[-2:] == [
1159
+ [
1160
+ "rollout",
1161
+ "restart",
1162
+ "deployment/envoy-gateway",
1163
+ "-n",
1164
+ "envoy-gateway-system",
1165
+ ],
1166
+ [
1167
+ "rollout",
1168
+ "status",
1169
+ "deployment/envoy-gateway",
1170
+ "-n",
1171
+ "envoy-gateway-system",
1172
+ "--timeout=180s",
1173
+ ],
1174
+ ]
1150
1175
 
1151
1176
 
1152
1177
  class TestAuthWiring:
@@ -1183,6 +1208,105 @@ class TestAuthWiring:
1183
1208
  # Each value is preceded by a --set flag.
1184
1209
  assert args.count("--set") == 4
1185
1210
 
1211
+ def test_seed_aib_identity_bootstrap(self):
1212
+ from contextlib import nullcontext
1213
+ from types import SimpleNamespace
1214
+ from unittest.mock import MagicMock, patch
1215
+
1216
+ from kaos_cli.install.auth import _seed_aib_identity_bootstrap
1217
+
1218
+ empty = MagicMock()
1219
+ empty.json.side_effect = [[], {"items": []}]
1220
+ created_service = MagicMock()
1221
+ created_service.json.return_value = {"id": "service-1"}
1222
+ created_permission_set = MagicMock()
1223
+ created_permission_set.json.return_value = {"id": "permission-set-1"}
1224
+ client = MagicMock()
1225
+ client.__enter__.return_value = client
1226
+ client.get.side_effect = [empty, empty]
1227
+ client.post.side_effect = [created_service, created_permission_set]
1228
+ root = SimpleNamespace(
1229
+ _run_kubectl=lambda *args, **kwargs: SimpleNamespace(
1230
+ returncode=0, stdout="", stderr=""
1231
+ )
1232
+ )
1233
+
1234
+ with patch("kaos_cli.install.auth._root", return_value=root), patch(
1235
+ "kaos_cli.install.auth.local_service_url",
1236
+ return_value=nullcontext("http://127.0.0.1:14000/api"),
1237
+ ), patch("kaos_cli.install.auth.httpx.Client", return_value=client):
1238
+ assert _seed_aib_identity_bootstrap("aib-system", "aib") is True
1239
+
1240
+ service_body = client.post.call_args_list[0].kwargs["json"]
1241
+ assert service_body["canonical_id"] == "kaos-identity-placeholder"
1242
+ assert service_body["display_name"].startswith("KAOS identity-only bootstrap")
1243
+ permission_body = client.post.call_args_list[1].kwargs["json"]
1244
+ assert permission_body["name"] == "kaos-identity"
1245
+ assert permission_body["service_scopes"] == [
1246
+ {"service_id": "service-1", "scopes": []}
1247
+ ]
1248
+
1249
+ def test_aib_extproc_manifest_uses_public_v018_contract(self):
1250
+ from kaos_cli.install.auth import _aib_extproc_manifests
1251
+
1252
+ secret, deployment, service = _aib_extproc_manifests(
1253
+ "aib-system",
1254
+ "aib",
1255
+ "http://keycloak.keycloak.svc.cluster.local:8080/realms/kaos",
1256
+ )
1257
+
1258
+ assert secret["kind"] == "Secret"
1259
+ container = deployment["spec"]["template"]["spec"]["containers"][0]
1260
+ assert container["image"].endswith(
1261
+ "agentic-identity-broker-extproc:v0.1.8"
1262
+ )
1263
+ env = {item["name"]: item for item in container["env"]}
1264
+ assert env["EXTPROC_OAUTH2_TOKEN_ENDPOINT"]["value"] == (
1265
+ "http://aib-agentic-identity-broker.aib-system.svc.cluster.local:8000"
1266
+ "/oauth2/token"
1267
+ )
1268
+ assert env["EXTPROC_OAUTH2_CLIENT_CREDENTIALS_ENDPOINT"]["value"].endswith(
1269
+ "/realms/kaos/protocol/openid-connect/token"
1270
+ )
1271
+ assert env["EXTPROC_OAUTH2_CLIENT_ASSERTION_TYPE"]["value"] == "access_token"
1272
+ assert env["EXTPROC_OAUTH2_TLS_ALLOW_HTTP"]["value"] == "true"
1273
+ assert "valueFrom" in env["EXTPROC_OAUTH2_CLIENT_SECRET"]
1274
+ assert service["metadata"]["name"] == (
1275
+ "aib-agentic-identity-broker-extproc"
1276
+ )
1277
+ assert service["spec"]["ports"][0]["port"] == 50051
1278
+
1279
+ def test_install_aib_extproc_applies_and_waits(self):
1280
+ from types import SimpleNamespace
1281
+ from unittest.mock import patch
1282
+
1283
+ from kaos_cli.install.auth import _install_aib_extproc
1284
+
1285
+ calls = []
1286
+
1287
+ def fake_kubectl(args, check=False, input=None):
1288
+ calls.append((args, input))
1289
+ return SimpleNamespace(returncode=0, stdout="", stderr="")
1290
+
1291
+ with patch("kaos_cli.install._run_kubectl", side_effect=fake_kubectl):
1292
+ assert _install_aib_extproc(
1293
+ "aib-system",
1294
+ "aib",
1295
+ "http://keycloak:8080/realms/kaos",
1296
+ wait=True,
1297
+ )
1298
+
1299
+ assert [json.loads(body)["kind"] for _, body in calls[:3]] == [
1300
+ "Secret",
1301
+ "Deployment",
1302
+ "Service",
1303
+ ]
1304
+ assert calls[3][0][:3] == [
1305
+ "rollout",
1306
+ "status",
1307
+ "deployment/aib-agentic-identity-broker-extproc",
1308
+ ]
1309
+
1186
1310
  def test_install_uses_one_aib_issuer_for_broker_and_operator(self):
1187
1311
  from types import SimpleNamespace
1188
1312
  from unittest.mock import patch
@@ -1644,6 +1768,24 @@ class TestAuthWiring:
1644
1768
  "--features=token-exchange,admin-fine-grained-authz",
1645
1769
  ]
1646
1770
 
1771
+ def test_keycloak_te_realm_adds_exchange_audience_to_dynamic_clients(self):
1772
+ from kaos_cli.install.auth import _keycloak_realm_json
1773
+
1774
+ realm = _keycloak_realm_json(
1775
+ "kaos",
1776
+ "kaos",
1777
+ "secret",
1778
+ "kaos",
1779
+ "user",
1780
+ "password",
1781
+ token_exchange_enabled=True,
1782
+ )
1783
+ audiences = {
1784
+ mapper["config"]["included.custom.audience"]
1785
+ for mapper in realm["clientScopes"][0]["protocolMappers"]
1786
+ }
1787
+ assert audiences == {"kaos-gateway", "token-exchange-broker"}
1788
+
1647
1789
  def test_auth_enabled_wires_user_auth_values(self):
1648
1790
  """aib-keycloak (user-auth on) adds security.userAuth.* args."""
1649
1791
  from unittest.mock import patch
@@ -1934,6 +2076,7 @@ class TestAuthWiring:
1934
2076
  "security.agentAuth.issuer=http://aib-agentic-identity-broker.aib-system.svc.cluster.local:8000",
1935
2077
  "security.agentAuth.credentialSecretPrefix=kaos-aib",
1936
2078
  "security.agentAuth.adminUrl=http://aib-agentic-identity-broker.aib-system.svc.cluster.local:14000/api",
2079
+ "security.agentAuth.defaultPermissionSet=kaos-identity",
1937
2080
  "security.agentAuth.authorization.policyDataSource=automated",
1938
2081
  "security.agentAuth.projection.policyConfigMap.name=kaos-authz-policy",
1939
2082
  "security.agentAuth.projection.policyConfigMap.namespace=kaos-system",
@@ -1954,6 +2097,7 @@ class TestAuthWiring:
1954
2097
  "security.agentAuth.issuer=http://aib-agentic-identity-broker.aib-system.svc.cluster.local:8000",
1955
2098
  "security.agentAuth.credentialSecretPrefix=kaos-aib",
1956
2099
  "security.agentAuth.adminUrl=http://aib-agentic-identity-broker.aib-system.svc.cluster.local:14000/api",
2100
+ "security.agentAuth.defaultPermissionSet=kaos-identity",
1957
2101
  "security.agentAuth.authorization.policyDataSource=automated",
1958
2102
  "security.agentAuth.projection.policyConfigMap.name=kaos-authz-policy",
1959
2103
  "security.agentAuth.projection.policyConfigMap.namespace=kaos-system",
@@ -2142,7 +2286,7 @@ class TestAuthWiring:
2142
2286
  assert result.exit_code != 0
2143
2287
  assert "No such option: --auth-enabled" in strip_ansi(result.output)
2144
2288
 
2145
- def test_token_exchange_expands_keycloak_aib_and_operator_wiring(self):
2289
+ def test_token_exchange_deploys_public_extproc_and_wires_operator(self):
2146
2290
  from types import SimpleNamespace
2147
2291
  from unittest.mock import patch
2148
2292
 
@@ -2156,7 +2300,9 @@ class TestAuthWiring:
2156
2300
  "kaos_cli.install.run_helm_command", side_effect=fake_helm
2157
2301
  ), patch("kaos_cli.install._install_aib", return_value=True) as mock_aib, patch(
2158
2302
  "kaos_cli.install._install_keycloak", return_value=True
2159
- ) as mock_keycloak:
2303
+ ) as mock_keycloak, patch(
2304
+ "kaos_cli.install._install_aib_extproc", return_value=True
2305
+ ) as mock_extproc:
2160
2306
  result = runner.invoke(
2161
2307
  app,
2162
2308
  [
@@ -2173,27 +2319,23 @@ class TestAuthWiring:
2173
2319
  assert result.exit_code == 0, result.output
2174
2320
  mock_keycloak.assert_called_once()
2175
2321
  mock_aib.assert_called_once()
2176
- aib_args = mock_aib.call_args.kwargs["extra_set"]
2177
- joined_aib = " ".join(aib_args)
2178
- assert "extProc.enabled=true" in joined_aib
2179
- assert "EXTPROC_OAUTH2_ISSUER" in joined_aib
2180
- assert "EXTPROC_OAUTH2_CLIENT_ID" in joined_aib
2181
- assert "EXTPROC_OAUTH2_CLIENT_SECRET" in joined_aib
2182
- assert "EXTPROC_OAUTH2_CLIENT_ASSERTION_TYPE" in joined_aib
2183
- assert "EXTPROC_OAUTH2_TOKEN_ENDPOINT" not in joined_aib
2184
- assert "EXTPROC_OAUTH2_TLS_ALLOW_HTTP" not in joined_aib
2185
- assert "EXTPROC_OAUTH2_CLIENT_CREDENTIALS_ENDPOINT" not in joined_aib
2186
- assert "extProc.oauth2.clientCredentialsEndpoint=" in joined_aib
2322
+ mock_extproc.assert_called_once_with(
2323
+ "aib-system",
2324
+ "aib",
2325
+ "http://keycloak.keycloak.svc.cluster.local:8080/realms/kaos",
2326
+ False,
2327
+ )
2328
+ joined_aib = " ".join(mock_aib.call_args.kwargs["extra_set"])
2329
+ assert mock_aib.call_args.kwargs["seed_identity_bootstrap"] is False
2330
+ assert "broker.oauth2AuthorizationServer.mode=proxy" in joined_aib
2331
+ assert "resolveAgentIdByClientId(subject_token.azp)" in joined_aib
2187
2332
  assert 'client_assertion.azp == "kaos"' in joined_aib
2333
+ assert "broker.security.skipThirdpartyHttpsValidation=true" in joined_aib
2334
+ assert "extProc." not in joined_aib
2188
2335
 
2189
- assert mock_keycloak.call_args.args[-1] is True
2190
-
2191
- joined = " ".join(captured["operator"])
2192
- assert "security.agentAuth.identity.provider=oidc" in joined
2193
- assert "security.userAuth.audience=kaos" in joined
2194
- assert "security.tokenExchange.enabled=true" in joined
2195
- assert "security.tokenExchange.aib.adminUrl=" in joined
2196
- assert "security.tokenExchange.extProc.port=50051" in joined
2336
+ joined_operator = " ".join(captured["operator"])
2337
+ assert "security.tokenExchange.enabled=true" in joined_operator
2338
+ assert "security.tokenExchange.extProc.port=50051" in joined_operator
2197
2339
 
2198
2340
  @pytest.mark.parametrize(
2199
2341
  "args,message",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes