@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,130 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import urllib.parse
5
+ from typing import Any
6
+
7
+ REDACTED_VALUE = "[REDACTED]"
8
+
9
+ _URL_PATTERN = re.compile(r"https?://[^\s\"'<>]+")
10
+ _URL_SECRET_KEY_PARTS = (
11
+ "api_key",
12
+ "apikey",
13
+ "authorization",
14
+ "bearer",
15
+ "password",
16
+ "secret",
17
+ "token",
18
+ )
19
+ _API_KEY_PATTERN = re.compile(r"\bsk-[A-Za-z0-9:_-]{6,}\b")
20
+ _BEARER_TOKEN_PATTERN = re.compile(
21
+ r"(?i)\b(bearer[\s-]+)([A-Za-z0-9._:/+=-]{6,})"
22
+ )
23
+ _GITHUB_TOKEN_PATTERN = re.compile(
24
+ r"\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b"
25
+ )
26
+ _KNOWN_SECRET_ASSIGNMENT_PATTERN = re.compile(
27
+ r"(?i)(\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|"
28
+ r"github[_-]?token|password|secret[_-]?key)[\"']?\s*[:=]\s*[\"']?)"
29
+ r"([^\s\"',;}{&#\[\]]{4,})([\"']?)"
30
+ )
31
+ _ENV_SECRET_ASSIGNMENT_PATTERN = re.compile(
32
+ r"(\b[A-Z][A-Z0-9_]*(?:API_KEY|ACCESS_TOKEN|AUTH_TOKEN|CLIENT_SECRET|"
33
+ r"SECRET_ACCESS_KEY|SECRET_KEY|PASSWORD|PRIVATE_KEY|TOKEN)[\"']?"
34
+ r"\s*[:=]\s*[\"']?)([^\s\"',;}{&#\[\]]{4,})([\"']?)"
35
+ )
36
+ _PRIVATE_KEY_PATTERN = re.compile(
37
+ r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----.*?"
38
+ r"-----END [A-Z0-9 ]*PRIVATE KEY-----",
39
+ re.DOTALL,
40
+ )
41
+ _SECRET_VALUE_PATTERNS = (
42
+ _API_KEY_PATTERN,
43
+ _BEARER_TOKEN_PATTERN,
44
+ _GITHUB_TOKEN_PATTERN,
45
+ )
46
+
47
+
48
+ def redact_runtime_payload(value: Any) -> Any:
49
+ if isinstance(value, str):
50
+ return redact_runtime_text(value)
51
+ if isinstance(value, dict):
52
+ return {key: redact_runtime_payload(item) for key, item in value.items()}
53
+ if isinstance(value, list):
54
+ return [redact_runtime_payload(item) for item in value]
55
+ return value
56
+
57
+
58
+ def redact_runtime_text(value: str) -> str:
59
+ redacted = _URL_PATTERN.sub(lambda match: _redact_url(match.group(0)), value)
60
+ redacted = _PRIVATE_KEY_PATTERN.sub(REDACTED_VALUE, redacted)
61
+ redacted = _KNOWN_SECRET_ASSIGNMENT_PATTERN.sub(_redact_assignment, redacted)
62
+ redacted = _ENV_SECRET_ASSIGNMENT_PATTERN.sub(_redact_assignment, redacted)
63
+ redacted = _API_KEY_PATTERN.sub(REDACTED_VALUE, redacted)
64
+ redacted = _GITHUB_TOKEN_PATTERN.sub(REDACTED_VALUE, redacted)
65
+ return _BEARER_TOKEN_PATTERN.sub(
66
+ lambda match: match.group(1) + REDACTED_VALUE,
67
+ redacted,
68
+ )
69
+
70
+
71
+ def _redact_assignment(match: re.Match[str]) -> str:
72
+ return f"{match.group(1)}{REDACTED_VALUE}{match.group(3)}"
73
+
74
+
75
+ def _redact_url(value: str) -> str:
76
+ parsed = urllib.parse.urlsplit(value)
77
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
78
+ return value
79
+ return urllib.parse.urlunsplit(
80
+ (
81
+ parsed.scheme,
82
+ _redact_url_netloc(parsed.netloc),
83
+ parsed.path,
84
+ _redact_url_component(parsed.query),
85
+ _redact_url_component(parsed.fragment),
86
+ )
87
+ )
88
+
89
+
90
+ def _redact_url_netloc(netloc: str) -> str:
91
+ if "@" not in netloc:
92
+ return netloc
93
+ return f"{REDACTED_VALUE}@{netloc.rsplit('@', 1)[1]}"
94
+
95
+
96
+ def _redact_url_component(value: str) -> str:
97
+ if not value:
98
+ return value
99
+ if "=" not in value and _contains_secret_like_value(urllib.parse.unquote_plus(value)):
100
+ return REDACTED_VALUE
101
+ parts = []
102
+ for item in value.split("&"):
103
+ if "=" not in item:
104
+ parts.append(_redact_bare_url_component_item(item))
105
+ continue
106
+ key, item_value = item.split("=", 1)
107
+ decoded_key = urllib.parse.unquote_plus(key).lower()
108
+ decoded_value = urllib.parse.unquote_plus(item_value)
109
+ if _is_secret_like_key(decoded_key) or _contains_secret_like_value(decoded_value):
110
+ parts.append(f"{key}={REDACTED_VALUE}")
111
+ else:
112
+ parts.append(item)
113
+ return "&".join(parts)
114
+
115
+
116
+ def _redact_bare_url_component_item(value: str) -> str:
117
+ decoded_value = urllib.parse.unquote_plus(value)
118
+ if _is_secret_like_key(decoded_value.lower()) or _contains_secret_like_value(
119
+ decoded_value
120
+ ):
121
+ return REDACTED_VALUE
122
+ return value
123
+
124
+
125
+ def _is_secret_like_key(value: str) -> bool:
126
+ return any(part in value for part in _URL_SECRET_KEY_PARTS)
127
+
128
+
129
+ def _contains_secret_like_value(value: str) -> bool:
130
+ return any(pattern.search(value) for pattern in _SECRET_VALUE_PATTERNS)
@@ -0,0 +1,331 @@
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Dict, List, Sequence, Union
10
+
11
+ SandboxArgv = Union[str, List[str]]
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ShellSandboxExecution:
16
+ argv: SandboxArgv
17
+ shell: bool
18
+ env: Dict[str, str]
19
+ metadata: Dict[str, str]
20
+ profile: str = ""
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ShellSandboxResult:
25
+ completed: subprocess.CompletedProcess
26
+ metadata: Dict[str, str]
27
+
28
+
29
+ def prepare_shell_sandbox(
30
+ command: str,
31
+ *,
32
+ workspace_root: Path,
33
+ cwd: Path,
34
+ env: Dict[str, str],
35
+ ) -> ShellSandboxExecution:
36
+ system = platform.system().lower()
37
+ if system == "darwin":
38
+ return _prepare_macos_seatbelt(command, workspace_root=workspace_root, env=env)
39
+ if system == "linux":
40
+ return _prepare_linux_bwrap(command, workspace_root=workspace_root, cwd=cwd, env=env)
41
+ if system == "windows":
42
+ return _soft_execution(command, env=env, backend="windows-soft")
43
+ return _soft_execution(command, env=env, backend="soft")
44
+
45
+
46
+ def run_shell_sandboxed(
47
+ command: str,
48
+ *,
49
+ workspace_root: Path,
50
+ cwd: Path,
51
+ env: Dict[str, str],
52
+ timeout_seconds: float,
53
+ ) -> ShellSandboxResult:
54
+ execution = prepare_shell_sandbox(
55
+ command,
56
+ workspace_root=workspace_root,
57
+ cwd=cwd,
58
+ env=env,
59
+ )
60
+ try:
61
+ completed = _run_prepared_execution(execution, cwd=cwd, timeout_seconds=timeout_seconds)
62
+ if _native_sandbox_startup_failed(execution, completed):
63
+ fallback = _soft_execution(
64
+ command,
65
+ env=env,
66
+ backend="soft",
67
+ fallback_reason="native sandbox startup failed",
68
+ )
69
+ completed = _run_prepared_execution(
70
+ fallback,
71
+ cwd=cwd,
72
+ timeout_seconds=timeout_seconds,
73
+ )
74
+ return ShellSandboxResult(completed=completed, metadata=fallback.metadata)
75
+ return ShellSandboxResult(completed=completed, metadata=execution.metadata)
76
+ except (FileNotFoundError, PermissionError, OSError):
77
+ fallback = _soft_execution(
78
+ command,
79
+ env=env,
80
+ backend="soft",
81
+ fallback_reason="native sandbox exec failed",
82
+ )
83
+ completed = _run_prepared_execution(fallback, cwd=cwd, timeout_seconds=timeout_seconds)
84
+ return ShellSandboxResult(completed=completed, metadata=fallback.metadata)
85
+
86
+
87
+ def _run_prepared_execution(
88
+ execution: ShellSandboxExecution,
89
+ *,
90
+ cwd: Path,
91
+ timeout_seconds: float,
92
+ ) -> subprocess.CompletedProcess:
93
+ return subprocess.run(
94
+ execution.argv,
95
+ shell=execution.shell,
96
+ cwd=str(cwd),
97
+ env=execution.env,
98
+ stdin=subprocess.DEVNULL,
99
+ capture_output=True,
100
+ text=False,
101
+ timeout=timeout_seconds,
102
+ start_new_session=True,
103
+ )
104
+
105
+
106
+ def _native_sandbox_startup_failed(
107
+ execution: ShellSandboxExecution,
108
+ completed: subprocess.CompletedProcess,
109
+ ) -> bool:
110
+ if execution.metadata.get("enforced") != "true" or completed.returncode == 0:
111
+ return False
112
+ stderr = (completed.stderr or b"").decode("utf-8", errors="replace").lower()
113
+ backend = execution.metadata.get("backend", "")
114
+ if backend == "linux-bwrap":
115
+ startup_markers = (
116
+ "creating new namespace failed",
117
+ "bubblewrap:",
118
+ "bwrap:",
119
+ "failed to make",
120
+ )
121
+ return any(marker in stderr for marker in startup_markers)
122
+ if backend == "macos-seatbelt":
123
+ return "sandbox-exec: execvp" in stderr
124
+ return False
125
+
126
+
127
+ def _prepare_macos_seatbelt(
128
+ command: str,
129
+ *,
130
+ workspace_root: Path,
131
+ env: Dict[str, str],
132
+ ) -> ShellSandboxExecution:
133
+ sandbox_exec = shutil.which("sandbox-exec")
134
+ if not sandbox_exec:
135
+ return _soft_execution(
136
+ command,
137
+ env=env,
138
+ backend="soft",
139
+ fallback_reason="native sandbox unavailable",
140
+ )
141
+ profile = _macos_seatbelt_profile(workspace_root)
142
+ return ShellSandboxExecution(
143
+ argv=[
144
+ sandbox_exec,
145
+ "-p",
146
+ profile,
147
+ "/bin/bash",
148
+ "--noprofile",
149
+ "--norc",
150
+ "-c",
151
+ command,
152
+ ],
153
+ shell=False,
154
+ env=env,
155
+ metadata=_sandbox_metadata(backend="macos-seatbelt", enforced=True),
156
+ profile=profile,
157
+ )
158
+
159
+
160
+ def _prepare_linux_bwrap(
161
+ command: str,
162
+ *,
163
+ workspace_root: Path,
164
+ cwd: Path,
165
+ env: Dict[str, str],
166
+ ) -> ShellSandboxExecution:
167
+ bwrap = shutil.which("bwrap")
168
+ if not bwrap:
169
+ return _soft_execution(
170
+ command,
171
+ env=env,
172
+ backend="soft",
173
+ fallback_reason="native sandbox unavailable",
174
+ )
175
+ argv: List[str] = [
176
+ bwrap,
177
+ "--die-with-parent",
178
+ "--new-session",
179
+ "--unshare-all",
180
+ "--unshare-net",
181
+ "--proc",
182
+ "/proc",
183
+ "--dev",
184
+ "/dev",
185
+ "--tmpfs",
186
+ "/tmp",
187
+ ]
188
+ for source in _linux_readonly_binds():
189
+ argv.extend(["--ro-bind", source, source])
190
+ argv.extend(["--bind", str(workspace_root), str(workspace_root)])
191
+ argv.extend(["--chdir", str(cwd)])
192
+ argv.extend(["/bin/sh", "-lc", command])
193
+ return ShellSandboxExecution(
194
+ argv=argv,
195
+ shell=False,
196
+ env=env,
197
+ metadata=_sandbox_metadata(backend="linux-bwrap", enforced=True),
198
+ )
199
+
200
+
201
+ def _linux_readonly_binds() -> Sequence[str]:
202
+ candidates = (
203
+ "/bin",
204
+ "/usr",
205
+ "/lib",
206
+ "/lib64",
207
+ "/etc/alternatives",
208
+ "/etc/ssl",
209
+ "/etc/ca-certificates",
210
+ )
211
+ return tuple(path for path in candidates if Path(path).exists())
212
+
213
+
214
+ def _soft_execution(
215
+ command: str,
216
+ *,
217
+ env: Dict[str, str],
218
+ backend: str,
219
+ fallback_reason: str = "",
220
+ ) -> ShellSandboxExecution:
221
+ return ShellSandboxExecution(
222
+ argv=command,
223
+ shell=True,
224
+ env=env,
225
+ metadata=_sandbox_metadata(
226
+ backend=backend,
227
+ enforced=False,
228
+ fallback_reason=fallback_reason,
229
+ ),
230
+ )
231
+
232
+
233
+ def _sandbox_metadata(
234
+ *,
235
+ backend: str,
236
+ enforced: bool,
237
+ fallback_reason: str = "",
238
+ ) -> Dict[str, str]:
239
+ metadata = {
240
+ "enabled": "true",
241
+ "backend": backend,
242
+ "enforced": "true" if enforced else "false",
243
+ "filesystem": "workspace",
244
+ "network": "disabled",
245
+ "env_policy": "minimal",
246
+ }
247
+ if fallback_reason:
248
+ metadata["fallback_reason"] = fallback_reason
249
+ return metadata
250
+
251
+
252
+ def _macos_seatbelt_profile(workspace_root: Path) -> str:
253
+ workspace_read_rules = " ".join(
254
+ f"(subpath {_seatbelt_string(str(path))})"
255
+ for path in _macos_path_aliases(workspace_root)
256
+ )
257
+ read_exception_filters = _macos_read_exception_filters(workspace_root)
258
+ runtime_read_rules = " ".join(
259
+ f"(subpath {_seatbelt_string(str(path))})"
260
+ for runtime_path in _macos_runtime_read_paths()
261
+ for path in _macos_path_aliases(runtime_path)
262
+ )
263
+ workspace_write_rules = " ".join(
264
+ f"(subpath {_seatbelt_string(str(path))})"
265
+ for path in _macos_path_aliases(workspace_root)
266
+ )
267
+ return "\n".join(
268
+ [
269
+ "(version 1)",
270
+ "(allow default)",
271
+ "(deny network*)",
272
+ "(deny file-read*",
273
+ " (require-all",
274
+ ' (subpath "/Users")',
275
+ " (require-not",
276
+ " (require-any",
277
+ f" {read_exception_filters}))))",
278
+ f"(allow file-read* {workspace_read_rules})",
279
+ f"(allow file-read* {runtime_read_rules})" if runtime_read_rules else "",
280
+ "(deny file-write*)",
281
+ f"(allow file-write* {workspace_write_rules})",
282
+ ]
283
+ )
284
+
285
+
286
+ def _seatbelt_string(value: str) -> str:
287
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
288
+ return f'"{escaped}"'
289
+
290
+
291
+ def _macos_runtime_read_paths() -> Sequence[Path]:
292
+ candidates = {
293
+ Path(sys.executable).absolute().parent,
294
+ Path(sys.executable).resolve().parent,
295
+ Path(sys.prefix).resolve(),
296
+ }
297
+ return tuple(path for path in candidates if str(path).startswith("/Users/"))
298
+
299
+
300
+ def _macos_read_exception_filters(workspace_root: Path) -> str:
301
+ subpaths = set(_macos_path_aliases(workspace_root))
302
+ for runtime_path in _macos_runtime_read_paths():
303
+ subpaths.update(_macos_path_aliases(runtime_path))
304
+ literals = set(subpaths)
305
+ for path in tuple(subpaths):
306
+ literals.update(_macos_user_ancestor_literals(path))
307
+ filters = [
308
+ f"(subpath {_seatbelt_string(str(path))})"
309
+ for path in sorted(subpaths, key=str)
310
+ ]
311
+ filters.extend(
312
+ f"(literal {_seatbelt_string(str(path))})"
313
+ for path in sorted(literals, key=str)
314
+ )
315
+ return " ".join(filters)
316
+
317
+
318
+ def _macos_user_ancestor_literals(path: Path) -> Sequence[Path]:
319
+ candidates = [path]
320
+ candidates.extend(path.parents)
321
+ return tuple(candidate for candidate in candidates if str(candidate).startswith("/Users"))
322
+
323
+
324
+ def _macos_path_aliases(path: Path) -> Sequence[Path]:
325
+ path_text = str(path)
326
+ aliases = {path}
327
+ if path_text.startswith(("/var/", "/tmp/", "/etc/")):
328
+ aliases.add(Path(f"/private{path_text}"))
329
+ if path_text.startswith("/private/"):
330
+ aliases.add(Path(path_text.removeprefix("/private")))
331
+ return tuple(aliases)
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Any, Dict
7
+
8
+ _SAFE_SKILL_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$")
9
+
10
+
11
+ class RuntimeSkillRegistry:
12
+ def __init__(self, skills_dir: str | Path) -> None:
13
+ self.skills_dir = Path(skills_dir)
14
+
15
+ def list_skills(self) -> list[Dict[str, Any]]:
16
+ if not self.skills_dir.exists():
17
+ return []
18
+ skills = []
19
+ for path in sorted(self.skills_dir.glob("*.json")):
20
+ if path.is_symlink() or not path.is_file():
21
+ continue
22
+ skill = self._read_skill_file(path)
23
+ skills.append(
24
+ {
25
+ "name": skill["name"],
26
+ "description": skill["description"],
27
+ "tags": skill.get("tags", []),
28
+ }
29
+ )
30
+ return skills
31
+
32
+ def get_skill(self, name: str) -> Dict[str, Any]:
33
+ normalized_name = self._validate_skill_name(name)
34
+ path = self.skills_dir / f"{normalized_name}.json"
35
+ if path.is_symlink():
36
+ raise ValueError("skill file must not be a symlink")
37
+ if not path.exists():
38
+ raise ValueError(f"skill does not exist: {normalized_name}")
39
+ return self._read_skill_file(path)
40
+
41
+ def _read_skill_file(self, path: Path) -> Dict[str, Any]:
42
+ payload = json.loads(path.read_text(encoding="utf-8"))
43
+ if not isinstance(payload, dict):
44
+ raise ValueError("skill payload must be an object")
45
+ name = self._validate_skill_name(str(payload.get("name", "")))
46
+ description = str(payload.get("description", "")).strip()
47
+ instructions = str(payload.get("instructions", "")).strip()
48
+ if not description:
49
+ raise ValueError(f"skill description is required: {name}")
50
+ if not instructions:
51
+ raise ValueError(f"skill instructions are required: {name}")
52
+ tags = payload.get("tags", [])
53
+ if not isinstance(tags, list):
54
+ raise ValueError(f"skill tags must be an array: {name}")
55
+ return {
56
+ "name": name,
57
+ "description": description,
58
+ "instructions": instructions,
59
+ "tags": [str(tag) for tag in tags],
60
+ }
61
+
62
+ def _validate_skill_name(self, name: str) -> str:
63
+ normalized = str(name).strip()
64
+ if not _SAFE_SKILL_NAME.match(normalized):
65
+ raise ValueError("skill name must be a safe identifier")
66
+ return normalized
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from threading import Lock
4
+ from typing import Callable
5
+
6
+
7
+ class RuntimeSteeringBuffer:
8
+ """Thread-safe latest-wins instruction slot for an active runtime run."""
9
+
10
+ def __init__(self) -> None:
11
+ self._lock = Lock()
12
+ self._instruction = ""
13
+ self._revision = 0
14
+ self._closed = False
15
+
16
+ def submit(
17
+ self,
18
+ instruction: str,
19
+ *,
20
+ accepted: Callable[[dict[str, str]], None] | None = None,
21
+ ) -> dict[str, str]:
22
+ normalized = instruction.strip()
23
+ if not normalized:
24
+ raise ValueError("steering instruction is required")
25
+ with self._lock:
26
+ if self._closed:
27
+ raise RuntimeError("active run is no longer accepting steering")
28
+ replaced = bool(self._instruction)
29
+ self._instruction = normalized
30
+ self._revision += 1
31
+ snapshot = {
32
+ "revision": str(self._revision),
33
+ "replaced": str(replaced).lower(),
34
+ }
35
+ if accepted is not None:
36
+ accepted(snapshot)
37
+ return snapshot
38
+
39
+ def consume(self) -> tuple[str, str]:
40
+ with self._lock:
41
+ instruction = self._instruction
42
+ if not instruction:
43
+ return "", str(self._revision)
44
+ self._instruction = ""
45
+ return instruction, str(self._revision)
46
+
47
+ def pending(self) -> bool:
48
+ with self._lock:
49
+ return bool(self._instruction)
50
+
51
+ def close(self) -> tuple[str, str]:
52
+ with self._lock:
53
+ self._closed = True
54
+ instruction = self._instruction
55
+ self._instruction = ""
56
+ return instruction, str(self._revision)