@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.
- package/README.md +353 -0
- package/npm/bin/kagent-serve.js +6 -0
- package/npm/bin/kagent.js +6 -0
- package/npm/lib/App.js +524 -0
- package/npm/lib/app-state.js +224 -0
- package/npm/lib/approval-choice.js +25 -0
- package/npm/lib/commands.js +59 -0
- package/npm/lib/editor.js +188 -0
- package/npm/lib/ink-runner.js +41 -0
- package/npm/lib/kagent-home.js +39 -0
- package/npm/lib/launcher.js +221 -0
- package/npm/lib/protocol.js +33 -0
- package/npm/lib/provider-setup.js +139 -0
- package/npm/lib/python-runner.js +892 -0
- package/npm/lib/runtime-client.js +390 -0
- package/npm/lib/terminal-input.js +127 -0
- package/npm/lib/terminal-text.js +19 -0
- package/npm/lib/terminal-width.js +14 -0
- package/npm/lib/transcript.js +227 -0
- package/npm/lib/ui-components.js +247 -0
- package/npm/lib/update-manager.js +334 -0
- package/package.json +39 -0
- package/pyproject.toml +55 -0
- package/src/kagent/__init__.py +90 -0
- package/src/kagent/cli/__init__.py +5 -0
- package/src/kagent/cli/__main__.py +6 -0
- package/src/kagent/cli/commands.py +112 -0
- package/src/kagent/cli/conversation.py +127 -0
- package/src/kagent/cli/interactive.py +841 -0
- package/src/kagent/cli/main.py +685 -0
- package/src/kagent/cli/memory.py +460 -0
- package/src/kagent/cli/pending_approval.py +169 -0
- package/src/kagent/cli/provider.py +27 -0
- package/src/kagent/cli/session_commands.py +401 -0
- package/src/kagent/cli/stdio_runtime.py +784 -0
- package/src/kagent/cli/trace.py +67 -0
- package/src/kagent/cli/ui.py +931 -0
- package/src/kagent/core/__init__.py +0 -0
- package/src/kagent/core/agent.py +296 -0
- package/src/kagent/core/faults.py +11 -0
- package/src/kagent/core/invariants.py +47 -0
- package/src/kagent/core/normalization.py +81 -0
- package/src/kagent/core/planning.py +48 -0
- package/src/kagent/core/state.py +73 -0
- package/src/kagent/core/summary.py +64 -0
- package/src/kagent/core/tools.py +196 -0
- package/src/kagent/core/trace.py +57 -0
- package/src/kagent/eval/__init__.py +11 -0
- package/src/kagent/eval/cases.py +229 -0
- package/src/kagent/eval/evaluator.py +216 -0
- package/src/kagent/integrations/__init__.py +3 -0
- package/src/kagent/integrations/audit.py +95 -0
- package/src/kagent/integrations/backends.py +131 -0
- package/src/kagent/integrations/memory.py +301 -0
- package/src/kagent/ops/__init__.py +0 -0
- package/src/kagent/ops/batch.py +156 -0
- package/src/kagent/ops/doctor.py +255 -0
- package/src/kagent/ops/metrics.py +214 -0
- package/src/kagent/ops/release_evidence.py +877 -0
- package/src/kagent/ops/release_manifest.py +142 -0
- package/src/kagent/ops/trace_replay.py +285 -0
- package/src/kagent/providers/__init__.py +7 -0
- package/src/kagent/providers/embeddings.py +187 -0
- package/src/kagent/providers/llm.py +770 -0
- package/src/kagent/py.typed +1 -0
- package/src/kagent/runtime/__init__.py +28 -0
- package/src/kagent/runtime/action_graph.py +543 -0
- package/src/kagent/runtime/agent.py +2089 -0
- package/src/kagent/runtime/approval.py +64 -0
- package/src/kagent/runtime/cancellation.py +64 -0
- package/src/kagent/runtime/checkpoint_state.py +146 -0
- package/src/kagent/runtime/checkpoint_storage.py +270 -0
- package/src/kagent/runtime/context.py +65 -0
- package/src/kagent/runtime/file_transaction.py +195 -0
- package/src/kagent/runtime/hooks.py +74 -0
- package/src/kagent/runtime/metadata.py +116 -0
- package/src/kagent/runtime/patch_checkpoints.py +385 -0
- package/src/kagent/runtime/policy.py +50 -0
- package/src/kagent/runtime/presentation.py +205 -0
- package/src/kagent/runtime/redaction.py +130 -0
- package/src/kagent/runtime/sandbox.py +331 -0
- package/src/kagent/runtime/skills.py +66 -0
- package/src/kagent/runtime/steering.py +56 -0
- package/src/kagent/runtime/steps.py +255 -0
- package/src/kagent/runtime/task_state.py +40 -0
- package/src/kagent/runtime/tools.py +3532 -0
- package/src/kagent/runtime/types.py +240 -0
- package/src/kagent/runtime/workspace.py +597 -0
- package/src/kagent/service/__init__.py +26 -0
- package/src/kagent/service/__main__.py +6 -0
- package/src/kagent/service/active_runs.py +178 -0
- package/src/kagent/service/cli.py +737 -0
- package/src/kagent/service/contract.py +2571 -0
- package/src/kagent/service/errors.py +71 -0
- package/src/kagent/service/idempotency.py +584 -0
- package/src/kagent/service/router.py +884 -0
- package/src/kagent/service/run.py +150 -0
- package/src/kagent/service/runtime.py +1915 -0
- package/src/kagent/service/runtime_approval.py +20 -0
- package/src/kagent/service/runtime_cancel.py +192 -0
- package/src/kagent/service/runtime_lifecycle.py +229 -0
- package/src/kagent/service/runtime_metadata.py +21 -0
- package/src/kagent/service/runtime_policy.py +115 -0
- package/src/kagent/service/runtime_recovery.py +573 -0
- package/src/kagent/service/runtime_resume.py +382 -0
- package/src/kagent/service/runtime_resume_claim.py +116 -0
- package/src/kagent/service/runtime_run.py +350 -0
- package/src/kagent/service/runtime_status.py +2007 -0
- package/src/kagent/service/safety.py +139 -0
- package/src/kagent/service/server.py +114 -0
- package/src/kagent/service/status.py +233 -0
- package/src/kagent/service/trace_store.py +322 -0
- package/src/kagent/service/transport.py +24 -0
- package/src/kagent/utils/__init__.py +0 -0
- package/src/kagent/utils/config_validation.py +21 -0
- package/src/kagent/utils/json_output.py +23 -0
- 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
|
+
)
|