@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.
@@ -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"]