@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,737 @@
1
+ from __future__ import annotations
2
+
3
+ # ruff: noqa: E402, I001
4
+
5
+ import argparse
6
+ import json
7
+ import signal
8
+ import socket
9
+ import sys
10
+ import time
11
+ import warnings
12
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
13
+ from typing import Any, List, Mapping, Optional
14
+ from urllib.parse import urlparse
15
+ from uuid import uuid4
16
+
17
+ warnings.filterwarnings("ignore")
18
+
19
+ from kagent.service import (
20
+ contract as service_contract,
21
+ )
22
+ from kagent.service import (
23
+ errors as service_errors,
24
+ )
25
+ from kagent.service import (
26
+ router as service_router,
27
+ )
28
+ from kagent.service import (
29
+ run as service_run,
30
+ )
31
+ from kagent.service import (
32
+ runtime as service_runtime,
33
+ )
34
+ from kagent.service import (
35
+ safety as service_safety,
36
+ )
37
+ from kagent.service import (
38
+ server as service_server,
39
+ )
40
+ from kagent.service import (
41
+ status as service_status,
42
+ )
43
+ from kagent.service import (
44
+ trace_store as service_trace_store,
45
+ )
46
+ from kagent.service import (
47
+ transport as service_transport,
48
+ )
49
+ from kagent.service.runtime import (
50
+ ServiceConcurrencyLimiter,
51
+ ServiceConfig,
52
+ ServiceIdempotencyCache,
53
+ ServiceMetrics,
54
+ ServiceRateLimiter,
55
+ access_log_record,
56
+ )
57
+ from kagent.service.active_runs import ActiveRunRegistry
58
+ from kagent.utils.json_output import json_ready
59
+
60
+ _ALLOWED_HTTP_METHODS = service_contract.ALLOWED_HTTP_METHODS
61
+ _KNOWN_METRICS_PATHS = frozenset(
62
+ {
63
+ "/config",
64
+ "/health",
65
+ "/metrics",
66
+ "/metrics.prom",
67
+ "/openapi.json",
68
+ "/ready",
69
+ "/run",
70
+ "/runtime/resume",
71
+ "/runtime/approvals",
72
+ "/runtime/approvals/summary",
73
+ "/runtime/graph",
74
+ "/runtime/policy",
75
+ "/runtime/runs",
76
+ "/runtime/runs/summary",
77
+ "/runtime/runs/{run_id}",
78
+ "/runtime/runs/{run_id}/artifacts",
79
+ "/runtime/runs/{run_id}/artifacts/{artifact_id}",
80
+ "/runtime/runs/{run_id}/cancel",
81
+ "/runtime/runs/{run_id}/timeline",
82
+ "/runtime/run",
83
+ "/runtime/tools",
84
+ "/tools",
85
+ "/version",
86
+ }
87
+ )
88
+ _UNKNOWN_METRICS_PATH = "__unknown__"
89
+ access_log_schema = service_runtime.access_log_schema
90
+ service_openapi = service_contract.service_openapi
91
+ readiness_payload = service_status.readiness_payload
92
+ service_config_snapshot = service_status.service_config_snapshot
93
+ _failure_payload = service_errors.failure_payload
94
+ _authorized = service_safety.authorized
95
+ _json_content_type = service_safety.json_content_type
96
+ _json_ready = json_ready
97
+ _rate_limit_key = service_safety.rate_limit_key
98
+ _request_id_from_headers = service_safety.request_id_from_headers
99
+ _persist_trace = service_trace_store.persist_trace
100
+ handle_request = service_router.handle_request
101
+ _handle_run = service_run.execute_run_request
102
+ _run_with_timeout = service_run.run_with_timeout
103
+ _optional_int = service_run.optional_int
104
+ _payload_error_code = service_transport.error_code_from_payload
105
+ _metrics_snapshot = service_router.metrics_snapshot
106
+ _agent_run_status = service_router.agent_run_status
107
+
108
+
109
+ class _SignalShutdown(Exception):
110
+ def __init__(self, signum: int) -> None:
111
+ self.signum = signum
112
+ super().__init__(f"received signal {signum}")
113
+
114
+
115
+ def create_server(
116
+ host: str,
117
+ port: int,
118
+ *,
119
+ config: Optional[ServiceConfig] = None,
120
+ ) -> ThreadingHTTPServer:
121
+ return service_server.create_threading_server(
122
+ host,
123
+ port,
124
+ _AgentRequestHandler,
125
+ config=config,
126
+ )
127
+
128
+
129
+ def main(argv: Optional[List[str]] = None) -> int:
130
+ _suppress_noisy_dependency_warnings()
131
+ parser = argparse.ArgumentParser(description="Serve the kagent API.")
132
+ try:
133
+ defaults = ServiceConfig.from_env()
134
+ except ValueError as exc:
135
+ parser.error(str(exc))
136
+ parser.add_argument("--host", default=defaults.host)
137
+ parser.add_argument("--port", type=int, default=defaults.port)
138
+ parser.add_argument("--max-request-bytes", type=int, default=defaults.max_request_bytes)
139
+ parser.add_argument("--max-goal-chars", type=int, default=defaults.max_goal_chars)
140
+ parser.add_argument("--auth-token", default=defaults.auth_token)
141
+ parser.add_argument("--rate-limit-per-minute", type=int, default=defaults.rate_limit_per_minute)
142
+ parser.add_argument("--max-concurrent-runs", type=int, default=defaults.max_concurrent_runs)
143
+ parser.add_argument(
144
+ "--idempotency-cache-size",
145
+ type=int,
146
+ default=defaults.idempotency_cache_size,
147
+ )
148
+ parser.add_argument(
149
+ "--idempotency-cache-path",
150
+ default=defaults.idempotency_cache_path,
151
+ help="Optional SQLite file for persistent/shared Idempotency-Key responses.",
152
+ )
153
+ parser.add_argument(
154
+ "--runtime-max-iterations",
155
+ type=int,
156
+ default=defaults.runtime_max_iterations,
157
+ help="Maximum runtime planner iterations per request.",
158
+ )
159
+ parser.add_argument(
160
+ "--runtime-pending-approval-stale-seconds",
161
+ type=int,
162
+ default=defaults.runtime_pending_approval_stale_seconds,
163
+ help=(
164
+ "Age threshold for stale pending approval gauges exposed by "
165
+ "/metrics and /metrics.prom."
166
+ ),
167
+ )
168
+ parser.add_argument(
169
+ "--runtime-instance-heartbeat-seconds",
170
+ type=float,
171
+ default=defaults.runtime_instance_heartbeat_seconds,
172
+ help="Heartbeat interval for this service instance in the shared trace store.",
173
+ )
174
+ parser.add_argument(
175
+ "--runtime-orphaned-run-stale-seconds",
176
+ type=float,
177
+ default=defaults.runtime_orphaned_run_stale_seconds,
178
+ help="Owner heartbeat age after which interrupted runtime runs are recovered.",
179
+ )
180
+ parser.add_argument(
181
+ "--runtime-allowed-tools",
182
+ default=",".join(defaults.runtime_allowed_tools),
183
+ help=(
184
+ "Comma-separated runtime tools allowed to execute without approval; "
185
+ "empty uses the default policy."
186
+ ),
187
+ )
188
+ parser.add_argument(
189
+ "--runtime-allowed-tools-by-subject",
190
+ default=_subject_tools_json(defaults.runtime_allowed_tools_by_subject),
191
+ help=(
192
+ "JSON object mapping auth_subject values to comma-separated tool lists "
193
+ "or arrays of tool names."
194
+ ),
195
+ )
196
+ parser.add_argument(
197
+ "--allow-full-trace-response",
198
+ action=argparse.BooleanOptionalAction,
199
+ default=defaults.allow_full_trace_response,
200
+ help="Allow POST /run full_trace=true to return internal trace bodies.",
201
+ )
202
+ parser.add_argument(
203
+ "--protect-diagnostics",
204
+ action=argparse.BooleanOptionalAction,
205
+ default=defaults.protect_diagnostics,
206
+ help="Require bearer auth for diagnostic GET endpoints.",
207
+ )
208
+ parser.add_argument(
209
+ "--trust-forwarded-for",
210
+ action="store_true",
211
+ default=defaults.trust_forwarded_for,
212
+ )
213
+ parser.add_argument("--trace-dir", default=defaults.trace_dir)
214
+ parser.add_argument(
215
+ "--runtime-workspace-dir",
216
+ default=defaults.runtime_workspace_dir,
217
+ help=(
218
+ "Optional runtime virtual workspace root for /workspace, "
219
+ "/reports, /logs, /policies, and /memories assets."
220
+ ),
221
+ )
222
+ parser.add_argument("--run-timeout-seconds", type=float, default=defaults.run_timeout_seconds)
223
+ parser.add_argument(
224
+ "--request-timeout-seconds",
225
+ type=float,
226
+ default=defaults.request_timeout_seconds,
227
+ )
228
+ args = parser.parse_args(argv)
229
+ try:
230
+ config = ServiceConfig(
231
+ host=args.host,
232
+ port=args.port,
233
+ max_request_bytes=args.max_request_bytes,
234
+ max_goal_chars=args.max_goal_chars,
235
+ auth_token=args.auth_token,
236
+ auth_tokens=defaults.auth_tokens,
237
+ rate_limit_per_minute=args.rate_limit_per_minute,
238
+ max_concurrent_runs=args.max_concurrent_runs,
239
+ idempotency_cache_size=args.idempotency_cache_size,
240
+ idempotency_cache_path=args.idempotency_cache_path,
241
+ runtime_allowed_tools=_csv_tuple(args.runtime_allowed_tools),
242
+ runtime_allowed_tools_by_subject=_subject_tools_map(
243
+ args.runtime_allowed_tools_by_subject
244
+ ),
245
+ runtime_max_iterations=args.runtime_max_iterations,
246
+ runtime_pending_approval_stale_seconds=(
247
+ args.runtime_pending_approval_stale_seconds
248
+ ),
249
+ runtime_instance_heartbeat_seconds=(
250
+ args.runtime_instance_heartbeat_seconds
251
+ ),
252
+ runtime_orphaned_run_stale_seconds=(
253
+ args.runtime_orphaned_run_stale_seconds
254
+ ),
255
+ allow_full_trace_response=args.allow_full_trace_response,
256
+ protect_diagnostics=args.protect_diagnostics,
257
+ trust_forwarded_for=args.trust_forwarded_for,
258
+ trace_dir=args.trace_dir,
259
+ runtime_workspace_dir=args.runtime_workspace_dir,
260
+ redis_url=defaults.redis_url,
261
+ milvus_url=defaults.milvus_url,
262
+ kafka_audit_url=defaults.kafka_audit_url,
263
+ kafka_audit_topic=defaults.kafka_audit_topic,
264
+ external_backend_timeout_seconds=(
265
+ defaults.external_backend_timeout_seconds
266
+ ),
267
+ run_timeout_seconds=args.run_timeout_seconds,
268
+ request_timeout_seconds=args.request_timeout_seconds,
269
+ )
270
+ except ValueError as exc:
271
+ parser.error(str(exc))
272
+
273
+ server = create_server(config.host, config.port, config=config)
274
+ host, port = server.server_address
275
+ print(json.dumps({"status": "serving", "host": host, "port": port}), flush=True)
276
+ previous_sigterm = signal.getsignal(signal.SIGTERM)
277
+ signal.signal(signal.SIGTERM, _raise_signal_shutdown)
278
+ try:
279
+ server.serve_forever()
280
+ except _SignalShutdown as exc:
281
+ return 128 + exc.signum
282
+ except KeyboardInterrupt:
283
+ return 130
284
+ finally:
285
+ signal.signal(signal.SIGTERM, previous_sigterm)
286
+ server.server_close()
287
+ return 0
288
+
289
+
290
+ def _suppress_noisy_dependency_warnings() -> None:
291
+ warnings.filterwarnings("ignore")
292
+
293
+
294
+ def _raise_signal_shutdown(signum: int, _frame: Any) -> None:
295
+ raise _SignalShutdown(signum)
296
+
297
+
298
+ def _csv_tuple(value: str) -> tuple[str, ...]:
299
+ if not value:
300
+ return ()
301
+ return tuple(sorted({item.strip() for item in value.split(",") if item.strip()}))
302
+
303
+
304
+ def _subject_tools_json(value: Mapping[str, tuple[str, ...]]) -> str:
305
+ if not value:
306
+ return ""
307
+ return json.dumps({subject: list(tools) for subject, tools in value.items()})
308
+
309
+
310
+ def _subject_tools_map(value: str) -> dict[str, tuple[str, ...]]:
311
+ if not value:
312
+ return {}
313
+ payload = json.loads(value)
314
+ if not isinstance(payload, dict):
315
+ raise ValueError("runtime_allowed_tools_by_subject must be a JSON object")
316
+ result: dict[str, tuple[str, ...]] = {}
317
+ for subject, tools in payload.items():
318
+ if isinstance(tools, str):
319
+ result[str(subject)] = _csv_tuple(tools)
320
+ elif isinstance(tools, list) and all(isinstance(item, str) for item in tools):
321
+ result[str(subject)] = tuple(sorted({item.strip() for item in tools if item.strip()}))
322
+ else:
323
+ raise ValueError(
324
+ "runtime_allowed_tools_by_subject values must be strings or arrays of strings"
325
+ )
326
+ return result
327
+
328
+
329
+ class _AgentRequestHandler(BaseHTTPRequestHandler):
330
+ server_version = "kagentHTTP/0.1"
331
+
332
+ def version_string(self) -> str:
333
+ return self.server_version
334
+
335
+ def setup(self) -> None:
336
+ super().setup()
337
+ self.connection.settimeout(self._config().request_timeout_seconds)
338
+
339
+ def do_GET(self) -> None:
340
+ self._started_at = time.perf_counter()
341
+ self._request_id_value = _request_id_from_headers(self.headers)
342
+ self._idempotency_key_present = None
343
+ if self._has_ambiguous_authorization():
344
+ self._send_unauthorized_response()
345
+ return
346
+ self._send_response(
347
+ *handle_request(
348
+ "GET",
349
+ self.path,
350
+ b"",
351
+ headers=dict(self.headers.items()),
352
+ config=self._config(),
353
+ metrics=self._metrics(),
354
+ rate_limiter=self._rate_limiter(),
355
+ concurrency_limiter=self._concurrency_limiter(),
356
+ active_run_registry=self._active_run_registry(),
357
+ idempotency_cache=self._idempotency_cache(),
358
+ remote_addr=self._remote_addr(),
359
+ )
360
+ )
361
+
362
+ def do_HEAD(self) -> None:
363
+ self._started_at = time.perf_counter()
364
+ self._request_id_value = _request_id_from_headers(self.headers)
365
+ self._idempotency_key_present = None
366
+ if self._has_ambiguous_authorization():
367
+ self._send_unauthorized_response()
368
+ return
369
+ self._send_response(
370
+ *handle_request(
371
+ "GET",
372
+ self.path,
373
+ b"",
374
+ headers=dict(self.headers.items()),
375
+ config=self._config(),
376
+ metrics=self._metrics(),
377
+ rate_limiter=self._rate_limiter(),
378
+ concurrency_limiter=self._concurrency_limiter(),
379
+ active_run_registry=self._active_run_registry(),
380
+ idempotency_cache=self._idempotency_cache(),
381
+ remote_addr=self._remote_addr(),
382
+ ),
383
+ write_body=False,
384
+ )
385
+
386
+ def do_OPTIONS(self) -> None:
387
+ self._started_at = time.perf_counter()
388
+ self._request_id_value = _request_id_from_headers(self.headers)
389
+ self._idempotency_key_present = None
390
+ self._send_empty_response(204, headers={"Allow": _ALLOWED_HTTP_METHODS})
391
+
392
+ def do_POST(self) -> None:
393
+ self._started_at = time.perf_counter()
394
+ self._request_id_value = _request_id_from_headers(self.headers)
395
+ self._request_body_bytes = None
396
+ if self._has_ambiguous_authorization():
397
+ self._idempotency_key_present = None
398
+ self._send_unauthorized_response()
399
+ return
400
+ expect_headers = self.headers.get_all("Expect", [])
401
+ if expect_headers:
402
+ self._idempotency_key_present = None
403
+ self._send_response(
404
+ 417,
405
+ _failure_payload(
406
+ service_errors.EXPECTATION_FAILED,
407
+ "expect header is unsupported",
408
+ ),
409
+ )
410
+ return
411
+ idempotency_keys = self.headers.get_all("Idempotency-Key", [])
412
+ self._idempotency_key_present = bool(idempotency_keys)
413
+ if len(idempotency_keys) > 1:
414
+ self._send_response(
415
+ 400,
416
+ _failure_payload(
417
+ service_errors.INVALID_IDEMPOTENCY_KEY,
418
+ "idempotency key must be single-valued",
419
+ ),
420
+ )
421
+ return
422
+ transfer_encodings = self.headers.get_all("Transfer-Encoding", [])
423
+ if transfer_encodings:
424
+ self._send_response(
425
+ 400,
426
+ _failure_payload(
427
+ service_errors.INVALID_TRANSFER_ENCODING,
428
+ "transfer-encoding is unsupported",
429
+ ),
430
+ )
431
+ return
432
+ content_types = self.headers.get_all("Content-Type", [])
433
+ if len(content_types) > 1:
434
+ self._send_response(
435
+ 415,
436
+ _failure_payload(
437
+ service_errors.UNSUPPORTED_MEDIA_TYPE,
438
+ "content-type must be single-valued application/json",
439
+ ),
440
+ )
441
+ return
442
+ content_lengths = self.headers.get_all("Content-Length", [])
443
+ if len(content_lengths) != 1:
444
+ self._send_response(
445
+ 400,
446
+ _failure_payload(service_errors.INVALID_CONTENT_LENGTH, "invalid content-length"),
447
+ )
448
+ return
449
+ try:
450
+ length = int(content_lengths[0])
451
+ except ValueError:
452
+ self._send_response(
453
+ 400,
454
+ _failure_payload(service_errors.INVALID_CONTENT_LENGTH, "invalid content-length"),
455
+ )
456
+ return
457
+ if length < 0:
458
+ self._send_response(
459
+ 400,
460
+ _failure_payload(service_errors.INVALID_CONTENT_LENGTH, "invalid content-length"),
461
+ )
462
+ return
463
+ if length > self._config().max_request_bytes:
464
+ self._send_response(
465
+ 413,
466
+ _failure_payload(service_errors.REQUEST_TOO_LARGE, "request body too large"),
467
+ )
468
+ return
469
+ try:
470
+ body = self.rfile.read(length) if length else b""
471
+ except socket.timeout:
472
+ payload = _failure_payload(
473
+ service_errors.REQUEST_BODY_TIMEOUT,
474
+ "timed out while reading request body",
475
+ )
476
+ payload["retry_after_seconds"] = "1"
477
+ self._send_response(
478
+ 408,
479
+ payload,
480
+ )
481
+ return
482
+ self._request_body_bytes = len(body)
483
+ if len(body) != length:
484
+ self._send_response(
485
+ 400,
486
+ _failure_payload(
487
+ service_errors.INCOMPLETE_REQUEST_BODY,
488
+ "request body ended before content-length bytes were read",
489
+ ),
490
+ )
491
+ return
492
+ self._send_response(
493
+ *handle_request(
494
+ "POST",
495
+ self.path,
496
+ body,
497
+ headers=dict(self.headers.items()),
498
+ config=self._config(),
499
+ metrics=self._metrics(),
500
+ rate_limiter=self._rate_limiter(),
501
+ concurrency_limiter=self._concurrency_limiter(),
502
+ active_run_registry=self._active_run_registry(),
503
+ idempotency_cache=self._idempotency_cache(),
504
+ remote_addr=self._remote_addr(),
505
+ )
506
+ )
507
+
508
+ def do_DELETE(self) -> None:
509
+ self._send_method_not_allowed()
510
+
511
+ def do_PATCH(self) -> None:
512
+ self._send_method_not_allowed()
513
+
514
+ def do_PUT(self) -> None:
515
+ self._send_method_not_allowed()
516
+
517
+ def log_message(self, format: str, *args: Any) -> None:
518
+ return None
519
+
520
+ def _send_method_not_allowed(self) -> None:
521
+ self._started_at = time.perf_counter()
522
+ self._request_id_value = _request_id_from_headers(self.headers)
523
+ self._idempotency_key_present = None
524
+ if self._has_ambiguous_authorization():
525
+ self._send_unauthorized_response()
526
+ return
527
+ self._send_response(
528
+ 405,
529
+ _failure_payload(service_errors.METHOD_NOT_ALLOWED, "method not allowed"),
530
+ headers={"Allow": _ALLOWED_HTTP_METHODS},
531
+ )
532
+
533
+ def _send_response(
534
+ self,
535
+ status_code: int,
536
+ payload: Any,
537
+ *,
538
+ headers: Optional[Mapping[str, str]] = None,
539
+ write_body: bool = True,
540
+ ) -> None:
541
+ data, content_type = service_transport.response_body(payload)
542
+ self.send_response(status_code)
543
+ self.send_header("Content-Type", content_type)
544
+ self.send_header("X-Content-Type-Options", service_transport.NOSNIFF_HEADER_VALUE)
545
+ self.send_header("Cache-Control", service_transport.CACHE_CONTROL_HEADER_VALUE)
546
+ self.send_header("Referrer-Policy", service_transport.REFERRER_POLICY_HEADER_VALUE)
547
+ self.send_header(
548
+ "Content-Security-Policy",
549
+ service_transport.CONTENT_SECURITY_POLICY_HEADER_VALUE,
550
+ )
551
+ self.send_header("X-Frame-Options", service_transport.X_FRAME_OPTIONS_HEADER_VALUE)
552
+ self.send_header("Content-Length", str(len(data)))
553
+ self.send_header("X-Request-ID", self._request_id())
554
+ run_id = _payload_field(payload, "run_id")
555
+ if _safe_response_header_value(run_id):
556
+ self.send_header("X-Run-ID", run_id)
557
+ trace_path = _payload_field(payload, "trace_path")
558
+ if _safe_trace_path_response_header_value(trace_path):
559
+ self.send_header("X-Trace-Path", trace_path)
560
+ if status_code == 401:
561
+ self.send_header("WWW-Authenticate", "Bearer")
562
+ retry_after = _retry_after_value(status_code, payload)
563
+ if retry_after:
564
+ self.send_header("Retry-After", retry_after)
565
+ for header_name, header_value in (headers or {}).items():
566
+ self.send_header(header_name, header_value)
567
+ self.end_headers()
568
+ if write_body:
569
+ self.wfile.write(data)
570
+ self._write_access_log(status_code, payload)
571
+
572
+ def _send_empty_response(
573
+ self,
574
+ status_code: int,
575
+ *,
576
+ headers: Optional[Mapping[str, str]] = None,
577
+ ) -> None:
578
+ self.send_response(status_code)
579
+ self.send_header("X-Content-Type-Options", service_transport.NOSNIFF_HEADER_VALUE)
580
+ self.send_header("Cache-Control", service_transport.CACHE_CONTROL_HEADER_VALUE)
581
+ self.send_header("Referrer-Policy", service_transport.REFERRER_POLICY_HEADER_VALUE)
582
+ self.send_header(
583
+ "Content-Security-Policy",
584
+ service_transport.CONTENT_SECURITY_POLICY_HEADER_VALUE,
585
+ )
586
+ self.send_header("X-Frame-Options", service_transport.X_FRAME_OPTIONS_HEADER_VALUE)
587
+ self.send_header("Content-Length", "0")
588
+ self.send_header("X-Request-ID", self._request_id())
589
+ for header_name, header_value in (headers or {}).items():
590
+ self.send_header(header_name, header_value)
591
+ self.end_headers()
592
+ self._write_access_log(status_code, {})
593
+
594
+ def _send_unauthorized_response(self) -> None:
595
+ self._send_response(
596
+ 401,
597
+ _failure_payload(service_errors.UNAUTHORIZED, "unauthorized"),
598
+ )
599
+
600
+ def _has_ambiguous_authorization(self) -> bool:
601
+ authorization_headers = self.headers.get_all("Authorization", [])
602
+ return bool(self._config().auth_required and len(authorization_headers) > 1)
603
+
604
+ def _config(self) -> ServiceConfig:
605
+ return getattr(self.server, "service_config", ServiceConfig())
606
+
607
+ def _metrics(self) -> ServiceMetrics:
608
+ return getattr(self.server, "service_metrics", ServiceMetrics())
609
+
610
+ def _rate_limiter(self) -> ServiceRateLimiter:
611
+ return getattr(self.server, "service_rate_limiter", ServiceRateLimiter(limit_per_minute=0))
612
+
613
+ def _concurrency_limiter(self) -> ServiceConcurrencyLimiter:
614
+ return getattr(
615
+ self.server,
616
+ "service_concurrency_limiter",
617
+ ServiceConcurrencyLimiter(max_concurrent_runs=0),
618
+ )
619
+
620
+ def _idempotency_cache(self) -> ServiceIdempotencyCache:
621
+ return getattr(
622
+ self.server,
623
+ "service_idempotency_cache",
624
+ ServiceIdempotencyCache(max_entries=0),
625
+ )
626
+
627
+ def _active_run_registry(self) -> ActiveRunRegistry:
628
+ return getattr(
629
+ self.server,
630
+ "service_active_run_registry",
631
+ ActiveRunRegistry(),
632
+ )
633
+
634
+ def _request_id(self) -> str:
635
+ return getattr(self, "_request_id_value", str(uuid4()))
636
+
637
+ def _write_access_log(self, status_code: int, payload: Any) -> None:
638
+ started_at = getattr(self, "_started_at", time.perf_counter())
639
+ duration_seconds = time.perf_counter() - started_at
640
+ error_code = _payload_error_code(payload)
641
+ run_id = _payload_field(payload, "run_id")
642
+ trace_path = _payload_field(payload, "trace_path")
643
+ runtime_owner_auth_subject = _payload_field(payload, "auth_subject")
644
+ resumed_by_auth_subject = _payload_field(payload, "resumed_by_auth_subject")
645
+ approved_by_auth_subject = _payload_field(payload, "approved_by_auth_subject")
646
+ idempotency_key_present = getattr(self, "_idempotency_key_present", None)
647
+ request_body_bytes = getattr(self, "_request_body_bytes", None)
648
+ config = self._config() if hasattr(self, "_config") else ServiceConfig()
649
+ raw_headers = dict(self.headers.items()) if hasattr(self, "headers") else {}
650
+ record = access_log_record(
651
+ method=self.command,
652
+ path=urlparse(self.path).path,
653
+ status_code=status_code,
654
+ duration_seconds=duration_seconds,
655
+ request_id=self._request_id(),
656
+ remote_addr=self._remote_addr(),
657
+ error_code=error_code,
658
+ run_id=run_id,
659
+ trace_path=trace_path,
660
+ idempotency_key_present=idempotency_key_present,
661
+ request_body_bytes=request_body_bytes,
662
+ auth_subject=service_safety.authenticated_subject(
663
+ raw_headers,
664
+ config.auth_token,
665
+ config.auth_tokens,
666
+ ),
667
+ runtime_owner_auth_subject=runtime_owner_auth_subject,
668
+ resumed_by_auth_subject=resumed_by_auth_subject,
669
+ approved_by_auth_subject=approved_by_auth_subject,
670
+ )
671
+ self._metrics().record(
672
+ method=record["method"],
673
+ path=_metrics_path(record["path"]),
674
+ status_code=status_code,
675
+ duration_seconds=duration_seconds,
676
+ error_code=error_code,
677
+ auth_subject=record.get("auth_subject", ""),
678
+ )
679
+ sys.stderr.write(json.dumps(record, sort_keys=True) + "\n")
680
+ sys.stderr.flush()
681
+
682
+ def _remote_addr(self) -> str:
683
+ return str(self.client_address[0])
684
+
685
+
686
+ def _payload_field(payload: Any, field_name: str) -> str:
687
+ if isinstance(payload, dict):
688
+ return str(payload.get(field_name, ""))
689
+ return ""
690
+
691
+
692
+ def _metrics_path(path: str) -> str:
693
+ if path == "/runtime/approvals/summary":
694
+ return "/runtime/approvals/summary"
695
+ if path == "/runtime/approvals":
696
+ return "/runtime/approvals"
697
+ if path == "/runtime/policy":
698
+ return "/runtime/policy"
699
+ if path == "/runtime/runs/summary":
700
+ return "/runtime/runs/summary"
701
+ if path.startswith("/runtime/runs/") and "/artifacts/" in path:
702
+ return "/runtime/runs/{run_id}/artifacts/{artifact_id}"
703
+ if path.startswith("/runtime/runs/") and path.endswith("/artifacts"):
704
+ return "/runtime/runs/{run_id}/artifacts"
705
+ if path.startswith("/runtime/runs/") and path.endswith("/cancel"):
706
+ return "/runtime/runs/{run_id}/cancel"
707
+ if path.startswith("/runtime/runs/") and path.endswith("/timeline"):
708
+ return "/runtime/runs/{run_id}/timeline"
709
+ if path.startswith("/runtime/runs/"):
710
+ return "/runtime/runs/{run_id}"
711
+ return path if path in _KNOWN_METRICS_PATHS else _UNKNOWN_METRICS_PATH
712
+
713
+
714
+ def _safe_response_header_value(value: str) -> bool:
715
+ return service_safety.safe_request_id(value)
716
+
717
+
718
+ def _safe_trace_path_response_header_value(value: str) -> bool:
719
+ return bool(value and len(value) <= 1024 and service_safety.safe_header_value(value))
720
+
721
+
722
+ def _retry_after_value(status_code: int, payload: Any) -> str:
723
+ error_code = _payload_error_code(payload)
724
+ if status_code == 429 and error_code == service_errors.RATE_LIMIT_EXCEEDED:
725
+ retry_after_seconds = _payload_field(payload, "retry_after_seconds")
726
+ return retry_after_seconds if retry_after_seconds else "60"
727
+ if status_code == 503 and error_code == service_errors.TOO_MANY_CONCURRENT_RUNS:
728
+ retry_after_seconds = _payload_field(payload, "retry_after_seconds")
729
+ return retry_after_seconds if retry_after_seconds else "1"
730
+ if status_code == 408 and error_code == service_errors.REQUEST_BODY_TIMEOUT:
731
+ retry_after_seconds = _payload_field(payload, "retry_after_seconds")
732
+ return retry_after_seconds if retry_after_seconds else "1"
733
+ return ""
734
+
735
+
736
+ if __name__ == "__main__":
737
+ raise SystemExit(main())