open-codex-ui 0.1.4__py3-none-any.whl
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.
- open_codex_ui-0.1.4.dist-info/METADATA +249 -0
- open_codex_ui-0.1.4.dist-info/RECORD +40 -0
- open_codex_ui-0.1.4.dist-info/WHEEL +5 -0
- open_codex_ui-0.1.4.dist-info/entry_points.txt +7 -0
- open_codex_ui-0.1.4.dist-info/top_level.txt +1 -0
- yier_web/__init__.py +6 -0
- yier_web/app.py +223 -0
- yier_web/auth.py +269 -0
- yier_web/cli.py +172 -0
- yier_web/codex/__init__.py +3 -0
- yier_web/codex/ipc_manager.py +1761 -0
- yier_web/codex/session_events.py +110 -0
- yier_web/codex/ws_commands.py +299 -0
- yier_web/config.py +651 -0
- yier_web/directory_picker.py +93 -0
- yier_web/event_stream.py +29 -0
- yier_web/frontend.py +204 -0
- yier_web/routes/__init__.py +17 -0
- yier_web/routes/codex.py +573 -0
- yier_web/routes/core.py +281 -0
- yier_web/schemas.py +534 -0
- yier_web/static/assets/CodexEmbedView-CN_-Mhe2.js +1 -0
- yier_web/static/assets/CodexView-wpI61iXa.js +606 -0
- yier_web/static/assets/LoginView-CELCom2O.js +101 -0
- yier_web/static/assets/api-CeihACIV.js +1099 -0
- yier_web/static/assets/index-BdFqJ-Kl.css +1 -0
- yier_web/static/assets/index-CjVNk6ja.js +183 -0
- yier_web/static/assets/index-mSBvq1p8.js +79 -0
- yier_web/static/assets/open-codex-ui-icon-DKJ1ZKj4.svg +99 -0
- yier_web/static/assets/primeicons-C6QP2o4f.woff2 +0 -0
- yier_web/static/assets/primeicons-DMOk5skT.eot +0 -0
- yier_web/static/assets/primeicons-Dr5RGzOO.svg +345 -0
- yier_web/static/assets/primeicons-MpK4pl85.ttf +0 -0
- yier_web/static/assets/primeicons-WjwUDZjB.woff +0 -0
- yier_web/static/assets/useCodexWorkspace-6QenDzvb.css +1 -0
- yier_web/static/assets/useCodexWorkspace-D1rCOO1x.js +1128 -0
- yier_web/static/brand/open-codex-ui-logo.svg +85 -0
- yier_web/static/favicon.ico +0 -0
- yier_web/static/favicon.svg +99 -0
- yier_web/static/index.html +24 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: open-codex-ui
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Remote-friendly web workspace for Codex sessions
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: open-codex-bridge>=0.1.9
|
|
8
|
+
Requires-Dist: granian[reload]>=2.7.2
|
|
9
|
+
Requires-Dist: httpx>=0.28.1
|
|
10
|
+
Requires-Dist: litestar>=2.21.1
|
|
11
|
+
Requires-Dist: pydantic>=2.12.5
|
|
12
|
+
Requires-Dist: yier-agents>=0.1.3
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="web/public/brand/open-codex-ui-logo.svg" alt="Open Codex UI" width="520">
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
# open-codex-ui
|
|
19
|
+
|
|
20
|
+
Remote-friendly Codex web workspace for continuing sessions from desktop and mobile browsers.
|
|
21
|
+
|
|
22
|
+
## Resume
|
|
23
|
+
[一二Resume](https://baike.baidu.com/item/%E4%B8%80%E4%BA%8C/23434669)
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- [`uv`](https://docs.astral.sh/uv/) for running the published application
|
|
28
|
+
- Python 3.12+, Node.js 20+, and `pnpm` only for source development
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
Run the published application without a permanent installation:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uvx open-codex-ui
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The wheel includes the compiled frontend and starts in production mode. For a
|
|
39
|
+
persistent command, install it as a uv tool:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv tool install open-codex-ui
|
|
43
|
+
open-codex-ui
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For source development, install the backend dependencies:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv sync
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Codex workspace support uses the published `open-codex-bridge` package from PyPI.
|
|
53
|
+
|
|
54
|
+
Then install the frontend dependencies:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cd web
|
|
58
|
+
pnpm install
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Authentication
|
|
62
|
+
|
|
63
|
+
This app now supports password protection for deployed environments.
|
|
64
|
+
|
|
65
|
+
Enable auth with either:
|
|
66
|
+
|
|
67
|
+
- `YIER_AUTH_PASSWORD`
|
|
68
|
+
- `YIER_AUTH_PASSWORD_HASH`
|
|
69
|
+
|
|
70
|
+
Plain password example:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
export YIER_AUTH_PASSWORD='change-this-password'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Hashed password example:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
uv run python -c "from yier_web.auth import hash_password; print(hash_password('change-this-password'))"
|
|
80
|
+
export YIER_AUTH_PASSWORD_HASH='paste-generated-hash-here'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Optional auth settings:
|
|
84
|
+
|
|
85
|
+
- `YIER_AUTH_SECRET`: optional extra signing secret for session cookies
|
|
86
|
+
- `YIER_AUTH_SESSION_TTL_HOURS`: cookie lifetime in hours, default is `168`
|
|
87
|
+
- `YIER_CODEX_EMBED_TOKEN`: token for unauthenticated Codex iframe access
|
|
88
|
+
|
|
89
|
+
If neither password variable is set, authentication is disabled.
|
|
90
|
+
|
|
91
|
+
## Codex Iframe Embed
|
|
92
|
+
|
|
93
|
+
See [IFRAME.md](./IFRAME.md) for iframe setup, authentication, and the
|
|
94
|
+
`postMessage` API.
|
|
95
|
+
|
|
96
|
+
## Development Startup
|
|
97
|
+
|
|
98
|
+
Development mode is different from production:
|
|
99
|
+
|
|
100
|
+
- The backend should run with `--debug`
|
|
101
|
+
- The frontend should run with Vite dev server
|
|
102
|
+
- In this mode, the backend proxies frontend requests to `http://127.0.0.1:5173`
|
|
103
|
+
|
|
104
|
+
Recommended one-command startup:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
uv run open-codex-ui-dev
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This starts:
|
|
111
|
+
|
|
112
|
+
- frontend: `pnpm dev`
|
|
113
|
+
- backend: debug mode with reload
|
|
114
|
+
|
|
115
|
+
If you prefer split terminals:
|
|
116
|
+
|
|
117
|
+
Frontend only:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
uv run open-codex-ui-dev-web
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Backend only:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
uv run open-codex-ui-dev-backend
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
You can also override backend bind settings:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
uv run open-codex-ui-dev --host 127.0.0.1 --port 9999
|
|
133
|
+
uv run open-codex-ui-dev-backend --host 127.0.0.1 --port 9999
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Default address:
|
|
137
|
+
|
|
138
|
+
- App: `http://127.0.0.1:9999`
|
|
139
|
+
- Vite dev server: `http://127.0.0.1:5173`
|
|
140
|
+
|
|
141
|
+
Notes:
|
|
142
|
+
|
|
143
|
+
- Keep `pnpm dev` running, otherwise the backend cannot proxy the frontend in debug mode.
|
|
144
|
+
- API requests still go through the Python server at port `9999`.
|
|
145
|
+
|
|
146
|
+
## Production Startup
|
|
147
|
+
|
|
148
|
+
The published wheel includes the compiled frontend and does not use the Vite
|
|
149
|
+
dev server:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
uvx open-codex-ui
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The server listens on `127.0.0.1:9999` by default. Bind to the local network
|
|
156
|
+
explicitly when needed:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
uvx open-codex-ui --host 0.0.0.0 --port 9999
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
When running from a source checkout, build the frontend before starting:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
uv run open-codex-ui-build-web
|
|
166
|
+
uv run open-codex-ui-prod
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
In production mode:
|
|
170
|
+
|
|
171
|
+
- The backend serves the compiled assets packaged under `yier_web/static`
|
|
172
|
+
- No Vite proxy is used
|
|
173
|
+
- Authentication should usually be enabled with `YIER_AUTH_PASSWORD` or `YIER_AUTH_PASSWORD_HASH`
|
|
174
|
+
|
|
175
|
+
Production example:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
export YIER_AUTH_PASSWORD='change-this-password'
|
|
179
|
+
uvx open-codex-ui --host 0.0.0.0 --port 9999
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Common Commands
|
|
183
|
+
|
|
184
|
+
Backend tests:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
uv run --all-packages pytest
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Targeted backend tests:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
uv run --all-packages pytest tests/test_codex_backend.py tests/test_codex_workspace.py tests/test_app.py
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Backend compile check:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
uv run python -m compileall yier_web
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Frontend unit tests:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
cd web
|
|
206
|
+
pnpm test:unit
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Frontend type check:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
cd web
|
|
213
|
+
pnpm type-check
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Frontend production build:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
uv run open-codex-ui-build-web
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Startup Summary
|
|
223
|
+
|
|
224
|
+
Development:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
uv run open-codex-ui-dev
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Production:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
export YIER_AUTH_PASSWORD='change-this-password'
|
|
234
|
+
uvx open-codex-ui --host 0.0.0.0 --port 9999
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Available uv Scripts
|
|
238
|
+
|
|
239
|
+
Development:
|
|
240
|
+
|
|
241
|
+
- `uv run open-codex-ui-dev`: start frontend and backend together
|
|
242
|
+
- `uv run open-codex-ui-dev-web`: start Vite only
|
|
243
|
+
- `uv run open-codex-ui-dev-backend`: start backend only in debug mode
|
|
244
|
+
|
|
245
|
+
Production:
|
|
246
|
+
|
|
247
|
+
- `uvx open-codex-ui`: run the published wheel in production mode
|
|
248
|
+
- `uv run open-codex-ui-build-web`: build frontend assets
|
|
249
|
+
- `uv run open-codex-ui-prod`: start backend in production mode
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
yier_web/__init__.py,sha256=-r2mRpa4wroHvmUsgEW-KHSGwYwKBWBVfacqLSTXBjU,205
|
|
2
|
+
yier_web/app.py,sha256=vL0k_CyXF34QTNgtb8APT4AMU3CMqlnld0wGfgiWDns,7183
|
|
3
|
+
yier_web/auth.py,sha256=69hRO2QbLVE-f77EldFUQz9JXD6EdC9ErUmUWgyjdDE,8533
|
|
4
|
+
yier_web/cli.py,sha256=t6W5E9qBEF4Plf0DhB-YB-RR5S-o_8rz3jyyhBNMZ8k,4487
|
|
5
|
+
yier_web/config.py,sha256=KpoOu5Oq0Roqxga7p8ioeZ3HNNqINNx-UnaI_ON-4OE,25515
|
|
6
|
+
yier_web/directory_picker.py,sha256=clnA-nDqXBWuXxCmylM0tUkUd4k6mtd3Orra8uZVW14,3149
|
|
7
|
+
yier_web/event_stream.py,sha256=G4ekSXUOHyymfBnPJHdR_cPm5dhO9Trty558F4Kpwyc,854
|
|
8
|
+
yier_web/frontend.py,sha256=JDfRqHj3rShl2ASD2Je-5ya7dC2a2jElbsfpZimqKIs,6607
|
|
9
|
+
yier_web/schemas.py,sha256=h-p2CwQqFebaGXBbBBxoUieTMaxhZ6szhubHrgdzrUs,15410
|
|
10
|
+
yier_web/codex/__init__.py,sha256=HOpAEcNgvHUEbRPUxdueiVzR9dHCZcZ8Kdrge-JSDpA,86
|
|
11
|
+
yier_web/codex/ipc_manager.py,sha256=10hd2xDCWms7BrGBU0pAb8rg-Zohquszp104yeGx-Z8,63588
|
|
12
|
+
yier_web/codex/session_events.py,sha256=USaxcbLZ8xm5ZW8JavVRxlPh-O7v2_JAjJdu_ZpnTl4,3423
|
|
13
|
+
yier_web/codex/ws_commands.py,sha256=SCkajF0FIKG7U4ZyIsjjajTmW4PlVZJC9BDbVIu5InE,12130
|
|
14
|
+
yier_web/routes/__init__.py,sha256=e78z7qWu_A8iBECHGSsfkuHgl53o14ck34cm94yRRAI,351
|
|
15
|
+
yier_web/routes/codex.py,sha256=Hg2bao3OP5Pyq2RrZake7ufeUC3Nlc8LWj8giqkAHwA,18994
|
|
16
|
+
yier_web/routes/core.py,sha256=OyLJzX_utJG7X2e1E_uYxlbF9hwkIXDeYw1qLRErq7w,9337
|
|
17
|
+
yier_web/static/favicon.ico,sha256=23SrC3gzjB93j4OYxF9BA8ma6g6EWjEYp3ULTur9NEU,4286
|
|
18
|
+
yier_web/static/favicon.svg,sha256=LD-C-zFycjHm88vsZ8CufvM5FtTgSXBCKrm1aF497-Y,5456
|
|
19
|
+
yier_web/static/index.html,sha256=tw1-6vi7ZkHiVVzELn5Kgxfj28flOEQcTLsjkzoYCGY,461
|
|
20
|
+
yier_web/static/assets/CodexEmbedView-CN_-Mhe2.js,sha256=lw6gRNcZEqy8TbAvchvq43g_KzgxP097BYJ8LG4mNRY,13167
|
|
21
|
+
yier_web/static/assets/CodexView-wpI61iXa.js,sha256=H_2hX3G7Vaje0Kli4_20tei3R8qA0KGWnd-6wpQbwq0,140981
|
|
22
|
+
yier_web/static/assets/LoginView-CELCom2O.js,sha256=OIM2-1NDoICAUfMQWBq2JMdRQo10y2y0gzpEU6TsvAM,25258
|
|
23
|
+
yier_web/static/assets/api-CeihACIV.js,sha256=46tjWnbZqNHmu01V2FywnWjlz2jiqAxSlrp4cbuCVRM,89285
|
|
24
|
+
yier_web/static/assets/index-BdFqJ-Kl.css,sha256=p_DpD0lihpNIfCS7SFdqsWLz7HoRVcntpSYVQNgeeDE,225466
|
|
25
|
+
yier_web/static/assets/index-CjVNk6ja.js,sha256=TFF8oghOBwOo01FHwoMwcRY7QU4d0e3XmkFKYAh36as,262843
|
|
26
|
+
yier_web/static/assets/index-mSBvq1p8.js,sha256=ZI-MHBca6RRdu4FS0nbcFAcB4VlNNaya_GQTdnf4ZlI,7940
|
|
27
|
+
yier_web/static/assets/open-codex-ui-icon-DKJ1ZKj4.svg,sha256=LD-C-zFycjHm88vsZ8CufvM5FtTgSXBCKrm1aF497-Y,5456
|
|
28
|
+
yier_web/static/assets/primeicons-C6QP2o4f.woff2,sha256=nupuC8uXTsVo_cse7BgtO1ODo6I4maaQFAssAWeIaII,35148
|
|
29
|
+
yier_web/static/assets/primeicons-DMOk5skT.eot,sha256=mH8j6t-nDGAydqccPJdcjQU02ccZ2Kbj2WUAXnuTdHs,85156
|
|
30
|
+
yier_web/static/assets/primeicons-Dr5RGzOO.svg,sha256=NsVM7PypR6aRPJbfiEMAzhQQjE2MscskGWkyE0L4kMk,342525
|
|
31
|
+
yier_web/static/assets/primeicons-MpK4pl85.ttf,sha256=3Ilf7p884bDq8WtLn6sgB3_CmB5sCzMuTJfBdoMvwFs,84980
|
|
32
|
+
yier_web/static/assets/primeicons-WjwUDZjB.woff,sha256=9dHSGV4FuAhZU6p9ULLncHDNESDgEAqGAHcapboRoB4,85056
|
|
33
|
+
yier_web/static/assets/useCodexWorkspace-6QenDzvb.css,sha256=5zqWEvm9Rk9W_0yFhBM38z9_Z-rQ8kKweqgVj1rNzjk,688
|
|
34
|
+
yier_web/static/assets/useCodexWorkspace-D1rCOO1x.js,sha256=vARGj-G54vKSYdH2VdwcGJww7O4mfMHu2oOebFQ-ORU,535100
|
|
35
|
+
yier_web/static/brand/open-codex-ui-logo.svg,sha256=1b_HKKMW5KJJRFKbMfVMUtYgrvCm9sIIqq34hcigGsk,4659
|
|
36
|
+
open_codex_ui-0.1.4.dist-info/METADATA,sha256=JdXS10nB8qKFgUcdXv7JWCZGjjrX7exGUzql9EL77Bs,5129
|
|
37
|
+
open_codex_ui-0.1.4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
38
|
+
open_codex_ui-0.1.4.dist-info/entry_points.txt,sha256=msxW84jsEug9cqyQb1KtvAH67L6YucmA9kREUoPCSbo,275
|
|
39
|
+
open_codex_ui-0.1.4.dist-info/top_level.txt,sha256=oz3Nm_WgVsyUMc_ygyvELnJvP6nllfVQu1cn_XRHZKU,9
|
|
40
|
+
open_codex_ui-0.1.4.dist-info/RECORD,,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
[console_scripts]
|
|
2
|
+
open-codex-ui = yier_web.cli:prod
|
|
3
|
+
open-codex-ui-build-web = yier_web.cli:build_web
|
|
4
|
+
open-codex-ui-dev = yier_web.cli:dev
|
|
5
|
+
open-codex-ui-dev-backend = yier_web.cli:dev_backend
|
|
6
|
+
open-codex-ui-dev-web = yier_web.cli:dev_web
|
|
7
|
+
open-codex-ui-prod = yier_web.cli:prod
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
yier_web
|
yier_web/__init__.py
ADDED
yier_web/app.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import signal
|
|
9
|
+
from typing import Any, AsyncIterator
|
|
10
|
+
|
|
11
|
+
from litestar import Litestar, Request, Router, get, websocket
|
|
12
|
+
from litestar.connection import WebSocket
|
|
13
|
+
from litestar.datastructures import State
|
|
14
|
+
from litestar.exceptions import HTTPException
|
|
15
|
+
from litestar.logging import LoggingConfig
|
|
16
|
+
from litestar.response import Response
|
|
17
|
+
from litestar.status_codes import HTTP_404_NOT_FOUND
|
|
18
|
+
|
|
19
|
+
from yier_web.auth import AuthService
|
|
20
|
+
from yier_web.config import AppConfigService
|
|
21
|
+
from yier_web.codex.ipc_manager import CodexIpcManager
|
|
22
|
+
from yier_web.directory_picker import LocalDirectoryPickerService
|
|
23
|
+
from yier_web.event_stream import EventStreamBroker
|
|
24
|
+
from yier_web.frontend import FrontendService
|
|
25
|
+
from yier_web.routes import (
|
|
26
|
+
AuthController,
|
|
27
|
+
CodexController,
|
|
28
|
+
ConfigController,
|
|
29
|
+
EventsController,
|
|
30
|
+
HealthController,
|
|
31
|
+
SystemController,
|
|
32
|
+
)
|
|
33
|
+
from yier_web.routes.core import (
|
|
34
|
+
wait_for_event_stream_item as wait_for_event_stream_item,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(slots=True)
|
|
39
|
+
class AppServices:
|
|
40
|
+
config_service: AppConfigService
|
|
41
|
+
codex_ipc_manager: CodexIpcManager
|
|
42
|
+
event_broker: EventStreamBroker
|
|
43
|
+
frontend_service: FrontendService
|
|
44
|
+
directory_picker_service: LocalDirectoryPickerService
|
|
45
|
+
auth_service: AuthService
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _env_flag(name: str) -> bool:
|
|
49
|
+
return os.getenv(name, "").strip().lower() not in {
|
|
50
|
+
"",
|
|
51
|
+
"0",
|
|
52
|
+
"false",
|
|
53
|
+
"no",
|
|
54
|
+
"off",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _build_logging_config(*, debug: bool) -> LoggingConfig:
|
|
59
|
+
return LoggingConfig(
|
|
60
|
+
root={"level": "DEBUG" if debug else "INFO", "handlers": ["queue_listener"]},
|
|
61
|
+
formatters={
|
|
62
|
+
"standard": {
|
|
63
|
+
"format": "%(asctime)s %(levelname)s %(name)s - %(message)s",
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
loggers={
|
|
67
|
+
"granian": {"level": "INFO", "handlers": ["queue_listener"]},
|
|
68
|
+
"httpx": {"level": "WARNING", "handlers": ["queue_listener"]},
|
|
69
|
+
"httpcore": {"level": "WARNING", "handlers": ["queue_listener"]},
|
|
70
|
+
"httpcore.connection": {"level": "WARNING", "handlers": ["queue_listener"]},
|
|
71
|
+
"httpcore.http11": {"level": "WARNING", "handlers": ["queue_listener"]},
|
|
72
|
+
},
|
|
73
|
+
log_exceptions="always",
|
|
74
|
+
disable_stack_trace={HTTP_404_NOT_FOUND},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def install_shutdown_signal_bridge(shutdown_event: asyncio.Event) -> None:
|
|
79
|
+
try:
|
|
80
|
+
loop = asyncio.get_running_loop()
|
|
81
|
+
except RuntimeError:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if getattr(loop, "_yier_shutdown_signal_bridge_installed", False):
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
signal_handlers = getattr(loop, "_signal_handlers", None)
|
|
88
|
+
if not isinstance(signal_handlers, dict):
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
installed = False
|
|
92
|
+
for signum in (signal.SIGINT, signal.SIGTERM):
|
|
93
|
+
existing_handle = signal_handlers.get(signum)
|
|
94
|
+
if existing_handle is None or not hasattr(existing_handle, "_run"):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
def chained_handler(
|
|
98
|
+
handle: Any = existing_handle,
|
|
99
|
+
current_loop: asyncio.AbstractEventLoop = loop,
|
|
100
|
+
) -> None:
|
|
101
|
+
shutdown_event.set()
|
|
102
|
+
current_loop.call_soon(handle._run)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
loop.add_signal_handler(signum, chained_handler)
|
|
106
|
+
except (NotImplementedError, RuntimeError, ValueError):
|
|
107
|
+
continue
|
|
108
|
+
installed = True
|
|
109
|
+
|
|
110
|
+
if installed:
|
|
111
|
+
setattr(loop, "_yier_shutdown_signal_bridge_installed", True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def build_services(
|
|
115
|
+
project_root: Path | None = None,
|
|
116
|
+
home_dir: Path | None = None,
|
|
117
|
+
) -> AppServices:
|
|
118
|
+
resolved_root = (project_root or Path.cwd()).resolve()
|
|
119
|
+
config_service = AppConfigService(project_root=resolved_root, home_dir=home_dir)
|
|
120
|
+
event_broker = EventStreamBroker()
|
|
121
|
+
return AppServices(
|
|
122
|
+
config_service=config_service,
|
|
123
|
+
codex_ipc_manager=CodexIpcManager(
|
|
124
|
+
config_service=config_service,
|
|
125
|
+
event_broker=event_broker,
|
|
126
|
+
),
|
|
127
|
+
event_broker=event_broker,
|
|
128
|
+
frontend_service=FrontendService(
|
|
129
|
+
debug=_env_flag("YIER_DEBUG"),
|
|
130
|
+
),
|
|
131
|
+
directory_picker_service=LocalDirectoryPickerService(),
|
|
132
|
+
auth_service=AuthService(),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
api_router = Router(
|
|
137
|
+
path="/api",
|
|
138
|
+
route_handlers=[
|
|
139
|
+
AuthController,
|
|
140
|
+
HealthController,
|
|
141
|
+
ConfigController,
|
|
142
|
+
SystemController,
|
|
143
|
+
CodexController,
|
|
144
|
+
EventsController,
|
|
145
|
+
],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@get(path=["/", "/{path:path}"], include_in_schema=False)
|
|
150
|
+
async def frontend_entry(
|
|
151
|
+
request: Request[Any, Any, Any], state: State, path: str = ""
|
|
152
|
+
) -> Response:
|
|
153
|
+
if path.startswith("api/"):
|
|
154
|
+
raise HTTPException(status_code=HTTP_404_NOT_FOUND)
|
|
155
|
+
return await state.frontend_service.handle_request(request, path)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@websocket(path=["/", "/{path:path}"], include_in_schema=False)
|
|
159
|
+
async def frontend_entry_websocket(
|
|
160
|
+
socket: WebSocket,
|
|
161
|
+
path: str = "",
|
|
162
|
+
) -> None:
|
|
163
|
+
if path.startswith("api/"):
|
|
164
|
+
await socket.accept()
|
|
165
|
+
await socket.close(code=1008, reason="WebSocket route not found.")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
await socket.app.state.frontend_service.handle_websocket(socket, path)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def create_app(
|
|
172
|
+
project_root: Path | None = None,
|
|
173
|
+
home_dir: Path | None = None,
|
|
174
|
+
services: AppServices | None = None,
|
|
175
|
+
) -> Litestar:
|
|
176
|
+
resolved_root = (project_root or Path.cwd()).resolve()
|
|
177
|
+
debug = _env_flag("YIER_DEBUG")
|
|
178
|
+
app_services = services or build_services(
|
|
179
|
+
project_root=resolved_root, home_dir=home_dir
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
async def before_request(request: Request[Any, Any, Any]) -> Response | None:
|
|
183
|
+
auth_service = app_services.auth_service
|
|
184
|
+
if not auth_service.enabled:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
request_path = request.url.path
|
|
188
|
+
if auth_service.is_public_path(request_path):
|
|
189
|
+
return None
|
|
190
|
+
if auth_service.is_authenticated(request):
|
|
191
|
+
return None
|
|
192
|
+
if request_path.startswith("/api/"):
|
|
193
|
+
return auth_service.build_unauthorized_api_response()
|
|
194
|
+
return auth_service.build_login_redirect(request)
|
|
195
|
+
|
|
196
|
+
@asynccontextmanager
|
|
197
|
+
async def lifespan(app: Litestar) -> AsyncIterator[None]:
|
|
198
|
+
shutdown_event = asyncio.Event()
|
|
199
|
+
install_shutdown_signal_bridge(shutdown_event)
|
|
200
|
+
app.state.shutdown_event = shutdown_event
|
|
201
|
+
app.state.config_service = app_services.config_service
|
|
202
|
+
app.state.codex_ipc_manager = app_services.codex_ipc_manager
|
|
203
|
+
app.state.event_broker = app_services.event_broker
|
|
204
|
+
app.state.frontend_service = app_services.frontend_service
|
|
205
|
+
app.state.directory_picker_service = app_services.directory_picker_service
|
|
206
|
+
app.state.auth_service = app_services.auth_service
|
|
207
|
+
await app_services.codex_ipc_manager.start()
|
|
208
|
+
try:
|
|
209
|
+
yield
|
|
210
|
+
finally:
|
|
211
|
+
shutdown_event.set()
|
|
212
|
+
await app_services.codex_ipc_manager.stop()
|
|
213
|
+
|
|
214
|
+
return Litestar(
|
|
215
|
+
route_handlers=[api_router, frontend_entry, frontend_entry_websocket],
|
|
216
|
+
before_request=before_request,
|
|
217
|
+
lifespan=[lifespan],
|
|
218
|
+
debug=debug,
|
|
219
|
+
logging_config=_build_logging_config(debug=debug),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
app = create_app()
|