@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,195 @@
1
+ from __future__ import annotations
2
+
3
+ import fcntl
4
+ import os
5
+ import stat
6
+ import tempfile
7
+ from contextlib import contextmanager
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Iterator
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class TextFileState:
15
+ content: str | None
16
+ mode: int
17
+
18
+
19
+ @contextmanager
20
+ def workspace_transaction(workspace_root: Path) -> Iterator[None]:
21
+ flags = os.O_RDONLY
22
+ if hasattr(os, "O_DIRECTORY"):
23
+ flags |= os.O_DIRECTORY
24
+ if hasattr(os, "O_NOFOLLOW"):
25
+ flags |= os.O_NOFOLLOW
26
+ descriptor = os.open(workspace_root, flags)
27
+ try:
28
+ fcntl.flock(descriptor, fcntl.LOCK_EX)
29
+ yield
30
+ finally:
31
+ try:
32
+ fcntl.flock(descriptor, fcntl.LOCK_UN)
33
+ finally:
34
+ os.close(descriptor)
35
+
36
+
37
+ def commit_text_changes(
38
+ workspace_root: Path,
39
+ staged_contents: dict[Path, str | None],
40
+ *,
41
+ original_states: dict[Path, TextFileState] | None = None,
42
+ target_modes: dict[Path, int] | None = None,
43
+ ) -> None:
44
+ snapshots = original_states or capture_text_states(staged_contents)
45
+ resolved_target_modes = target_modes or {}
46
+ created_directories: list[Path] = []
47
+ try:
48
+ for target, content in staged_contents.items():
49
+ _reject_symlink_parts(workspace_root, target)
50
+ if content is None:
51
+ if target.exists():
52
+ target.unlink()
53
+ _fsync_directory(target.parent)
54
+ continue
55
+ _ensure_parent(
56
+ workspace_root,
57
+ target.parent,
58
+ created_directories=created_directories,
59
+ )
60
+ _atomic_write_text(target, content, mode=resolved_target_modes.get(target))
61
+ except Exception as commit_error:
62
+ try:
63
+ _rollback(
64
+ workspace_root,
65
+ snapshots,
66
+ created_directories=created_directories,
67
+ )
68
+ except Exception as rollback_error:
69
+ raise RuntimeError(
70
+ f"patch commit failed: {commit_error}; rollback failed: {rollback_error}"
71
+ ) from commit_error
72
+ raise
73
+
74
+
75
+ def capture_text_states(paths) -> dict[Path, TextFileState]:
76
+ return {target: _snapshot(target) for target in paths}
77
+
78
+
79
+ def validate_workspace_targets(workspace_root: Path, paths) -> None:
80
+ for target in paths:
81
+ target.relative_to(workspace_root)
82
+ _reject_symlink_parts(workspace_root, target)
83
+
84
+
85
+ def _snapshot(target: Path) -> TextFileState:
86
+ if not target.exists():
87
+ return TextFileState(content=None, mode=0o600)
88
+ if not target.is_file():
89
+ raise ValueError("path is not a regular file")
90
+ return TextFileState(
91
+ content=target.read_text(encoding="utf-8"),
92
+ mode=stat.S_IMODE(target.stat().st_mode),
93
+ )
94
+
95
+
96
+ def _rollback(
97
+ workspace_root: Path,
98
+ snapshots: dict[Path, TextFileState],
99
+ *,
100
+ created_directories: list[Path],
101
+ ) -> None:
102
+ errors = []
103
+ for target, snapshot in reversed(tuple(snapshots.items())):
104
+ try:
105
+ _reject_symlink_parts(workspace_root, target)
106
+ if snapshot.content is None:
107
+ if target.exists():
108
+ target.unlink()
109
+ _fsync_directory(target.parent)
110
+ continue
111
+ _ensure_parent(workspace_root, target.parent, created_directories=[])
112
+ _atomic_write_text(target, snapshot.content, mode=snapshot.mode)
113
+ except Exception as exc:
114
+ relative_path = target.relative_to(workspace_root).as_posix()
115
+ errors.append(f"{relative_path}: {exc}")
116
+ for directory in reversed(created_directories):
117
+ try:
118
+ directory.rmdir()
119
+ _fsync_directory(directory.parent)
120
+ except OSError:
121
+ continue
122
+ if errors:
123
+ raise RuntimeError("; ".join(errors))
124
+
125
+
126
+ def _reject_symlink_parts(workspace_root: Path, target: Path) -> None:
127
+ current = workspace_root
128
+ for part in target.relative_to(workspace_root).parts:
129
+ current = current / part
130
+ if current.is_symlink():
131
+ raise ValueError("path must not be a symlink")
132
+ if not current.exists():
133
+ return
134
+
135
+
136
+ def _ensure_parent(
137
+ workspace_root: Path,
138
+ target_parent: Path,
139
+ *,
140
+ created_directories: list[Path],
141
+ ) -> None:
142
+ current = workspace_root
143
+ for part in target_parent.relative_to(workspace_root).parts:
144
+ current = current / part
145
+ if current.is_symlink():
146
+ raise ValueError("path must not be a symlink")
147
+ if current.exists():
148
+ if not current.is_dir():
149
+ raise ValueError("parent path is not a directory")
150
+ continue
151
+ current.mkdir()
152
+ created_directories.append(current)
153
+ _fsync_directory(current.parent)
154
+
155
+
156
+ def _atomic_write_text(
157
+ target: Path,
158
+ content: str,
159
+ *,
160
+ mode: int | None = None,
161
+ ) -> None:
162
+ resolved_mode = mode
163
+ if resolved_mode is None:
164
+ resolved_mode = stat.S_IMODE(target.stat().st_mode) if target.exists() else 0o600
165
+ descriptor, temporary_name = tempfile.mkstemp(
166
+ prefix=f".{target.name}.",
167
+ suffix=".tmp",
168
+ dir=target.parent,
169
+ )
170
+ temporary_path = Path(temporary_name)
171
+ try:
172
+ os.chmod(temporary_path, resolved_mode)
173
+ with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as handle:
174
+ descriptor = -1
175
+ handle.write(content)
176
+ handle.flush()
177
+ os.fsync(handle.fileno())
178
+ temporary_path.replace(target)
179
+ _fsync_directory(target.parent)
180
+ except Exception:
181
+ if descriptor != -1:
182
+ os.close(descriptor)
183
+ temporary_path.unlink(missing_ok=True)
184
+ raise
185
+
186
+
187
+ def _fsync_directory(directory: Path) -> None:
188
+ flags = os.O_RDONLY
189
+ if hasattr(os, "O_DIRECTORY"):
190
+ flags |= os.O_DIRECTORY
191
+ descriptor = os.open(directory, flags)
192
+ try:
193
+ os.fsync(descriptor)
194
+ finally:
195
+ os.close(descriptor)
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, Iterable, Protocol
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class RuntimeHookDecision:
9
+ status: str
10
+ reason: str = ""
11
+
12
+ @classmethod
13
+ def allow(cls) -> "RuntimeHookDecision":
14
+ return cls(status="allowed")
15
+
16
+ @classmethod
17
+ def deny(cls, reason: str) -> "RuntimeHookDecision":
18
+ return cls(status="denied", reason=reason or "runtime hook denied execution")
19
+
20
+
21
+ class RuntimeHook(Protocol):
22
+ def on_run_start(self, context: Dict[str, Any]) -> None:
23
+ ...
24
+
25
+ def before_tool(self, context: Dict[str, Any]) -> RuntimeHookDecision:
26
+ ...
27
+
28
+ def after_tool(self, context: Dict[str, Any]) -> None:
29
+ ...
30
+
31
+ def on_run_end(self, context: Dict[str, Any]) -> None:
32
+ ...
33
+
34
+
35
+ class RuntimeHookChain:
36
+ def __init__(self, hooks: Iterable[Any] = ()) -> None:
37
+ self._hooks = tuple(hooks)
38
+
39
+ def __bool__(self) -> bool:
40
+ return bool(self._hooks)
41
+
42
+ def on_run_start(self, context: Dict[str, Any]) -> None:
43
+ for hook in self._hooks:
44
+ handler = getattr(hook, "on_run_start", None)
45
+ if callable(handler):
46
+ handler(dict(context))
47
+
48
+ def before_tool(self, context: Dict[str, Any]) -> RuntimeHookDecision:
49
+ for hook in self._hooks:
50
+ handler = getattr(hook, "before_tool", None)
51
+ if not callable(handler):
52
+ continue
53
+ decision = handler(dict(context))
54
+ if decision is None:
55
+ continue
56
+ if not isinstance(decision, RuntimeHookDecision):
57
+ raise ValueError("runtime hook before_tool must return RuntimeHookDecision")
58
+ if decision.status == "denied":
59
+ return decision
60
+ if decision.status != "allowed":
61
+ raise ValueError("runtime hook decision status must be allowed or denied")
62
+ return RuntimeHookDecision.allow()
63
+
64
+ def after_tool(self, context: Dict[str, Any]) -> None:
65
+ for hook in self._hooks:
66
+ handler = getattr(hook, "after_tool", None)
67
+ if callable(handler):
68
+ handler(dict(context))
69
+
70
+ def on_run_end(self, context: Dict[str, Any]) -> None:
71
+ for hook in self._hooks:
72
+ handler = getattr(hook, "on_run_end", None)
73
+ if callable(handler):
74
+ handler(dict(context))
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Dict, Tuple
5
+
6
+ MAX_RUNTIME_TAGS = 16
7
+ MAX_RUNTIME_TAG_CHARS = 64
8
+ MAX_RUNTIME_METADATA_ENTRIES = 16
9
+ MAX_RUNTIME_METADATA_KEY_CHARS = 64
10
+ MAX_RUNTIME_METADATA_VALUE_CHARS = 256
11
+
12
+ _SAFE_KEY_CHARS = frozenset(
13
+ "abcdefghijklmnopqrstuvwxyz"
14
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
15
+ "0123456789"
16
+ "._:-"
17
+ )
18
+ _SECRET_KEY_PARTS = (
19
+ "api_key",
20
+ "apikey",
21
+ "authorization",
22
+ "bearer",
23
+ "password",
24
+ "secret",
25
+ "token",
26
+ )
27
+ _API_KEY_VALUE_PATTERN = re.compile(r"\bsk-[A-Za-z0-9:_-]{8,}\b")
28
+ _BEARER_VALUE_PATTERN = re.compile(
29
+ r"\b(Authorization:\s*Bearer\s+|Bearer\s+)[A-Za-z0-9._~+/:-]{8,}",
30
+ re.IGNORECASE,
31
+ )
32
+ _URL_CREDENTIAL_VALUE_PATTERN = re.compile(r"\bhttps?://[^/\s:@]+:[^/\s@]+@")
33
+
34
+
35
+ def validate_runtime_metadata(
36
+ metadata: Any,
37
+ ) -> Tuple[Dict[str, str], str]:
38
+ if metadata is None:
39
+ return {}, ""
40
+ if not isinstance(metadata, dict):
41
+ return {}, "metadata must be a JSON object"
42
+ if len(metadata) > MAX_RUNTIME_METADATA_ENTRIES:
43
+ return {}, (
44
+ f"metadata must contain at most {MAX_RUNTIME_METADATA_ENTRIES} entries"
45
+ )
46
+ normalized = {}
47
+ for raw_key, raw_value in metadata.items():
48
+ if not isinstance(raw_key, str):
49
+ return {}, "metadata keys must be strings"
50
+ key = raw_key.strip()
51
+ if not key:
52
+ return {}, "metadata keys must be non-empty strings"
53
+ if len(key) > MAX_RUNTIME_METADATA_KEY_CHARS:
54
+ return {}, (
55
+ f"metadata keys must contain at most "
56
+ f"{MAX_RUNTIME_METADATA_KEY_CHARS} characters"
57
+ )
58
+ if any(character not in _SAFE_KEY_CHARS for character in key):
59
+ return {}, "metadata keys may only contain letters, numbers, . _ : and -"
60
+ lowered_key = key.lower()
61
+ if any(secret_part in lowered_key for secret_part in _SECRET_KEY_PARTS):
62
+ return {}, "metadata must not contain secret-like keys"
63
+ if not isinstance(raw_value, str):
64
+ return {}, "metadata values must be strings"
65
+ value = raw_value.strip()
66
+ if len(value) > MAX_RUNTIME_METADATA_VALUE_CHARS:
67
+ return {}, (
68
+ f"metadata values must contain at most "
69
+ f"{MAX_RUNTIME_METADATA_VALUE_CHARS} characters"
70
+ )
71
+ if not _safe_runtime_label_value(value):
72
+ return {}, "metadata values must not contain control characters"
73
+ if _secret_like_runtime_label_value(value):
74
+ return {}, "metadata values must not contain secret-like values"
75
+ normalized[key] = value
76
+ return {key: normalized[key] for key in sorted(normalized)}, ""
77
+
78
+
79
+ def validate_runtime_tags(tags: Any) -> Tuple[list[str], str]:
80
+ if tags is None:
81
+ return [], ""
82
+ if not isinstance(tags, list):
83
+ return [], "tags must be an array of strings"
84
+ if len(tags) > MAX_RUNTIME_TAGS:
85
+ return [], f"tags must contain at most {MAX_RUNTIME_TAGS} entries"
86
+ normalized = []
87
+ seen = set()
88
+ for raw_tag in tags:
89
+ if not isinstance(raw_tag, str):
90
+ return [], "tags must be an array of strings"
91
+ tag = raw_tag.strip()
92
+ if not tag:
93
+ return [], "tags must contain non-empty strings"
94
+ if len(tag) > MAX_RUNTIME_TAG_CHARS:
95
+ return [], (
96
+ f"tags must contain strings of at most "
97
+ f"{MAX_RUNTIME_TAG_CHARS} characters"
98
+ )
99
+ if not _safe_runtime_label_value(tag):
100
+ return [], "tags must not contain control characters"
101
+ if tag not in seen:
102
+ seen.add(tag)
103
+ normalized.append(tag)
104
+ return sorted(normalized), ""
105
+
106
+
107
+ def _safe_runtime_label_value(value: str) -> bool:
108
+ return all(ord(character) >= 32 for character in value)
109
+
110
+
111
+ def _secret_like_runtime_label_value(value: str) -> bool:
112
+ return bool(
113
+ _API_KEY_VALUE_PATTERN.search(value)
114
+ or _BEARER_VALUE_PATTERN.search(value)
115
+ or _URL_CREDENTIAL_VALUE_PATTERN.search(value)
116
+ )