@openlucaskaka/kagent 0.1.7

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 (117) hide show
  1. package/README.md +353 -0
  2. package/npm/bin/kagent-serve.js +6 -0
  3. package/npm/bin/kagent.js +6 -0
  4. package/npm/lib/App.js +524 -0
  5. package/npm/lib/app-state.js +224 -0
  6. package/npm/lib/approval-choice.js +25 -0
  7. package/npm/lib/commands.js +59 -0
  8. package/npm/lib/editor.js +188 -0
  9. package/npm/lib/ink-runner.js +41 -0
  10. package/npm/lib/kagent-home.js +39 -0
  11. package/npm/lib/launcher.js +221 -0
  12. package/npm/lib/protocol.js +33 -0
  13. package/npm/lib/provider-setup.js +139 -0
  14. package/npm/lib/python-runner.js +892 -0
  15. package/npm/lib/runtime-client.js +390 -0
  16. package/npm/lib/terminal-input.js +127 -0
  17. package/npm/lib/terminal-text.js +19 -0
  18. package/npm/lib/terminal-width.js +14 -0
  19. package/npm/lib/transcript.js +227 -0
  20. package/npm/lib/ui-components.js +247 -0
  21. package/npm/lib/update-manager.js +334 -0
  22. package/package.json +39 -0
  23. package/pyproject.toml +55 -0
  24. package/src/kagent/__init__.py +90 -0
  25. package/src/kagent/cli/__init__.py +5 -0
  26. package/src/kagent/cli/__main__.py +6 -0
  27. package/src/kagent/cli/commands.py +112 -0
  28. package/src/kagent/cli/conversation.py +127 -0
  29. package/src/kagent/cli/interactive.py +841 -0
  30. package/src/kagent/cli/main.py +685 -0
  31. package/src/kagent/cli/memory.py +460 -0
  32. package/src/kagent/cli/pending_approval.py +169 -0
  33. package/src/kagent/cli/provider.py +27 -0
  34. package/src/kagent/cli/session_commands.py +401 -0
  35. package/src/kagent/cli/stdio_runtime.py +784 -0
  36. package/src/kagent/cli/trace.py +67 -0
  37. package/src/kagent/cli/ui.py +931 -0
  38. package/src/kagent/core/__init__.py +0 -0
  39. package/src/kagent/core/agent.py +296 -0
  40. package/src/kagent/core/faults.py +11 -0
  41. package/src/kagent/core/invariants.py +47 -0
  42. package/src/kagent/core/normalization.py +81 -0
  43. package/src/kagent/core/planning.py +48 -0
  44. package/src/kagent/core/state.py +73 -0
  45. package/src/kagent/core/summary.py +64 -0
  46. package/src/kagent/core/tools.py +196 -0
  47. package/src/kagent/core/trace.py +57 -0
  48. package/src/kagent/eval/__init__.py +11 -0
  49. package/src/kagent/eval/cases.py +229 -0
  50. package/src/kagent/eval/evaluator.py +216 -0
  51. package/src/kagent/integrations/__init__.py +3 -0
  52. package/src/kagent/integrations/audit.py +95 -0
  53. package/src/kagent/integrations/backends.py +131 -0
  54. package/src/kagent/integrations/memory.py +301 -0
  55. package/src/kagent/ops/__init__.py +0 -0
  56. package/src/kagent/ops/batch.py +156 -0
  57. package/src/kagent/ops/doctor.py +255 -0
  58. package/src/kagent/ops/metrics.py +214 -0
  59. package/src/kagent/ops/release_evidence.py +877 -0
  60. package/src/kagent/ops/release_manifest.py +142 -0
  61. package/src/kagent/ops/trace_replay.py +285 -0
  62. package/src/kagent/providers/__init__.py +7 -0
  63. package/src/kagent/providers/embeddings.py +187 -0
  64. package/src/kagent/providers/llm.py +770 -0
  65. package/src/kagent/py.typed +1 -0
  66. package/src/kagent/runtime/__init__.py +28 -0
  67. package/src/kagent/runtime/action_graph.py +543 -0
  68. package/src/kagent/runtime/agent.py +2089 -0
  69. package/src/kagent/runtime/approval.py +64 -0
  70. package/src/kagent/runtime/cancellation.py +64 -0
  71. package/src/kagent/runtime/checkpoint_state.py +146 -0
  72. package/src/kagent/runtime/checkpoint_storage.py +270 -0
  73. package/src/kagent/runtime/context.py +65 -0
  74. package/src/kagent/runtime/file_transaction.py +195 -0
  75. package/src/kagent/runtime/hooks.py +74 -0
  76. package/src/kagent/runtime/metadata.py +116 -0
  77. package/src/kagent/runtime/patch_checkpoints.py +385 -0
  78. package/src/kagent/runtime/policy.py +50 -0
  79. package/src/kagent/runtime/presentation.py +205 -0
  80. package/src/kagent/runtime/redaction.py +130 -0
  81. package/src/kagent/runtime/sandbox.py +331 -0
  82. package/src/kagent/runtime/skills.py +66 -0
  83. package/src/kagent/runtime/steering.py +56 -0
  84. package/src/kagent/runtime/steps.py +255 -0
  85. package/src/kagent/runtime/task_state.py +40 -0
  86. package/src/kagent/runtime/tools.py +3532 -0
  87. package/src/kagent/runtime/types.py +240 -0
  88. package/src/kagent/runtime/workspace.py +597 -0
  89. package/src/kagent/service/__init__.py +26 -0
  90. package/src/kagent/service/__main__.py +6 -0
  91. package/src/kagent/service/active_runs.py +178 -0
  92. package/src/kagent/service/cli.py +737 -0
  93. package/src/kagent/service/contract.py +2571 -0
  94. package/src/kagent/service/errors.py +71 -0
  95. package/src/kagent/service/idempotency.py +584 -0
  96. package/src/kagent/service/router.py +884 -0
  97. package/src/kagent/service/run.py +150 -0
  98. package/src/kagent/service/runtime.py +1915 -0
  99. package/src/kagent/service/runtime_approval.py +20 -0
  100. package/src/kagent/service/runtime_cancel.py +192 -0
  101. package/src/kagent/service/runtime_lifecycle.py +229 -0
  102. package/src/kagent/service/runtime_metadata.py +21 -0
  103. package/src/kagent/service/runtime_policy.py +115 -0
  104. package/src/kagent/service/runtime_recovery.py +573 -0
  105. package/src/kagent/service/runtime_resume.py +382 -0
  106. package/src/kagent/service/runtime_resume_claim.py +116 -0
  107. package/src/kagent/service/runtime_run.py +350 -0
  108. package/src/kagent/service/runtime_status.py +2007 -0
  109. package/src/kagent/service/safety.py +139 -0
  110. package/src/kagent/service/server.py +114 -0
  111. package/src/kagent/service/status.py +233 -0
  112. package/src/kagent/service/trace_store.py +322 -0
  113. package/src/kagent/service/transport.py +24 -0
  114. package/src/kagent/utils/__init__.py +0 -0
  115. package/src/kagent/utils/config_validation.py +21 -0
  116. package/src/kagent/utils/json_output.py +23 -0
  117. package/src/kagent/utils/paths.py +473 -0
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import hmac
4
+ from ipaddress import ip_address
5
+ from typing import Any, Mapping
6
+ from uuid import uuid4
7
+
8
+ _MAX_REQUEST_ID_LENGTH = 128
9
+ _MAX_IDEMPOTENCY_KEY_LENGTH = 128
10
+ _SAFE_TRACE_FILENAME_CHARS = frozenset(
11
+ "abcdefghijklmnopqrstuvwxyz"
12
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
13
+ "0123456789"
14
+ "._-"
15
+ )
16
+
17
+
18
+ def authorized(
19
+ headers: Mapping[str, str],
20
+ auth_token: str,
21
+ auth_tokens: Mapping[str, str] | None = None,
22
+ ) -> bool:
23
+ if not auth_token and not auth_tokens:
24
+ return True
25
+ return bool(authenticated_subject(headers, auth_token, auth_tokens or {}))
26
+
27
+
28
+ def authenticated_subject(
29
+ headers: Mapping[str, str],
30
+ auth_token: str,
31
+ auth_tokens: Mapping[str, str] | None = None,
32
+ ) -> str:
33
+ provided = header_value(headers, "Authorization")
34
+ if not safe_header_value(provided):
35
+ return ""
36
+ if auth_token:
37
+ expected = f"Bearer {auth_token}"
38
+ if safe_header_value(expected) and hmac.compare_digest(provided, expected):
39
+ return "default"
40
+ for subject, token in sorted((auth_tokens or {}).items()):
41
+ expected = f"Bearer {token}"
42
+ if safe_header_value(expected) and hmac.compare_digest(provided, expected):
43
+ return subject
44
+ return ""
45
+
46
+
47
+ def authenticated_with_primary_token(headers: Mapping[str, str], auth_token: str) -> bool:
48
+ if not auth_token:
49
+ return False
50
+ provided = header_value(headers, "Authorization")
51
+ expected = f"Bearer {auth_token}"
52
+ return (
53
+ safe_header_value(provided)
54
+ and safe_header_value(expected)
55
+ and hmac.compare_digest(provided, expected)
56
+ )
57
+
58
+
59
+ def json_content_type(headers: Mapping[str, str]) -> bool:
60
+ content_type = header_value(headers, "Content-Type")
61
+ if not content_type:
62
+ return True
63
+ media_type = content_type.split(";", 1)[0].strip().lower()
64
+ return media_type == "application/json"
65
+
66
+
67
+ def rate_limit_key(
68
+ headers: Mapping[str, str],
69
+ remote_addr: str,
70
+ *,
71
+ trust_forwarded_for: bool,
72
+ auth_token: str = "",
73
+ auth_tokens: Mapping[str, str] | None = None,
74
+ auth_subject: str = "",
75
+ ) -> str:
76
+ subject = auth_subject or authenticated_subject(headers, auth_token, auth_tokens or {})
77
+ if subject:
78
+ return f"auth:{subject}"
79
+ if trust_forwarded_for:
80
+ forwarded_for = header_value(headers, "X-Forwarded-For").split(",", 1)[0].strip()
81
+ forwarded_for_key = forwarded_for_client_key(forwarded_for)
82
+ if forwarded_for_key:
83
+ return forwarded_for_key
84
+ return remote_addr or "local"
85
+
86
+
87
+ def request_id_from_headers(headers: Mapping[str, str]) -> str:
88
+ request_id = header_value(headers, "X-Request-ID")
89
+ if safe_request_id(request_id):
90
+ return request_id
91
+ return str(uuid4())
92
+
93
+
94
+ def safe_request_id(value: str) -> bool:
95
+ if not value or len(value) > _MAX_REQUEST_ID_LENGTH:
96
+ return False
97
+ return all(33 <= ord(character) <= 126 for character in value)
98
+
99
+
100
+ def safe_idempotency_key(value: str) -> bool:
101
+ if not value or len(value) > _MAX_IDEMPOTENCY_KEY_LENGTH:
102
+ return False
103
+ return all(33 <= ord(character) <= 126 for character in value)
104
+
105
+
106
+ def safe_forwarded_for_client(value: str) -> bool:
107
+ return bool(forwarded_for_client_key(value))
108
+
109
+
110
+ def forwarded_for_client_key(value: str) -> str:
111
+ if not safe_request_id(value):
112
+ return ""
113
+ try:
114
+ return str(ip_address(value))
115
+ except ValueError:
116
+ return ""
117
+
118
+
119
+ def safe_trace_file_stem(value: Any) -> str:
120
+ raw_value = str(value or "trace")
121
+ safe_value = "".join(
122
+ character if character in _SAFE_TRACE_FILENAME_CHARS else "_"
123
+ for character in raw_value
124
+ ).strip("._")
125
+ return safe_value or "trace"
126
+
127
+
128
+ def header_value(headers: Mapping[str, str], name: str) -> str:
129
+ expected = name.lower()
130
+ for key, value in headers.items():
131
+ if key.lower() == expected:
132
+ return value
133
+ return ""
134
+
135
+
136
+ def safe_header_value(value: str) -> bool:
137
+ if not value:
138
+ return False
139
+ return all(32 <= ord(character) <= 126 for character in value)
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
4
+ from typing import Optional, Type
5
+
6
+ from kagent.service.active_runs import ActiveRunRegistry
7
+ from kagent.service.runtime import (
8
+ ServiceConcurrencyLimiter,
9
+ ServiceConfig,
10
+ ServiceIdempotencyCache,
11
+ ServiceMetrics,
12
+ ServiceRateLimiter,
13
+ SqliteServiceIdempotencyCache,
14
+ )
15
+ from kagent.service.runtime_recovery import (
16
+ RuntimeInstanceLease,
17
+ reconcile_orphaned_runtime_traces,
18
+ )
19
+
20
+
21
+ class ProductionThreadingHTTPServer(ThreadingHTTPServer):
22
+ daemon_threads = False
23
+ block_on_close = True
24
+ allow_reuse_address = True
25
+ request_queue_size = 64
26
+
27
+ def server_close(self) -> None:
28
+ super().server_close()
29
+ lease = getattr(self, "service_runtime_instance_lease", None)
30
+ registry = getattr(self, "service_active_run_registry", None)
31
+ if isinstance(lease, RuntimeInstanceLease) and isinstance(
32
+ registry, ActiveRunRegistry
33
+ ):
34
+ lease.stop_when_idle(registry)
35
+
36
+
37
+ def create_threading_server(
38
+ host: str,
39
+ port: int,
40
+ handler_class: Type[BaseHTTPRequestHandler],
41
+ *,
42
+ config: Optional[ServiceConfig] = None,
43
+ ) -> ThreadingHTTPServer:
44
+ server = ProductionThreadingHTTPServer((host, port), handler_class)
45
+ bound_host, bound_port = server.server_address
46
+ server.service_config = config or ServiceConfig( # type: ignore[attr-defined]
47
+ host=str(bound_host),
48
+ port=int(bound_port),
49
+ )
50
+ service_metrics = ServiceMetrics()
51
+ server.service_metrics = service_metrics # type: ignore[attr-defined]
52
+ server.service_rate_limiter = ServiceRateLimiter( # type: ignore[attr-defined]
53
+ limit_per_minute=server.service_config.rate_limit_per_minute # type: ignore[attr-defined]
54
+ )
55
+ server.service_concurrency_limiter = ServiceConcurrencyLimiter( # type: ignore[attr-defined]
56
+ max_concurrent_runs=server.service_config.max_concurrent_runs # type: ignore[attr-defined]
57
+ )
58
+ server.service_active_run_registry = ActiveRunRegistry() # type: ignore[attr-defined]
59
+ if server.service_config.idempotency_cache_path: # type: ignore[attr-defined]
60
+ server.service_idempotency_cache = SqliteServiceIdempotencyCache( # type: ignore[attr-defined]
61
+ max_entries=server.service_config.idempotency_cache_size, # type: ignore[attr-defined]
62
+ database_path=server.service_config.idempotency_cache_path, # type: ignore[attr-defined]
63
+ )
64
+ else:
65
+ server.service_idempotency_cache = ServiceIdempotencyCache( # type: ignore[attr-defined]
66
+ max_entries=server.service_config.idempotency_cache_size # type: ignore[attr-defined]
67
+ )
68
+ server.service_runtime_instance_lease = None # type: ignore[attr-defined]
69
+ server.service_runtime_reconciliation = { # type: ignore[attr-defined]
70
+ "enabled": False,
71
+ "reason": "trace_persistence_disabled",
72
+ }
73
+ if server.service_config.trace_dir: # type: ignore[attr-defined]
74
+ lease = RuntimeInstanceLease(
75
+ server.service_config.trace_dir, # type: ignore[attr-defined]
76
+ instance_id=server.service_active_run_registry.instance_id, # type: ignore[attr-defined]
77
+ heartbeat_seconds=(
78
+ server.service_config.runtime_instance_heartbeat_seconds # type: ignore[attr-defined]
79
+ ),
80
+ )
81
+ server.service_runtime_instance_lease = lease # type: ignore[attr-defined]
82
+ try:
83
+ lease.start()
84
+ server.service_runtime_reconciliation = ( # type: ignore[attr-defined]
85
+ reconcile_orphaned_runtime_traces(
86
+ server.service_config.trace_dir, # type: ignore[attr-defined]
87
+ current_instance_id=(
88
+ server.service_active_run_registry.instance_id # type: ignore[attr-defined]
89
+ ),
90
+ stale_after_seconds=(
91
+ server.service_config.runtime_orphaned_run_stale_seconds # type: ignore[attr-defined]
92
+ ),
93
+ )
94
+ )
95
+ service_metrics.record_runtime_reconciliation(
96
+ server.service_runtime_reconciliation # type: ignore[attr-defined]
97
+ )
98
+ except OSError as exc:
99
+ lease.stop()
100
+ server.service_runtime_instance_lease = None # type: ignore[attr-defined]
101
+ server.service_runtime_reconciliation = { # type: ignore[attr-defined]
102
+ "enabled": False,
103
+ "reason": "trace_persistence_unavailable",
104
+ "error_type": type(exc).__name__,
105
+ }
106
+ service_metrics.record_runtime_reconciliation(
107
+ {"errors": 1},
108
+ status="unavailable",
109
+ )
110
+ except Exception:
111
+ lease.stop()
112
+ super(ProductionThreadingHTTPServer, server).server_close()
113
+ raise
114
+ return server
@@ -0,0 +1,233 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Callable, Dict, Optional
5
+ from uuid import uuid4
6
+
7
+ from kagent.integrations.backends import (
8
+ ExternalBackendConfig,
9
+ check_external_backends,
10
+ )
11
+ from kagent.providers.embeddings import EmbeddingProviderConfig
12
+ from kagent.providers.llm import LLMProviderConfig
13
+ from kagent.runtime.workspace import VIRTUAL_WORKSPACE_KINDS, RuntimeWorkspace
14
+ from kagent.service import transport as service_transport
15
+ from kagent.service.contract import service_openapi
16
+ from kagent.service.errors import READINESS_FAILED
17
+ from kagent.service.runtime import (
18
+ ServiceConfig,
19
+ SqliteServiceIdempotencyCache,
20
+ )
21
+ from kagent.service.trace_store import (
22
+ _ensure_owner_only_trace_dir,
23
+ _write_owner_only_temporary_trace,
24
+ )
25
+
26
+
27
+ def readiness_payload(config: Optional[ServiceConfig] = None) -> Dict[str, Any]:
28
+ active_config = config or ServiceConfig()
29
+ checks = {
30
+ "agent_config": _readiness_check(
31
+ _check_agent_config,
32
+ "agent_config_unavailable",
33
+ ),
34
+ "openapi": _readiness_check(service_openapi, "openapi_unavailable"),
35
+ "tools": _readiness_check(_check_tools, "tools_unavailable"),
36
+ }
37
+ if active_config.trace_dir:
38
+ checks["trace_persistence"] = _readiness_check(
39
+ lambda: _check_trace_persistence(active_config.trace_dir),
40
+ "trace_persistence_unavailable",
41
+ )
42
+ if active_config.runtime_workspace_dir:
43
+ checks["runtime_workspace"] = _readiness_check(
44
+ lambda: _check_runtime_workspace(active_config.runtime_workspace_dir),
45
+ "runtime_workspace_unavailable",
46
+ )
47
+ if active_config.idempotency_cache_path:
48
+ checks["idempotency_cache_persistence"] = _readiness_check(
49
+ lambda: _check_idempotency_cache_persistence(active_config),
50
+ "idempotency_cache_unavailable",
51
+ )
52
+ if _embedding_provider_configured(active_config):
53
+ checks["embedding_provider"] = _readiness_check(
54
+ lambda: _check_embedding_provider_config(active_config),
55
+ "embedding_provider_unavailable",
56
+ )
57
+ checks.update(check_external_backends(_external_backend_config(active_config)))
58
+ failed_checks = [name for name, value in checks.items() if value != "ok"]
59
+ status = "ready" if not failed_checks else "not_ready"
60
+ payload = {"status": status, "checks": checks, "failed_checks": failed_checks}
61
+ if failed_checks:
62
+ payload["error_code"] = READINESS_FAILED
63
+ return payload
64
+
65
+
66
+ def service_config_snapshot(config: ServiceConfig) -> Dict[str, str]:
67
+ snapshot = {
68
+ "host": config.host,
69
+ "port": str(config.port),
70
+ "max_request_bytes": str(config.max_request_bytes),
71
+ "max_goal_chars": str(config.max_goal_chars),
72
+ "auth_required": str(config.auth_required).lower(),
73
+ "auth_subject_count": str(
74
+ len(config.auth_tokens) + (1 if config.auth_token else 0)
75
+ ),
76
+ "rate_limit_per_minute": str(config.rate_limit_per_minute),
77
+ "max_concurrent_runs": str(config.max_concurrent_runs),
78
+ "idempotency_cache_size": str(config.idempotency_cache_size),
79
+ "idempotency_cache_backend": (
80
+ "sqlite" if config.idempotency_cache_path else "memory"
81
+ ),
82
+ "idempotency_cache_path_configured": (
83
+ "true" if config.idempotency_cache_path else "false"
84
+ ),
85
+ "runtime_allowed_tools": _runtime_allowed_tools_snapshot(config),
86
+ "runtime_allowed_tools_by_subject_count": str(
87
+ len(config.runtime_allowed_tools_by_subject)
88
+ ),
89
+ "runtime_max_iterations": str(config.runtime_max_iterations),
90
+ "runtime_pending_approval_stale_seconds": str(
91
+ config.runtime_pending_approval_stale_seconds
92
+ ),
93
+ "runtime_instance_heartbeat_seconds": str(
94
+ config.runtime_instance_heartbeat_seconds
95
+ ),
96
+ "runtime_orphaned_run_stale_seconds": str(
97
+ config.runtime_orphaned_run_stale_seconds
98
+ ),
99
+ "allow_full_trace_response": str(config.allow_full_trace_response).lower(),
100
+ "protect_diagnostics": str(config.protect_diagnostics).lower(),
101
+ "trust_forwarded_for": str(config.trust_forwarded_for).lower(),
102
+ "run_timeout_seconds": str(config.run_timeout_seconds),
103
+ "request_timeout_seconds": str(config.request_timeout_seconds),
104
+ "trace_persistence": "enabled" if config.trace_dir else "disabled",
105
+ "runtime_workspace": (
106
+ "enabled" if config.runtime_workspace_dir else "disabled"
107
+ ),
108
+ "runtime_workspace_kinds": ",".join(VIRTUAL_WORKSPACE_KINDS),
109
+ }
110
+ snapshot.update(security_response_header_snapshot())
111
+ snapshot.update(trace_permission_policy_snapshot())
112
+ snapshot.update(_external_backend_config(config).redacted_snapshot())
113
+ snapshot.update(_embedding_provider_snapshot(config))
114
+ snapshot.update(llm_provider_snapshot())
115
+ return snapshot
116
+
117
+
118
+ def trace_permission_policy_snapshot() -> Dict[str, str]:
119
+ return {
120
+ "trace_directory_permissions": "0700",
121
+ "trace_file_permissions": "0600",
122
+ "trace_probe_file_permissions": "0600",
123
+ }
124
+
125
+
126
+ def llm_provider_snapshot() -> Dict[str, str]:
127
+ return LLMProviderConfig.from_sources().redacted_snapshot()
128
+
129
+
130
+ def _embedding_provider_snapshot(config: ServiceConfig) -> Dict[str, str]:
131
+ return EmbeddingProviderConfig(
132
+ base_url=config.embedding_base_url,
133
+ api_key=config.embedding_api_key,
134
+ model=config.embedding_model,
135
+ timeout_seconds=config.embedding_timeout_seconds,
136
+ max_retries=config.embedding_max_retries,
137
+ retry_backoff_seconds=config.embedding_retry_backoff_seconds,
138
+ ).redacted_snapshot()
139
+
140
+
141
+ def security_response_header_snapshot() -> Dict[str, str]:
142
+ return {
143
+ "security_response_headers": "enabled",
144
+ "cache_control_header": service_transport.CACHE_CONTROL_HEADER_VALUE,
145
+ "content_security_policy_header": (
146
+ service_transport.CONTENT_SECURITY_POLICY_HEADER_VALUE
147
+ ),
148
+ "referrer_policy_header": service_transport.REFERRER_POLICY_HEADER_VALUE,
149
+ "x_frame_options_header": service_transport.X_FRAME_OPTIONS_HEADER_VALUE,
150
+ "x_content_type_options_header": service_transport.NOSNIFF_HEADER_VALUE,
151
+ }
152
+
153
+
154
+ def _runtime_allowed_tools_snapshot(config: ServiceConfig) -> str:
155
+ if not config.runtime_allowed_tools:
156
+ return "default"
157
+ return ",".join(config.runtime_allowed_tools)
158
+
159
+
160
+ def _readiness_check(check: Callable[[], None], failure_code: str) -> str:
161
+ try:
162
+ check()
163
+ except Exception: # pragma: no cover - defensive readiness detail
164
+ return f"failed: {failure_code}"
165
+ return "ok"
166
+
167
+
168
+ def _check_agent_config() -> None:
169
+ from kagent.core.state import AgentConfig
170
+
171
+ AgentConfig()
172
+
173
+
174
+ def _check_tools() -> None:
175
+ from kagent.core.tools import registered_tool_names
176
+
177
+ if not registered_tool_names():
178
+ raise RuntimeError("no tools registered")
179
+
180
+
181
+ def _check_trace_persistence(trace_dir: str) -> None:
182
+ output_dir = Path(trace_dir)
183
+ _ensure_owner_only_trace_dir(output_dir)
184
+ probe_path = _write_owner_only_temporary_trace(
185
+ output_dir,
186
+ f"readiness-{uuid4().hex}.probe",
187
+ "ok\n",
188
+ )
189
+ probe_path.unlink()
190
+
191
+
192
+ def _check_runtime_workspace(runtime_workspace_dir: str) -> None:
193
+ RuntimeWorkspace(runtime_workspace_dir).ensure_layout()
194
+
195
+
196
+ def _check_idempotency_cache_persistence(config: ServiceConfig) -> None:
197
+ SqliteServiceIdempotencyCache(
198
+ max_entries=config.idempotency_cache_size,
199
+ database_path=config.idempotency_cache_path,
200
+ ).snapshot()
201
+
202
+
203
+ def _embedding_provider_configured(config: ServiceConfig) -> bool:
204
+ return bool(
205
+ config.embedding_base_url
206
+ or config.embedding_api_key
207
+ or config.embedding_model
208
+ )
209
+
210
+
211
+ def _check_embedding_provider_config(config: ServiceConfig) -> None:
212
+ if not config.embedding_base_url:
213
+ raise ValueError("embedding base_url is required")
214
+ if not config.embedding_model:
215
+ raise ValueError("embedding model is required")
216
+ EmbeddingProviderConfig(
217
+ base_url=config.embedding_base_url,
218
+ api_key=config.embedding_api_key,
219
+ model=config.embedding_model,
220
+ timeout_seconds=config.embedding_timeout_seconds,
221
+ max_retries=config.embedding_max_retries,
222
+ retry_backoff_seconds=config.embedding_retry_backoff_seconds,
223
+ )
224
+
225
+
226
+ def _external_backend_config(config: ServiceConfig) -> ExternalBackendConfig:
227
+ return ExternalBackendConfig(
228
+ redis_url=config.redis_url,
229
+ milvus_url=config.milvus_url,
230
+ kafka_audit_url=config.kafka_audit_url,
231
+ kafka_audit_topic=config.kafka_audit_topic,
232
+ timeout_seconds=config.external_backend_timeout_seconds,
233
+ )