kaos-cli 0.7.4__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.4 → kaos_cli-0.8.0}/PKG-INFO +1 -1
  2. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/__init__.py +1 -1
  3. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/deploy.py +5 -10
  4. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/install/__init__.py +40 -5
  5. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/install/auth.py +306 -33
  6. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/install/infra.py +26 -0
  7. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/modelapi/deploy.py +5 -10
  8. kaos_cli-0.8.0/kaos_cli/utils/wait.py +56 -0
  9. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli.egg-info/PKG-INFO +1 -1
  10. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli.egg-info/SOURCES.txt +2 -0
  11. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/pyproject.toml +1 -1
  12. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/tests/test_cli_integration.py +164 -22
  13. kaos_cli-0.8.0/tests/test_deploy_wait.py +103 -0
  14. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/README.md +0 -0
  15. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/__init__.py +0 -0
  16. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/a2a.py +0 -0
  17. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/build.py +0 -0
  18. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/init.py +0 -0
  19. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/invoke.py +0 -0
  20. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/memory.py +0 -0
  21. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/run.py +0 -0
  22. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/status.py +0 -0
  23. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/tools.py +0 -0
  24. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/auth/__init__.py +0 -0
  25. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/auth/consent.py +0 -0
  26. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/auth/grant.py +0 -0
  27. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/auth/login.py +0 -0
  28. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/cluster_http.py +0 -0
  29. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/config.py +0 -0
  30. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/config_cli.py +0 -0
  31. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/install/observability.py +0 -0
  32. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/main.py +0 -0
  33. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/__init__.py +0 -0
  34. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/build.py +0 -0
  35. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/crud.py +0 -0
  36. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/deploy.py +0 -0
  37. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/init.py +0 -0
  38. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/invoke.py +0 -0
  39. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/run.py +0 -0
  40. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/memory/__init__.py +0 -0
  41. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/memorystore/__init__.py +0 -0
  42. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/memorystore/create.py +0 -0
  43. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/modelapi/__init__.py +0 -0
  44. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/modelapi/invoke.py +0 -0
  45. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/proxy.py +0 -0
  46. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/__init__.py +0 -0
  47. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/1-simple-echo-agent.yaml +0 -0
  48. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/2-multi-agent-mcp.yaml +0 -0
  49. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/3-hierarchical-agents.yaml +0 -0
  50. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/4-dev-ollama-proxy-agent.yaml +0 -0
  51. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/5-proxy-external-api.yaml +0 -0
  52. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/6-autonomous-monitor.yaml +0 -0
  53. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/7-memory-agent.yaml +0 -0
  54. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/8-authorization-walkthrough.yaml +0 -0
  55. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/system/__init__.py +0 -0
  56. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/system/create_rbac.py +0 -0
  57. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/system/install.py +0 -0
  58. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/system/runtimes.py +0 -0
  59. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/system/status.py +0 -0
  60. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/ui.py +0 -0
  61. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/utils/__init__.py +0 -0
  62. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/utils/crud.py +0 -0
  63. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/utils/port_forward.py +0 -0
  64. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli.egg-info/dependency_links.txt +0 -0
  65. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli.egg-info/entry_points.txt +0 -0
  66. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli.egg-info/requires.txt +0 -0
  67. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli.egg-info/top_level.txt +0 -0
  68. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/setup.cfg +0 -0
  69. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/tests/test_agent_invoke.py +0 -0
  70. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/tests/test_agent_tools.py +0 -0
  71. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/tests/test_auth_cli.py +0 -0
  72. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/tests/test_config_cli.py +0 -0
  73. {kaos_cli-0.7.4 → kaos_cli-0.8.0}/tests/test_install_surface.py +0 -0
  74. {kaos_cli-0.7.4 → 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.4
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.4.dev0"
8
+ __version__ = "0.7.8.dev0"
@@ -7,6 +7,8 @@ import tempfile
7
7
  from pathlib import Path
8
8
  import typer
9
9
 
10
+ from kaos_cli.utils.wait import wait_for_deployment_available
11
+
10
12
 
11
13
  AGENT_TEMPLATE = """apiVersion: kaos.tools/v1alpha1
12
14
  kind: Agent
@@ -184,16 +186,9 @@ def deploy_agent(
184
186
  # Wait for deployment if requested
185
187
  if wait:
186
188
  typer.echo("⏳ Waiting for deployment to be available...")
187
- wait_args = [
188
- "kubectl",
189
- "wait",
190
- f"deployment/agent-{name}",
191
- "--for=condition=available",
192
- f"--timeout={wait_timeout}s",
193
- ]
194
- if namespace:
195
- wait_args.extend(["-n", namespace])
196
- wait_result = subprocess.run(wait_args, capture_output=True, text=True)
189
+ wait_result = wait_for_deployment_available(
190
+ f"agent-{name}", namespace, wait_timeout
191
+ )
197
192
  if wait_result.returncode != 0:
198
193
  typer.echo(wait_result.stderr or wait_result.stdout, err=True)
199
194
  sys.exit(wait_result.returncode)
@@ -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
 
@@ -8,6 +8,8 @@ import tempfile
8
8
  from pathlib import Path
9
9
  import typer
10
10
 
11
+ from kaos_cli.utils.wait import wait_for_deployment_available
12
+
11
13
 
12
14
  DEFAULT_WAIT_TIMEOUT = 120
13
15
 
@@ -174,16 +176,9 @@ spec:
174
176
  # Wait for deployment if requested
175
177
  if wait:
176
178
  typer.echo("⏳ Waiting for deployment to be available...")
177
- wait_args = [
178
- "kubectl",
179
- "wait",
180
- f"deployment/modelapi-{name}",
181
- "--for=condition=available",
182
- f"--timeout={wait_timeout}s",
183
- ]
184
- if namespace:
185
- wait_args.extend(["-n", namespace])
186
- wait_result = subprocess.run(wait_args, capture_output=True, text=True)
179
+ wait_result = wait_for_deployment_available(
180
+ f"modelapi-{name}", namespace, wait_timeout
181
+ )
187
182
  if wait_result.returncode != 0:
188
183
  typer.echo(wait_result.stderr or wait_result.stdout, err=True)
189
184
  sys.exit(wait_result.returncode)