@oneciel-ai/ciel-runtime 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ciel-runtime-menu.py +204 -22
- package/ciel-runtime-tool-guard.py +29 -24
- package/ciel_runtime.py +6023 -2515
- package/ciel_runtime_support/agent_router.py +97 -0
- package/ciel_runtime_support/agy_cli.py +248 -0
- package/ciel_runtime_support/claude_router.py +479 -0
- package/ciel_runtime_support/codex_app_server.py +430 -0
- package/ciel_runtime_support/codex_cli.py +303 -0
- package/ciel_runtime_support/codex_router.py +80 -0
- package/docs/AGY-CLI-Research.md +62 -0
- package/docs/Architecture.md +23 -0
- package/docs/CLI-Reference.md +1 -1
- package/docs/Home.md +1 -1
- package/docs/Installation.md +9 -5
- package/docs/MCP-Channels.md +2 -4
- package/docs/Module-Map.md +29 -1
- package/docs/Providers.md +3 -2
- package/docs/Router.md +1 -1
- package/docs/Test-Suite.md +1 -0
- package/package.json +2 -3
- package/npm-bin/postinstall.js +0 -46
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""HTTP runtime router contracts for ciel-runtime.
|
|
2
|
+
|
|
3
|
+
Runtime routers own request paths and feature parity for a local coding
|
|
4
|
+
runtime. They stay dependency-light and receive concrete transport callbacks
|
|
5
|
+
from the portable entrypoint.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Protocol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class RouterCapability:
|
|
16
|
+
"""One externally visible router behavior."""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
description: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
COMMON_RUNTIME_ROUTER_CAPABILITIES: tuple[str, ...] = (
|
|
23
|
+
"auth_forwarding",
|
|
24
|
+
"sse_stream_proxy",
|
|
25
|
+
"channel_context_injection",
|
|
26
|
+
"pending_delivery_ack",
|
|
27
|
+
"request_observability",
|
|
28
|
+
"upstream_error_mapping",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RuntimeRouter(Protocol):
|
|
33
|
+
"""Dispatch contract for one runtime's HTTP routes."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
runtime: str
|
|
37
|
+
protocol: str
|
|
38
|
+
request_paths: tuple[str, ...]
|
|
39
|
+
capabilities: tuple[RouterCapability, ...]
|
|
40
|
+
|
|
41
|
+
def can_handle_get(self, path: str, provider: str, pcfg: dict[str, Any]) -> bool:
|
|
42
|
+
"""Return whether this router owns a GET path."""
|
|
43
|
+
|
|
44
|
+
def handle_get(self, handler: Any, path: str, provider: str, pcfg: dict[str, Any]) -> bool:
|
|
45
|
+
"""Handle a GET request. Return True when consumed."""
|
|
46
|
+
|
|
47
|
+
def can_handle_post(self, path: str, provider: str, pcfg: dict[str, Any]) -> bool:
|
|
48
|
+
"""Return whether this router owns a POST path."""
|
|
49
|
+
|
|
50
|
+
def handle_post(
|
|
51
|
+
self,
|
|
52
|
+
handler: Any,
|
|
53
|
+
cfg: dict[str, Any],
|
|
54
|
+
provider: str,
|
|
55
|
+
pcfg: dict[str, Any],
|
|
56
|
+
path: str,
|
|
57
|
+
body: dict[str, Any],
|
|
58
|
+
) -> bool:
|
|
59
|
+
"""Handle a POST request. Return True when consumed."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def capability_names(router: RuntimeRouter) -> set[str]:
|
|
63
|
+
return {capability.name for capability in router.capabilities}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def router_capability_matrix(routers: tuple[RuntimeRouter, ...]) -> dict[str, dict[str, Any]]:
|
|
67
|
+
return {
|
|
68
|
+
router.name: {
|
|
69
|
+
"runtime": router.runtime,
|
|
70
|
+
"protocol": router.protocol,
|
|
71
|
+
"request_paths": list(router.request_paths),
|
|
72
|
+
"capabilities": sorted(capability_names(router)),
|
|
73
|
+
}
|
|
74
|
+
for router in routers
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def missing_common_capabilities(
|
|
79
|
+
routers: tuple[RuntimeRouter, ...],
|
|
80
|
+
required: tuple[str, ...] = COMMON_RUNTIME_ROUTER_CAPABILITIES,
|
|
81
|
+
) -> dict[str, list[str]]:
|
|
82
|
+
required_set = set(required)
|
|
83
|
+
return {
|
|
84
|
+
router.name: sorted(required_set - capability_names(router))
|
|
85
|
+
for router in routers
|
|
86
|
+
if required_set - capability_names(router)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
__all__ = [
|
|
91
|
+
"COMMON_RUNTIME_ROUTER_CAPABILITIES",
|
|
92
|
+
"RouterCapability",
|
|
93
|
+
"RuntimeRouter",
|
|
94
|
+
"capability_names",
|
|
95
|
+
"missing_common_capabilities",
|
|
96
|
+
"router_capability_matrix",
|
|
97
|
+
]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
AGY_COMMAND_NAMES = {
|
|
5
|
+
"changelog",
|
|
6
|
+
"help",
|
|
7
|
+
"install",
|
|
8
|
+
"models",
|
|
9
|
+
"plugin",
|
|
10
|
+
"plugins",
|
|
11
|
+
"update",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
AGY_OPTIONS_WITH_VALUE = {
|
|
16
|
+
"--add-dir",
|
|
17
|
+
"--conversation",
|
|
18
|
+
"--log-file",
|
|
19
|
+
"--model",
|
|
20
|
+
"--new-project",
|
|
21
|
+
"--print-timeout",
|
|
22
|
+
"--project",
|
|
23
|
+
"--sandbox",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
AGY_CLAUDE_ONLY_VALUE_FLAGS = {
|
|
28
|
+
"--allowedTools",
|
|
29
|
+
"--append-system-prompt",
|
|
30
|
+
"--disallowedTools",
|
|
31
|
+
"--fallback-model",
|
|
32
|
+
"--input-format",
|
|
33
|
+
"--mcp-config",
|
|
34
|
+
"--output-format",
|
|
35
|
+
"--permission-prompt-tool",
|
|
36
|
+
"--settings",
|
|
37
|
+
"--system-prompt",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def agy_passthrough_first_non_option_arg(passthrough: list[str]) -> str:
|
|
42
|
+
i = 0
|
|
43
|
+
while i < len(passthrough):
|
|
44
|
+
arg = str(passthrough[i])
|
|
45
|
+
if arg == "--":
|
|
46
|
+
return str(passthrough[i + 1]) if i + 1 < len(passthrough) else ""
|
|
47
|
+
if arg.startswith("--") and "=" in arg:
|
|
48
|
+
i += 1
|
|
49
|
+
continue
|
|
50
|
+
if arg in AGY_OPTIONS_WITH_VALUE:
|
|
51
|
+
i += 2 if i + 1 < len(passthrough) else 1
|
|
52
|
+
continue
|
|
53
|
+
if arg.startswith("-") and arg != "-":
|
|
54
|
+
i += 1
|
|
55
|
+
continue
|
|
56
|
+
return arg
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def agy_passthrough_has_command(passthrough: list[str]) -> bool:
|
|
61
|
+
return agy_passthrough_first_non_option_arg(passthrough) in AGY_COMMAND_NAMES
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _agy_consume_optional_value(passthrough: list[str], index: int) -> tuple[str, int]:
|
|
65
|
+
if index + 1 < len(passthrough):
|
|
66
|
+
value = str(passthrough[index + 1])
|
|
67
|
+
if value != "--" and not value.startswith("-"):
|
|
68
|
+
return value, index + 2
|
|
69
|
+
return "", index + 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_channel_spec_tagged(spec: str) -> bool:
|
|
73
|
+
return spec.startswith("plugin:") or spec.startswith("server:")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _agy_drop_passthrough_channel_args(passthrough: list[str], index: int) -> int:
|
|
77
|
+
arg = str(passthrough[index])
|
|
78
|
+
if arg.startswith("--channels=") or arg.startswith("--dangerously-load-development-channels="):
|
|
79
|
+
return index + 1
|
|
80
|
+
i = index + 1
|
|
81
|
+
while i < len(passthrough) and _is_channel_spec_tagged(str(passthrough[i])):
|
|
82
|
+
i += 1
|
|
83
|
+
return i
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _agy_drop_greedy_passthrough_values(passthrough: list[str], index: int) -> int:
|
|
87
|
+
i = index + 1
|
|
88
|
+
while i < len(passthrough) and not str(passthrough[i]).startswith("-"):
|
|
89
|
+
i += 1
|
|
90
|
+
return i
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def agy_passthrough_args_for_launch(passthrough: list[str]) -> tuple[list[str], list[str]]:
|
|
94
|
+
"""Translate shared Claude/Codex-oriented flags before launching AGY.
|
|
95
|
+
|
|
96
|
+
AGY exposes a smaller native CLI surface than Claude Code and Codex. Keep
|
|
97
|
+
intent-preserving mappings and drop flags that AGY does not parse.
|
|
98
|
+
"""
|
|
99
|
+
out: list[str] = []
|
|
100
|
+
notes: list[str] = []
|
|
101
|
+
existing_agy_command = agy_passthrough_has_command(passthrough)
|
|
102
|
+
mapped_permission_bypass = False
|
|
103
|
+
i = 0
|
|
104
|
+
while i < len(passthrough):
|
|
105
|
+
arg = str(passthrough[i])
|
|
106
|
+
|
|
107
|
+
if arg == "resume" and i == 0 and not existing_agy_command:
|
|
108
|
+
session_id = str(passthrough[i + 1]) if i + 1 < len(passthrough) else ""
|
|
109
|
+
if session_id and not session_id.startswith("-"):
|
|
110
|
+
out.extend(["--conversation", session_id])
|
|
111
|
+
notes.append("resume <conversation> -> --conversation <conversation>")
|
|
112
|
+
i += 2
|
|
113
|
+
else:
|
|
114
|
+
out.append("--continue")
|
|
115
|
+
notes.append("resume -> --continue")
|
|
116
|
+
i += 1
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
if arg in ("--continue", "-c"):
|
|
120
|
+
out.append(arg)
|
|
121
|
+
i += 1
|
|
122
|
+
continue
|
|
123
|
+
if arg.startswith("--continue="):
|
|
124
|
+
out.append("--continue")
|
|
125
|
+
prompt = arg.split("=", 1)[1]
|
|
126
|
+
if prompt:
|
|
127
|
+
out.append(prompt)
|
|
128
|
+
notes.append("--continue=<prompt> -> --continue <prompt>")
|
|
129
|
+
i += 1
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
if arg in ("--resume", "-r"):
|
|
133
|
+
session_id, i = _agy_consume_optional_value(passthrough, i)
|
|
134
|
+
if session_id:
|
|
135
|
+
out.extend(["--conversation", session_id])
|
|
136
|
+
notes.append(f"{arg} <session> -> --conversation <session>")
|
|
137
|
+
else:
|
|
138
|
+
out.append("--continue")
|
|
139
|
+
notes.append(f"{arg} -> --continue")
|
|
140
|
+
continue
|
|
141
|
+
if arg.startswith("--resume="):
|
|
142
|
+
session_id = arg.split("=", 1)[1]
|
|
143
|
+
if session_id:
|
|
144
|
+
out.extend(["--conversation", session_id])
|
|
145
|
+
notes.append("--resume=<session> -> --conversation <session>")
|
|
146
|
+
else:
|
|
147
|
+
out.append("--continue")
|
|
148
|
+
notes.append("--resume= -> --continue")
|
|
149
|
+
i += 1
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
if arg == "--session-id":
|
|
153
|
+
session_id, i = _agy_consume_optional_value(passthrough, i)
|
|
154
|
+
if session_id:
|
|
155
|
+
out.extend(["--conversation", session_id])
|
|
156
|
+
notes.append("--session-id <session> -> --conversation <session>")
|
|
157
|
+
continue
|
|
158
|
+
if arg.startswith("--session-id="):
|
|
159
|
+
session_id = arg.split("=", 1)[1]
|
|
160
|
+
if session_id:
|
|
161
|
+
out.extend(["--conversation", session_id])
|
|
162
|
+
notes.append("--session-id=<session> -> --conversation <session>")
|
|
163
|
+
i += 1
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
if arg in ("--print", "-p", "--prompt"):
|
|
167
|
+
out.append("--print")
|
|
168
|
+
i += 1
|
|
169
|
+
continue
|
|
170
|
+
if arg.startswith(("--print=", "--prompt=")):
|
|
171
|
+
prompt = arg.split("=", 1)[1]
|
|
172
|
+
out.append("--print")
|
|
173
|
+
if prompt:
|
|
174
|
+
out.append(prompt)
|
|
175
|
+
notes.append(f"{arg.split('=', 1)[0]}=<prompt> -> --print <prompt>")
|
|
176
|
+
i += 1
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
if arg == "exec" and i == 0 and not existing_agy_command:
|
|
180
|
+
out.append("--print")
|
|
181
|
+
notes.append("exec -> --print")
|
|
182
|
+
i += 1
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
if arg in ("--yolo", "--dangerously-bypass-approvals-and-sandbox"):
|
|
186
|
+
if not mapped_permission_bypass:
|
|
187
|
+
out.append("--dangerously-skip-permissions")
|
|
188
|
+
mapped_permission_bypass = True
|
|
189
|
+
notes.append(f"{arg} -> --dangerously-skip-permissions")
|
|
190
|
+
i += 1
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
if arg == "--dangerously-skip-permissions":
|
|
194
|
+
if not mapped_permission_bypass:
|
|
195
|
+
out.append(arg)
|
|
196
|
+
mapped_permission_bypass = True
|
|
197
|
+
i += 1
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
if arg == "--permission-mode" or arg.startswith("--permission-mode="):
|
|
201
|
+
if arg == "--permission-mode":
|
|
202
|
+
value, i = _agy_consume_optional_value(passthrough, i)
|
|
203
|
+
else:
|
|
204
|
+
value = arg.split("=", 1)[1]
|
|
205
|
+
i += 1
|
|
206
|
+
if value == "bypassPermissions" and not mapped_permission_bypass:
|
|
207
|
+
out.append("--dangerously-skip-permissions")
|
|
208
|
+
mapped_permission_bypass = True
|
|
209
|
+
notes.append("--permission-mode bypassPermissions -> --dangerously-skip-permissions")
|
|
210
|
+
else:
|
|
211
|
+
notes.append("--permission-mode ignored for AGY")
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
if arg in ("--channels", "--dangerously-load-development-channels") or arg.startswith(
|
|
215
|
+
("--channels=", "--dangerously-load-development-channels=")
|
|
216
|
+
):
|
|
217
|
+
i = _agy_drop_passthrough_channel_args(passthrough, i)
|
|
218
|
+
notes.append(f"{arg.split('=', 1)[0]} ignored for AGY launch")
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
if arg in AGY_CLAUDE_ONLY_VALUE_FLAGS:
|
|
222
|
+
if arg == "--mcp-config":
|
|
223
|
+
i = _agy_drop_greedy_passthrough_values(passthrough, i)
|
|
224
|
+
else:
|
|
225
|
+
_, i = _agy_consume_optional_value(passthrough, i)
|
|
226
|
+
notes.append(f"{arg} ignored for AGY launch")
|
|
227
|
+
continue
|
|
228
|
+
if any(arg.startswith(flag + "=") for flag in AGY_CLAUDE_ONLY_VALUE_FLAGS):
|
|
229
|
+
notes.append(f"{arg.split('=', 1)[0]} ignored for AGY launch")
|
|
230
|
+
i += 1
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
if arg == "--fork-session" or arg.startswith("--fork-session="):
|
|
234
|
+
if arg == "--fork-session":
|
|
235
|
+
i += 1
|
|
236
|
+
else:
|
|
237
|
+
i += 1
|
|
238
|
+
notes.append("--fork-session ignored for AGY launch")
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
out.append(arg)
|
|
242
|
+
i += 1
|
|
243
|
+
|
|
244
|
+
return out, notes
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def agy_dangerous_launch_args(passthrough: list[str]) -> list[str]:
|
|
248
|
+
return [] if "--dangerously-skip-permissions" in passthrough else ["--dangerously-skip-permissions"]
|