@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,20 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def validate_approved_action_ids(value: Any) -> tuple[list[str], str]:
7
+ if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
8
+ return [], "approved_action_ids must be an array of strings"
9
+ seen = set()
10
+ approved_action_ids = []
11
+ for action_id in value:
12
+ if not action_id.strip():
13
+ return [], "approved_action_ids must contain non-empty strings"
14
+ if action_id != action_id.strip():
15
+ return [], "approved_action_ids must not contain surrounding whitespace"
16
+ if action_id in seen:
17
+ return [], "approved_action_ids must not contain duplicate action ids"
18
+ seen.add(action_id)
19
+ approved_action_ids.append(action_id)
20
+ return approved_action_ids, ""
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Dict, Tuple
6
+
7
+ from kagent.service import errors as service_errors
8
+ from kagent.service.active_runs import ActiveRunRegistry, ActiveRunSnapshot
9
+ from kagent.service.errors import failure_payload
10
+ from kagent.service.runtime import ServiceConfig
11
+ from kagent.service.runtime_lifecycle import persist_cancelled_runtime_trace
12
+ from kagent.service.runtime_status import (
13
+ is_runtime_trace,
14
+ runtime_status_summary,
15
+ )
16
+ from kagent.service.trace_store import (
17
+ load_trace_by_run_id,
18
+ )
19
+ from kagent.utils.json_output import json_ready
20
+
21
+ _TRACE_READ_ERRORS = (OSError, ValueError)
22
+ _TERMINAL_RUNTIME_STATUSES = {"cancelled", "done", "failed", "resumed"}
23
+ MAX_CANCEL_REASON_CHARS = 500
24
+
25
+
26
+ def execute_runtime_cancel_request(
27
+ run_id: str,
28
+ body: bytes,
29
+ service_config: ServiceConfig,
30
+ auth_subject: str = "",
31
+ *,
32
+ request_auth_is_admin: bool = False,
33
+ active_run_registry: ActiveRunRegistry | None = None,
34
+ ) -> Tuple[int, Dict[str, Any]]:
35
+ if not service_config.trace_dir:
36
+ return 400, failure_payload(
37
+ service_errors.INVALID_AGENT_CONFIG,
38
+ "trace_dir is required for runtime cancel",
39
+ )
40
+ if not run_id.strip():
41
+ return 400, failure_payload(service_errors.INVALID_REQUEST_BODY, "run_id is required")
42
+ try:
43
+ payload = json.loads(body.decode("utf-8") or "{}")
44
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
45
+ return 400, failure_payload(service_errors.INVALID_JSON, f"invalid JSON: {exc}")
46
+ if not isinstance(payload, dict):
47
+ return 400, failure_payload(
48
+ service_errors.INVALID_REQUEST_BODY,
49
+ "request body must be a JSON object",
50
+ )
51
+ if set(payload) - {"reason"}:
52
+ return 400, failure_payload(
53
+ service_errors.INVALID_REQUEST_BODY,
54
+ "cancel request only accepts an optional reason field",
55
+ )
56
+ reason = payload.get("reason", "")
57
+ if not isinstance(reason, str):
58
+ return 400, failure_payload(
59
+ service_errors.INVALID_REQUEST_BODY,
60
+ "reason must be a string",
61
+ )
62
+ reason = reason.strip()
63
+ if len(reason) > MAX_CANCEL_REASON_CHARS:
64
+ return 400, failure_payload(
65
+ service_errors.INVALID_REQUEST_BODY,
66
+ f"reason must be at most {MAX_CANCEL_REASON_CHARS} characters",
67
+ )
68
+
69
+ try:
70
+ trace = load_trace_by_run_id(run_id, service_config.trace_dir)
71
+ except _TRACE_READ_ERRORS:
72
+ return 500, failure_payload(
73
+ service_errors.TRACE_READ_FAILED,
74
+ "runtime run trace could not be read",
75
+ )
76
+ if trace is None or not is_runtime_trace(trace):
77
+ return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
78
+ owner_auth_subject = str(trace.get("auth_subject", "")) or auth_subject
79
+ if auth_subject and not request_auth_is_admin and owner_auth_subject != auth_subject:
80
+ return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
81
+ if active_run_registry is not None:
82
+ active_run = active_run_registry.request_cancel(
83
+ run_id,
84
+ requested_by_auth_subject=auth_subject,
85
+ request_auth_is_admin=request_auth_is_admin,
86
+ reason=reason,
87
+ )
88
+ if active_run is not None:
89
+ if active_run.state != "cancelled":
90
+ return 409, failure_payload(
91
+ service_errors.INVALID_REQUEST_BODY,
92
+ "runtime run is already terminal",
93
+ )
94
+ try:
95
+ cancelled_trace = persist_cancelled_runtime_trace(
96
+ run_id=run_id,
97
+ trace_dir=service_config.trace_dir,
98
+ active_run=active_run,
99
+ cancelled_by_auth_subject=auth_subject,
100
+ )
101
+ except (OSError, ValueError) as exc:
102
+ return 500, failure_payload(
103
+ service_errors.TRACE_PERSISTENCE_FAILED,
104
+ f"could not persist trace: {exc}",
105
+ )
106
+ persisted_status = str(cancelled_trace.get("status", ""))
107
+ if persisted_status == "resuming":
108
+ return 409, failure_payload(
109
+ service_errors.INVALID_REQUEST_BODY,
110
+ "runtime run approval is being resumed",
111
+ )
112
+ if persisted_status != "cancelled":
113
+ return 409, failure_payload(
114
+ service_errors.INVALID_REQUEST_BODY,
115
+ "runtime run is already terminal",
116
+ )
117
+ return 200, json_ready(
118
+ runtime_status_summary(
119
+ cancelled_trace,
120
+ service_config.trace_dir,
121
+ run_id,
122
+ )
123
+ )
124
+ try:
125
+ refreshed_trace = load_trace_by_run_id(run_id, service_config.trace_dir)
126
+ except _TRACE_READ_ERRORS:
127
+ return 500, failure_payload(
128
+ service_errors.TRACE_READ_FAILED,
129
+ "runtime run trace could not be read",
130
+ )
131
+ if refreshed_trace is None or not is_runtime_trace(refreshed_trace):
132
+ return 404, failure_payload(
133
+ service_errors.NOT_FOUND,
134
+ "runtime run trace not found",
135
+ )
136
+ trace = refreshed_trace
137
+ owner_auth_subject = str(trace.get("auth_subject", "")) or auth_subject
138
+ if auth_subject and not request_auth_is_admin and owner_auth_subject != auth_subject:
139
+ return 404, failure_payload(
140
+ service_errors.NOT_FOUND,
141
+ "runtime run trace not found",
142
+ )
143
+ trace_status = str(trace.get("status", ""))
144
+ if trace_status in _TERMINAL_RUNTIME_STATUSES:
145
+ return 409, failure_payload(
146
+ service_errors.INVALID_REQUEST_BODY,
147
+ "runtime run is already terminal",
148
+ )
149
+ if trace_status == "resuming":
150
+ return 409, failure_payload(
151
+ service_errors.INVALID_REQUEST_BODY,
152
+ "runtime run approval is being resumed",
153
+ )
154
+
155
+ cancelled_at = _utc_timestamp()
156
+ try:
157
+ trace = persist_cancelled_runtime_trace(
158
+ run_id=run_id,
159
+ trace_dir=service_config.trace_dir,
160
+ active_run=ActiveRunSnapshot(
161
+ run_id=run_id,
162
+ owner_auth_subject=owner_auth_subject,
163
+ state="cancelled",
164
+ started_at=str(trace.get("started_at", "")) or cancelled_at,
165
+ cancel_reason=reason,
166
+ cancelled_at=cancelled_at,
167
+ ),
168
+ cancelled_by_auth_subject=auth_subject,
169
+ )
170
+ except (OSError, ValueError) as exc:
171
+ return 500, failure_payload(
172
+ service_errors.TRACE_PERSISTENCE_FAILED,
173
+ f"could not persist trace: {exc}",
174
+ )
175
+ persisted_status = str(trace.get("status", ""))
176
+ if persisted_status == "resuming":
177
+ return 409, failure_payload(
178
+ service_errors.INVALID_REQUEST_BODY,
179
+ "runtime run approval is being resumed",
180
+ )
181
+ if persisted_status != "cancelled":
182
+ return 409, failure_payload(
183
+ service_errors.INVALID_REQUEST_BODY,
184
+ "runtime run is already terminal",
185
+ )
186
+ return 200, json_ready(
187
+ runtime_status_summary(trace, service_config.trace_dir, run_id)
188
+ )
189
+
190
+
191
+ def _utc_timestamp() -> str:
192
+ return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,229 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Callable, Dict
6
+
7
+ from kagent.runtime import RUNTIME_TRACE_TYPE
8
+ from kagent.service.active_runs import ActiveRunSnapshot
9
+ from kagent.service.trace_store import (
10
+ load_trace_by_run_id,
11
+ persist_trace,
12
+ runtime_trace_lock,
13
+ trace_path_for_run_id,
14
+ )
15
+
16
+ _TERMINAL_RUNTIME_STATUSES = {"cancelled", "done", "failed", "resumed"}
17
+ _CANCELLATION_PROTECTED_STATUSES = _TERMINAL_RUNTIME_STATUSES | {"resuming"}
18
+ TracePersistFunction = Callable[[Dict[str, Any], str], str]
19
+
20
+
21
+ def running_runtime_trace(
22
+ *,
23
+ run_id: str,
24
+ goal: str,
25
+ max_iterations: int,
26
+ auth_subject: str = "",
27
+ resumed_from_run_id: str = "",
28
+ runtime_instance_id: str = "",
29
+ ) -> Dict[str, Any]:
30
+ started_at = _utc_timestamp()
31
+ trace: Dict[str, Any] = {
32
+ "trace_type": RUNTIME_TRACE_TYPE,
33
+ "run_id": run_id,
34
+ "status": "running",
35
+ "goal": goal,
36
+ "started_at": started_at,
37
+ "max_iterations": str(max_iterations),
38
+ "iteration_count": "0",
39
+ "iteration_budget_remaining": str(max_iterations),
40
+ "events": [],
41
+ "observations": [],
42
+ "plans": [],
43
+ "plan": {"actions": []},
44
+ }
45
+ if auth_subject:
46
+ trace["auth_subject"] = auth_subject
47
+ if resumed_from_run_id:
48
+ trace["resumed_from_run_id"] = resumed_from_run_id
49
+ if runtime_instance_id:
50
+ trace["runtime_instance_id"] = runtime_instance_id
51
+ return trace
52
+
53
+
54
+ def persist_cancelled_runtime_trace(
55
+ *,
56
+ run_id: str,
57
+ trace_dir: str,
58
+ active_run: ActiveRunSnapshot,
59
+ cancelled_by_auth_subject: str = "",
60
+ error_code: str = "run_cancelled",
61
+ error: str = "runtime run cancelled",
62
+ ) -> Dict[str, Any]:
63
+ with runtime_trace_lock(run_id, trace_dir):
64
+ trace = load_trace_by_run_id(run_id, trace_dir) or {
65
+ "trace_type": RUNTIME_TRACE_TYPE,
66
+ "run_id": run_id,
67
+ "status": "running",
68
+ "started_at": active_run.started_at,
69
+ "events": [],
70
+ }
71
+ current_status = str(trace.get("status", ""))
72
+ timeout_upgrade = (
73
+ active_run.state == "timed_out" and current_status == "cancelled"
74
+ )
75
+ if current_status in _CANCELLATION_PROTECTED_STATUSES and not timeout_upgrade:
76
+ return _with_trace_path(trace, run_id, trace_dir)
77
+ cancelled_at = active_run.cancelled_at or _utc_timestamp()
78
+ trace["status"] = "cancelled"
79
+ trace["completed_at"] = cancelled_at
80
+ trace["cancelled_at"] = cancelled_at
81
+ trace["error_code"] = error_code
82
+ trace["error"] = error
83
+ if cancelled_by_auth_subject:
84
+ trace["cancelled_by_auth_subject"] = cancelled_by_auth_subject
85
+ if active_run.cancel_reason:
86
+ trace["cancel_reason"] = active_run.cancel_reason
87
+ trace.pop("pending_approval", None)
88
+ _append_cancel_event(trace, cancelled_at, active_run.cancel_reason)
89
+ _refresh_duration_seconds(trace)
90
+ trace["trace_path"] = persist_trace(trace, trace_dir)
91
+ return trace
92
+
93
+
94
+ def persist_failed_runtime_trace(
95
+ *,
96
+ run_id: str,
97
+ trace_dir: str,
98
+ error_code: str,
99
+ error: str,
100
+ ) -> Dict[str, Any]:
101
+ with runtime_trace_lock(run_id, trace_dir):
102
+ trace = load_trace_by_run_id(run_id, trace_dir) or {
103
+ "trace_type": RUNTIME_TRACE_TYPE,
104
+ "run_id": run_id,
105
+ "status": "running",
106
+ "started_at": _utc_timestamp(),
107
+ "events": [],
108
+ }
109
+ if str(trace.get("status", "")) in _TERMINAL_RUNTIME_STATUSES:
110
+ return _with_trace_path(trace, run_id, trace_dir)
111
+ completed_at = _utc_timestamp()
112
+ trace["status"] = "failed"
113
+ trace["completed_at"] = completed_at
114
+ trace["error_code"] = error_code
115
+ trace["error"] = error
116
+ trace.pop("pending_approval", None)
117
+ _append_failure_event(trace, completed_at, error_code, error)
118
+ _refresh_duration_seconds(trace)
119
+ trace["trace_path"] = persist_trace(trace, trace_dir)
120
+ return trace
121
+
122
+
123
+ def persist_runtime_worker_result(
124
+ *,
125
+ run_id: str,
126
+ trace_dir: str,
127
+ result: Dict[str, Any],
128
+ persist_trace_fn: TracePersistFunction,
129
+ ) -> Dict[str, Any]:
130
+ """Persist a worker result unless another replica already committed a terminal state."""
131
+
132
+ with runtime_trace_lock(run_id, trace_dir):
133
+ try:
134
+ current = load_trace_by_run_id(run_id, trace_dir)
135
+ except ValueError:
136
+ current = None
137
+ if current is not None and str(current.get("status", "")) in (
138
+ _TERMINAL_RUNTIME_STATUSES
139
+ ):
140
+ return _with_trace_path(current, run_id, trace_dir)
141
+ result["trace_path"] = persist_trace_fn(result, trace_dir)
142
+ return result
143
+
144
+
145
+ def persisted_runtime_cancellation_probe(
146
+ *,
147
+ run_id: str,
148
+ trace_dir: str,
149
+ ) -> Dict[str, str] | None:
150
+ trace = load_trace_by_run_id(run_id, trace_dir)
151
+ if trace is None or str(trace.get("status", "")) != "cancelled":
152
+ return None
153
+ return {
154
+ "reason": str(trace.get("cancel_reason", "")),
155
+ "cancelled_at": str(trace.get("cancelled_at", "")),
156
+ }
157
+
158
+
159
+ def _with_trace_path(
160
+ trace: Dict[str, Any],
161
+ run_id: str,
162
+ trace_dir: str,
163
+ ) -> Dict[str, Any]:
164
+ trace["trace_path"] = str(trace_path_for_run_id(run_id, trace_dir))
165
+ return trace
166
+
167
+
168
+ def _append_cancel_event(trace: Dict[str, Any], cancelled_at: str, reason: str) -> None:
169
+ events = trace.get("events")
170
+ if not isinstance(events, list):
171
+ events = []
172
+ trace["events"] = events
173
+ if any(
174
+ isinstance(event, dict)
175
+ and event.get("node") == "control"
176
+ and event.get("status") == "cancelled"
177
+ for event in events
178
+ ):
179
+ return
180
+ event: Dict[str, Any] = {
181
+ "node": "control",
182
+ "status": "cancelled",
183
+ "started_at": cancelled_at,
184
+ "completed_at": cancelled_at,
185
+ "duration_seconds": "0.0000",
186
+ }
187
+ if reason:
188
+ event["reason"] = reason
189
+ events.append(event)
190
+
191
+
192
+ def _append_failure_event(
193
+ trace: Dict[str, Any],
194
+ completed_at: str,
195
+ error_code: str,
196
+ error: str,
197
+ ) -> None:
198
+ events = trace.get("events")
199
+ if not isinstance(events, list):
200
+ events = []
201
+ trace["events"] = events
202
+ events.append(
203
+ {
204
+ "node": "runtime",
205
+ "status": "failed",
206
+ "started_at": completed_at,
207
+ "completed_at": completed_at,
208
+ "duration_seconds": "0.0000",
209
+ "error_code": error_code,
210
+ "error": error,
211
+ }
212
+ )
213
+
214
+
215
+ def _refresh_duration_seconds(trace: Dict[str, Any]) -> None:
216
+ started_at = trace.get("started_at")
217
+ if not isinstance(started_at, str) or not started_at.strip():
218
+ return
219
+ try:
220
+ started = datetime.fromisoformat(started_at)
221
+ except ValueError:
222
+ return
223
+ if started.tzinfo is None:
224
+ started = started.replace(tzinfo=timezone.utc)
225
+ trace["duration_seconds"] = f"{max(0.0, time.time() - started.timestamp()):.4f}"
226
+
227
+
228
+ def _utc_timestamp() -> str:
229
+ return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from kagent.runtime.metadata import (
4
+ MAX_RUNTIME_METADATA_ENTRIES,
5
+ MAX_RUNTIME_METADATA_KEY_CHARS,
6
+ MAX_RUNTIME_METADATA_VALUE_CHARS,
7
+ MAX_RUNTIME_TAG_CHARS,
8
+ MAX_RUNTIME_TAGS,
9
+ validate_runtime_metadata,
10
+ validate_runtime_tags,
11
+ )
12
+
13
+ __all__ = [
14
+ "MAX_RUNTIME_METADATA_ENTRIES",
15
+ "MAX_RUNTIME_METADATA_KEY_CHARS",
16
+ "MAX_RUNTIME_METADATA_VALUE_CHARS",
17
+ "MAX_RUNTIME_TAG_CHARS",
18
+ "MAX_RUNTIME_TAGS",
19
+ "validate_runtime_metadata",
20
+ "validate_runtime_tags",
21
+ ]
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from hashlib import sha256
5
+ from typing import Any, Dict, Tuple
6
+
7
+ from kagent.runtime import RUNTIME_TRACE_TYPE
8
+ from kagent.runtime.policy import RuntimePolicy
9
+ from kagent.runtime.tools import default_runtime_tools
10
+ from kagent.service.runtime import ServiceConfig
11
+ from kagent.utils.json_output import json_ready
12
+
13
+
14
+ def execute_runtime_policy_request(
15
+ service_config: ServiceConfig,
16
+ *,
17
+ request_auth_subject: str = "",
18
+ request_auth_is_admin: bool = False,
19
+ ) -> Tuple[int, Dict[str, Any]]:
20
+ default_allowed_tools = _sorted_tuple(RuntimePolicy().allowed_tools)
21
+ global_allowed_tools = _sorted_tuple(service_config.runtime_allowed_tools)
22
+ subject_allowed_tools = _subject_allowed_tools(
23
+ service_config,
24
+ request_auth_subject=request_auth_subject,
25
+ request_auth_is_admin=request_auth_is_admin,
26
+ )
27
+ effective_allowed_tools, effective_policy_source = _effective_policy(
28
+ service_config,
29
+ request_auth_subject=request_auth_subject,
30
+ default_allowed_tools=default_allowed_tools,
31
+ global_allowed_tools=global_allowed_tools,
32
+ )
33
+ effective_tool_policy = _effective_tool_policy(effective_allowed_tools)
34
+ return 200, json_ready(
35
+ {
36
+ "trace_type": RUNTIME_TRACE_TYPE,
37
+ "auth_subject": request_auth_subject,
38
+ "is_admin": str(request_auth_is_admin).lower(),
39
+ "default_allowed_tools": list(default_allowed_tools),
40
+ "global_allowed_tools": list(global_allowed_tools),
41
+ "subject_policy_count": str(
42
+ len(service_config.runtime_allowed_tools_by_subject)
43
+ ),
44
+ "subject_allowed_tools": subject_allowed_tools,
45
+ "effective_allowed_tools": list(effective_allowed_tools),
46
+ "effective_policy_source": effective_policy_source,
47
+ "effective_tool_policy": effective_tool_policy,
48
+ "effective_tool_policy_sha256": _policy_sha256(
49
+ effective_tool_policy
50
+ ),
51
+ }
52
+ )
53
+
54
+
55
+ def _subject_allowed_tools(
56
+ service_config: ServiceConfig,
57
+ *,
58
+ request_auth_subject: str,
59
+ request_auth_is_admin: bool,
60
+ ) -> Dict[str, list[str]]:
61
+ subjects = service_config.runtime_allowed_tools_by_subject
62
+ if request_auth_is_admin:
63
+ return {
64
+ subject: list(_sorted_tuple(tools))
65
+ for subject, tools in sorted(subjects.items())
66
+ }
67
+ if request_auth_subject in subjects:
68
+ return {
69
+ request_auth_subject: list(
70
+ _sorted_tuple(subjects[request_auth_subject])
71
+ )
72
+ }
73
+ return {}
74
+
75
+
76
+ def _effective_policy(
77
+ service_config: ServiceConfig,
78
+ *,
79
+ request_auth_subject: str,
80
+ default_allowed_tools: tuple[str, ...],
81
+ global_allowed_tools: tuple[str, ...],
82
+ ) -> tuple[tuple[str, ...], str]:
83
+ subject_allowed_tools = service_config.runtime_allowed_tools_by_subject.get(
84
+ request_auth_subject,
85
+ )
86
+ if subject_allowed_tools is not None:
87
+ return _sorted_tuple(subject_allowed_tools), "subject"
88
+ if global_allowed_tools:
89
+ return global_allowed_tools, "global"
90
+ return default_allowed_tools, "default"
91
+
92
+
93
+ def _sorted_tuple(values: Any) -> tuple[str, ...]:
94
+ if not values:
95
+ return ()
96
+ return tuple(sorted(str(value) for value in values if str(value).strip()))
97
+
98
+
99
+ def _effective_tool_policy(
100
+ effective_allowed_tools: tuple[str, ...],
101
+ ) -> list[dict[str, str]]:
102
+ allowed_tools = set(effective_allowed_tools)
103
+ return [
104
+ {
105
+ "name": name,
106
+ "allowed": str(name in allowed_tools).lower(),
107
+ "approval_required": str(name not in allowed_tools).lower(),
108
+ }
109
+ for name in sorted(default_runtime_tools())
110
+ ]
111
+
112
+
113
+ def _policy_sha256(policy: list[dict[str, str]]) -> str:
114
+ payload = json.dumps(policy, sort_keys=True, separators=(",", ":"))
115
+ return sha256(payload.encode("utf-8")).hexdigest()