gyoshu 0.4.0 → 0.4.2
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/README.md +36 -8
- package/bin/gyoshu.js +181 -0
- package/package.json +8 -2
- package/src/bridge/__pycache__/gyoshu_bridge.cpython-310.pyc +0 -0
- package/src/bridge/gyoshu_bridge.py +130 -14
- package/src/index.ts +102 -29
- package/src/lib/artifact-security.ts +15 -39
- package/src/lib/atomic-write.ts +244 -34
- package/src/lib/auto-loop-state.ts +2 -3
- package/src/lib/cell-identity.ts +4 -2
- package/src/lib/paths.ts +136 -44
- package/src/lib/pdf-export.ts +297 -39
- package/src/lib/report-gates.ts +94 -29
- package/src/lib/report-markdown.ts +29 -8
- package/src/lib/session-lock.ts +21 -25
- package/src/plugin/gyoshu-hooks.ts +3 -2
- package/src/tool/checkpoint-manager.ts +90 -9
- package/src/tool/gyoshu-completion.ts +2 -2
- package/src/tool/gyoshu-snapshot.ts +6 -4
- package/src/tool/migration-tool.ts +15 -27
- package/src/tool/notebook-search.ts +72 -7
- package/src/tool/notebook-writer.ts +28 -3
- package/src/tool/parallel-manager.ts +10 -15
- package/src/tool/python-repl.ts +73 -37
- package/src/tool/research-manager.ts +10 -4
- package/src/tool/retrospective-store.ts +49 -18
- package/src/tool/session-manager.ts +8 -2
- package/src/tool/session-structure-validator.ts +27 -11
package/README.md
CHANGED
|
@@ -47,6 +47,8 @@ Think of it like a research lab:
|
|
|
47
47
|
|
|
48
48
|
## 🚀 Installation
|
|
49
49
|
|
|
50
|
+
### Option 1: OpenCode Plugin (Recommended)
|
|
51
|
+
|
|
50
52
|
Add Gyoshu to your `opencode.json`:
|
|
51
53
|
|
|
52
54
|
```json
|
|
@@ -55,12 +57,25 @@ Add Gyoshu to your `opencode.json`:
|
|
|
55
57
|
}
|
|
56
58
|
```
|
|
57
59
|
|
|
58
|
-
That's it! OpenCode will auto-install Gyoshu
|
|
60
|
+
That's it! OpenCode will auto-install Gyoshu from npm on next startup.
|
|
61
|
+
|
|
62
|
+
### Option 2: CLI Installer
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Using bunx (no global install needed)
|
|
66
|
+
bunx gyoshu install
|
|
67
|
+
|
|
68
|
+
# Or install globally first
|
|
69
|
+
npm install -g gyoshu
|
|
70
|
+
gyoshu install
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The CLI automatically adds Gyoshu to your `opencode.json`.
|
|
59
74
|
|
|
60
75
|
<details>
|
|
61
|
-
<summary>📦 Development installation</summary>
|
|
76
|
+
<summary>📦 Development installation (for contributors)</summary>
|
|
62
77
|
|
|
63
|
-
**Clone & link locally
|
|
78
|
+
**Clone & link locally:**
|
|
64
79
|
```bash
|
|
65
80
|
git clone https://github.com/Yeachan-Heo/My-Jogyo.git
|
|
66
81
|
cd My-Jogyo && bun install
|
|
@@ -77,6 +92,10 @@ Then in your `opencode.json`:
|
|
|
77
92
|
|
|
78
93
|
**Verify installation:**
|
|
79
94
|
```bash
|
|
95
|
+
# Check status via CLI
|
|
96
|
+
bunx gyoshu check
|
|
97
|
+
|
|
98
|
+
# Or in OpenCode
|
|
80
99
|
opencode
|
|
81
100
|
/gyoshu doctor
|
|
82
101
|
```
|
|
@@ -87,7 +106,7 @@ opencode
|
|
|
87
106
|
|
|
88
107
|
> *Using Claude, GPT, Gemini, or another AI assistant with OpenCode? This section is for you.*
|
|
89
108
|
|
|
90
|
-
**Setup is the same** — add `"gyoshu"` to your plugin array
|
|
109
|
+
**Setup is the same** — run `bunx gyoshu install` or add `"gyoshu"` to your plugin array. Then give your LLM the context it needs:
|
|
91
110
|
|
|
92
111
|
1. **Point your LLM to the guide:**
|
|
93
112
|
> "Read `AGENTS.md` in the Gyoshu directory for full context on how to use the research tools."
|
|
@@ -327,8 +346,7 @@ Gyoshu uses your project's `.venv/` virtual environment:
|
|
|
327
346
|
|
|
328
347
|
| Priority | Type | How It's Detected |
|
|
329
348
|
|----------|------|-------------------|
|
|
330
|
-
| 1️⃣ |
|
|
331
|
-
| 2️⃣ | venv | `.venv/bin/python` exists |
|
|
349
|
+
| 1️⃣ | venv | `.venv/bin/python` exists |
|
|
332
350
|
|
|
333
351
|
**Quick setup:**
|
|
334
352
|
```bash
|
|
@@ -358,15 +376,25 @@ python3 -m venv .venv
|
|
|
358
376
|
|
|
359
377
|
## 🔄 Updating
|
|
360
378
|
|
|
361
|
-
|
|
379
|
+
Gyoshu is distributed via npm. OpenCode automatically handles plugin updates.
|
|
362
380
|
|
|
381
|
+
**Force update:**
|
|
363
382
|
```bash
|
|
383
|
+
# Clear OpenCode's cache
|
|
364
384
|
rm -rf ~/.cache/opencode/node_modules/gyoshu
|
|
385
|
+
|
|
386
|
+
# Or reinstall with latest version
|
|
387
|
+
bunx gyoshu@latest install
|
|
365
388
|
```
|
|
366
389
|
|
|
367
390
|
Then restart OpenCode.
|
|
368
391
|
|
|
369
|
-
Verify
|
|
392
|
+
**Verify:** `opencode` then `/gyoshu doctor`
|
|
393
|
+
|
|
394
|
+
**Uninstall:**
|
|
395
|
+
```bash
|
|
396
|
+
bunx gyoshu uninstall
|
|
397
|
+
```
|
|
370
398
|
|
|
371
399
|
See [CHANGELOG.md](CHANGELOG.md) for what's new.
|
|
372
400
|
|
package/bin/gyoshu.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gyoshu CLI - Install/uninstall helper for OpenCode
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* bunx gyoshu install - Add gyoshu to opencode.json
|
|
8
|
+
* bunx gyoshu uninstall - Remove gyoshu from opencode.json
|
|
9
|
+
* bunx gyoshu check - Verify installation status
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
|
|
15
|
+
const OPENCODE_CONFIG = 'opencode.json';
|
|
16
|
+
|
|
17
|
+
function findOpencodeConfig() {
|
|
18
|
+
// Check current directory
|
|
19
|
+
if (existsSync(OPENCODE_CONFIG)) {
|
|
20
|
+
return OPENCODE_CONFIG;
|
|
21
|
+
}
|
|
22
|
+
// Check home directory
|
|
23
|
+
const homeConfig = join(process.env.HOME || '', OPENCODE_CONFIG);
|
|
24
|
+
if (existsSync(homeConfig)) {
|
|
25
|
+
return homeConfig;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readConfig(configPath) {
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
33
|
+
return JSON.parse(content);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeConfig(configPath, config) {
|
|
40
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function install() {
|
|
44
|
+
let configPath = findOpencodeConfig();
|
|
45
|
+
let config;
|
|
46
|
+
|
|
47
|
+
if (configPath) {
|
|
48
|
+
config = readConfig(configPath);
|
|
49
|
+
if (!config) {
|
|
50
|
+
console.error(`Error: Could not parse ${configPath}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
// Create new config in current directory
|
|
55
|
+
configPath = OPENCODE_CONFIG;
|
|
56
|
+
config = {};
|
|
57
|
+
console.log(`Creating new ${OPENCODE_CONFIG}...`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Ensure plugin array exists
|
|
61
|
+
if (!Array.isArray(config.plugin)) {
|
|
62
|
+
config.plugin = [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if already installed
|
|
66
|
+
if (config.plugin.includes('gyoshu')) {
|
|
67
|
+
console.log('Gyoshu is already installed in ' + configPath);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Add gyoshu
|
|
72
|
+
config.plugin.push('gyoshu');
|
|
73
|
+
writeConfig(configPath, config);
|
|
74
|
+
|
|
75
|
+
console.log('Gyoshu installed successfully!');
|
|
76
|
+
console.log(`Updated: ${configPath}`);
|
|
77
|
+
console.log('\nNext steps:');
|
|
78
|
+
console.log(' 1. Start OpenCode: opencode');
|
|
79
|
+
console.log(' 2. Run: /gyoshu doctor');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function uninstall() {
|
|
83
|
+
const configPath = findOpencodeConfig();
|
|
84
|
+
|
|
85
|
+
if (!configPath) {
|
|
86
|
+
console.log('No opencode.json found. Nothing to uninstall.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const config = readConfig(configPath);
|
|
91
|
+
if (!config) {
|
|
92
|
+
console.error(`Error: Could not parse ${configPath}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!Array.isArray(config.plugin) || !config.plugin.includes('gyoshu')) {
|
|
97
|
+
console.log('Gyoshu is not installed.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Remove gyoshu
|
|
102
|
+
config.plugin = config.plugin.filter(p => p !== 'gyoshu');
|
|
103
|
+
writeConfig(configPath, config);
|
|
104
|
+
|
|
105
|
+
console.log('Gyoshu uninstalled successfully!');
|
|
106
|
+
console.log(`Updated: ${configPath}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function check() {
|
|
110
|
+
const configPath = findOpencodeConfig();
|
|
111
|
+
|
|
112
|
+
if (!configPath) {
|
|
113
|
+
console.log('Status: NOT INSTALLED');
|
|
114
|
+
console.log('No opencode.json found.');
|
|
115
|
+
console.log('\nTo install: bunx gyoshu install');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const config = readConfig(configPath);
|
|
120
|
+
if (!config) {
|
|
121
|
+
console.log('Status: ERROR');
|
|
122
|
+
console.log(`Could not parse ${configPath}`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isInstalled = Array.isArray(config.plugin) && config.plugin.includes('gyoshu');
|
|
127
|
+
|
|
128
|
+
if (isInstalled) {
|
|
129
|
+
console.log('Status: INSTALLED');
|
|
130
|
+
console.log(`Config: ${configPath}`);
|
|
131
|
+
console.log('\nTo verify in OpenCode: /gyoshu doctor');
|
|
132
|
+
} else {
|
|
133
|
+
console.log('Status: NOT INSTALLED');
|
|
134
|
+
console.log(`Config exists: ${configPath}`);
|
|
135
|
+
console.log('\nTo install: bunx gyoshu install');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function showHelp() {
|
|
140
|
+
console.log(`
|
|
141
|
+
Gyoshu - Scientific Research Agent for OpenCode
|
|
142
|
+
|
|
143
|
+
Usage:
|
|
144
|
+
gyoshu install Add gyoshu to opencode.json
|
|
145
|
+
gyoshu uninstall Remove gyoshu from opencode.json
|
|
146
|
+
gyoshu check Verify installation status
|
|
147
|
+
gyoshu help Show this help message
|
|
148
|
+
|
|
149
|
+
Examples:
|
|
150
|
+
bunx gyoshu install
|
|
151
|
+
npx gyoshu check
|
|
152
|
+
|
|
153
|
+
More info: https://github.com/Yeachan-Heo/My-Jogyo
|
|
154
|
+
`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Main
|
|
158
|
+
const command = process.argv[2];
|
|
159
|
+
|
|
160
|
+
switch (command) {
|
|
161
|
+
case 'install':
|
|
162
|
+
install();
|
|
163
|
+
break;
|
|
164
|
+
case 'uninstall':
|
|
165
|
+
uninstall();
|
|
166
|
+
break;
|
|
167
|
+
case 'check':
|
|
168
|
+
check();
|
|
169
|
+
break;
|
|
170
|
+
case 'help':
|
|
171
|
+
case '--help':
|
|
172
|
+
case '-h':
|
|
173
|
+
showHelp();
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
if (command) {
|
|
177
|
+
console.error(`Unknown command: ${command}\n`);
|
|
178
|
+
}
|
|
179
|
+
showHelp();
|
|
180
|
+
process.exit(command ? 1 : 0);
|
|
181
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gyoshu",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Scientific research agent extension for OpenCode - turns research goals into reproducible Jupyter notebooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.ts"
|
|
9
9
|
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"gyoshu": "./bin/gyoshu.js"
|
|
12
|
+
},
|
|
10
13
|
"files": [
|
|
11
|
-
"src/"
|
|
14
|
+
"src/",
|
|
15
|
+
"bin/"
|
|
12
16
|
],
|
|
13
17
|
"scripts": {
|
|
14
18
|
"test": "bun test ./tests",
|
|
@@ -50,9 +54,11 @@
|
|
|
50
54
|
},
|
|
51
55
|
"devDependencies": {
|
|
52
56
|
"@types/node": "^20.0.0",
|
|
57
|
+
"@types/sanitize-html": "^2.16.0",
|
|
53
58
|
"bun-types": "latest"
|
|
54
59
|
},
|
|
55
60
|
"dependencies": {
|
|
61
|
+
"sanitize-html": "^2.17.0",
|
|
56
62
|
"zod": "^3.23.0"
|
|
57
63
|
}
|
|
58
64
|
}
|
|
Binary file
|
|
@@ -37,7 +37,7 @@ import argparse
|
|
|
37
37
|
import socket as socket_module
|
|
38
38
|
import stat
|
|
39
39
|
from datetime import datetime, timezone
|
|
40
|
-
from typing import Any, Dict, List, Optional, Callable
|
|
40
|
+
from typing import Any, Dict, List, Optional, Callable, Tuple
|
|
41
41
|
|
|
42
42
|
# =============================================================================
|
|
43
43
|
# STREAM SEPARATION
|
|
@@ -78,11 +78,10 @@ def send_response(
|
|
|
78
78
|
id: Optional[str], result: Optional[Dict] = None, error: Optional[Dict] = None
|
|
79
79
|
) -> None:
|
|
80
80
|
"""Send JSON-RPC 2.0 response via protocol channel."""
|
|
81
|
-
response: Dict[str, Any] = {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
response["id"] = id
|
|
81
|
+
response: Dict[str, Any] = {
|
|
82
|
+
"jsonrpc": JSON_RPC_VERSION,
|
|
83
|
+
"id": id,
|
|
84
|
+
}
|
|
86
85
|
|
|
87
86
|
if error is not None:
|
|
88
87
|
response["error"] = error
|
|
@@ -247,6 +246,66 @@ def parse_markers(text: str) -> List[Dict[str, Any]]:
|
|
|
247
246
|
return markers
|
|
248
247
|
|
|
249
248
|
|
|
249
|
+
# =============================================================================
|
|
250
|
+
# BOUNDED STRING IO (FIX-158: Prevent memory exhaustion)
|
|
251
|
+
# =============================================================================
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _get_max_capture_chars() -> int:
|
|
255
|
+
"""Safely parse GYOSHU_MAX_CAPTURE_CHARS env var with validation.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Capture limit in characters, clamped to [10KB, 100MB] range.
|
|
259
|
+
Default: 1MB (1048576 chars).
|
|
260
|
+
"""
|
|
261
|
+
default = 1048576 # 1MB
|
|
262
|
+
env_val = os.getenv("GYOSHU_MAX_CAPTURE_CHARS", "")
|
|
263
|
+
# FIX-167: Guard against DoS via extremely long numeric strings
|
|
264
|
+
# 20 digits is plenty for any sane value (10^20 > 100MB limit anyway)
|
|
265
|
+
if len(env_val) > 20:
|
|
266
|
+
return default
|
|
267
|
+
if not env_val:
|
|
268
|
+
return default
|
|
269
|
+
try:
|
|
270
|
+
val = int(env_val)
|
|
271
|
+
# Clamp to sane range: 10KB min, 100MB max
|
|
272
|
+
return max(10240, min(val, 104857600))
|
|
273
|
+
except (ValueError, TypeError):
|
|
274
|
+
return default
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
MAX_CAPTURE_CHARS = _get_max_capture_chars()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class BoundedStringIO:
|
|
281
|
+
"""StringIO wrapper that caps capture size to prevent memory exhaustion."""
|
|
282
|
+
|
|
283
|
+
def __init__(self, limit: int = MAX_CAPTURE_CHARS):
|
|
284
|
+
self._buffer = io.StringIO()
|
|
285
|
+
self._limit = limit
|
|
286
|
+
self._truncated = False
|
|
287
|
+
|
|
288
|
+
def write(self, s: str) -> int:
|
|
289
|
+
if self._truncated:
|
|
290
|
+
return len(s)
|
|
291
|
+
current = self._buffer.tell()
|
|
292
|
+
if current + len(s) > self._limit:
|
|
293
|
+
remaining = self._limit - current
|
|
294
|
+
if remaining > 0:
|
|
295
|
+
self._buffer.write(s[:remaining])
|
|
296
|
+
self._buffer.write("\n...[truncated]")
|
|
297
|
+
self._truncated = True
|
|
298
|
+
return len(s)
|
|
299
|
+
return self._buffer.write(s)
|
|
300
|
+
|
|
301
|
+
def getvalue(self) -> str:
|
|
302
|
+
return self._buffer.getvalue()
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def truncated(self) -> bool:
|
|
306
|
+
return self._truncated
|
|
307
|
+
|
|
308
|
+
|
|
250
309
|
# =============================================================================
|
|
251
310
|
# MEMORY UTILITIES
|
|
252
311
|
# =============================================================================
|
|
@@ -401,13 +460,15 @@ def execute_code(
|
|
|
401
460
|
Returns:
|
|
402
461
|
Dict with success, stdout, stderr, exception info
|
|
403
462
|
"""
|
|
404
|
-
stdout_capture =
|
|
405
|
-
stderr_capture =
|
|
463
|
+
stdout_capture = BoundedStringIO()
|
|
464
|
+
stderr_capture = BoundedStringIO()
|
|
406
465
|
|
|
407
466
|
result = {
|
|
408
467
|
"success": False,
|
|
409
468
|
"stdout": "",
|
|
410
469
|
"stderr": "",
|
|
470
|
+
"stdout_truncated": False,
|
|
471
|
+
"stderr_truncated": False,
|
|
411
472
|
"exception": None,
|
|
412
473
|
"exception_type": None,
|
|
413
474
|
"traceback": None,
|
|
@@ -457,7 +518,6 @@ def execute_code(
|
|
|
457
518
|
)
|
|
458
519
|
|
|
459
520
|
finally:
|
|
460
|
-
# Cancel timeout
|
|
461
521
|
if timeout and hasattr(signal, "SIGALRM"):
|
|
462
522
|
signal.alarm(0)
|
|
463
523
|
if old_handler is not None:
|
|
@@ -465,6 +525,8 @@ def execute_code(
|
|
|
465
525
|
|
|
466
526
|
result["stdout"] = stdout_capture.getvalue()
|
|
467
527
|
result["stderr"] = stderr_capture.getvalue()
|
|
528
|
+
result["stdout_truncated"] = stdout_capture.truncated
|
|
529
|
+
result["stderr_truncated"] = stderr_capture.truncated
|
|
468
530
|
|
|
469
531
|
return result
|
|
470
532
|
|
|
@@ -526,6 +588,8 @@ def handle_execute(id: str, params: Dict[str, Any]) -> None:
|
|
|
526
588
|
"success": exec_result["success"],
|
|
527
589
|
"stdout": exec_result["stdout"],
|
|
528
590
|
"stderr": exec_result["stderr"],
|
|
591
|
+
"stdout_truncated": exec_result.get("stdout_truncated", False),
|
|
592
|
+
"stderr_truncated": exec_result.get("stderr_truncated", False),
|
|
529
593
|
"markers": markers,
|
|
530
594
|
"artifacts": [], # TODO: Artifact detection from namespace
|
|
531
595
|
"timing": {
|
|
@@ -589,6 +653,38 @@ HANDLERS: Dict[str, Callable[[str, Dict[str, Any]], None]] = {
|
|
|
589
653
|
# MAIN LOOP
|
|
590
654
|
# =============================================================================
|
|
591
655
|
|
|
656
|
+
# FIX-173: Cap JSON-RPC request line size to prevent DoS
|
|
657
|
+
MAX_REQUEST_LINE_BYTES = 10 * 1024 * 1024 # 10MB
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def read_bounded_line(stream, max_bytes: int) -> Tuple[Optional[bytes], bool]:
|
|
661
|
+
"""Read a line with bounded byte count.
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
Tuple of (line_bytes or None if EOF, was_oversized)
|
|
665
|
+
- If EOF with no data: (None, False)
|
|
666
|
+
- If line fits in limit: (bytes, False)
|
|
667
|
+
- If line exceeded limit: (truncated_bytes, True) - already drained to newline
|
|
668
|
+
"""
|
|
669
|
+
data = bytearray()
|
|
670
|
+
while len(data) < max_bytes:
|
|
671
|
+
char = stream.read(1)
|
|
672
|
+
if not char:
|
|
673
|
+
# EOF - return what we have
|
|
674
|
+
return (bytes(data) if data else None, False)
|
|
675
|
+
if char == b"\n":
|
|
676
|
+
# Normal line termination
|
|
677
|
+
return (bytes(data), False)
|
|
678
|
+
data.extend(char)
|
|
679
|
+
|
|
680
|
+
# Limit exceeded - drain rest of line (already consumes up to and including \n)
|
|
681
|
+
while True:
|
|
682
|
+
char = stream.read(1)
|
|
683
|
+
if not char or char == b"\n":
|
|
684
|
+
break
|
|
685
|
+
# Return truncated data with oversized flag - no peek needed by caller
|
|
686
|
+
return (bytes(data[:max_bytes]), True)
|
|
687
|
+
|
|
592
688
|
|
|
593
689
|
def process_request(line: str) -> None:
|
|
594
690
|
"""Parse and handle a single JSON-RPC request."""
|
|
@@ -763,9 +859,19 @@ def handle_socket_connection(conn: socket_module.socket) -> None:
|
|
|
763
859
|
try:
|
|
764
860
|
_protocol_out = conn.makefile("w", buffering=1, encoding="utf-8")
|
|
765
861
|
|
|
766
|
-
reader = conn.makefile("
|
|
767
|
-
|
|
768
|
-
|
|
862
|
+
reader = conn.makefile("rb")
|
|
863
|
+
while True:
|
|
864
|
+
line_bytes, was_oversized = read_bounded_line(
|
|
865
|
+
reader, MAX_REQUEST_LINE_BYTES
|
|
866
|
+
)
|
|
867
|
+
if line_bytes is None:
|
|
868
|
+
break
|
|
869
|
+
if was_oversized:
|
|
870
|
+
send_response(
|
|
871
|
+
None, error=make_error(ERROR_INVALID_REQUEST, "Request too large")
|
|
872
|
+
)
|
|
873
|
+
continue
|
|
874
|
+
line = line_bytes.decode("utf-8", errors="replace").strip()
|
|
769
875
|
if not line:
|
|
770
876
|
continue
|
|
771
877
|
process_request(line)
|
|
@@ -789,8 +895,18 @@ def run_stdin_mode() -> None:
|
|
|
789
895
|
sys.stderr.flush()
|
|
790
896
|
|
|
791
897
|
try:
|
|
792
|
-
|
|
793
|
-
|
|
898
|
+
while True:
|
|
899
|
+
line_bytes, was_oversized = read_bounded_line(
|
|
900
|
+
sys.stdin.buffer, MAX_REQUEST_LINE_BYTES
|
|
901
|
+
)
|
|
902
|
+
if line_bytes is None:
|
|
903
|
+
break
|
|
904
|
+
if was_oversized:
|
|
905
|
+
send_response(
|
|
906
|
+
None, error=make_error(ERROR_INVALID_REQUEST, "Request too large")
|
|
907
|
+
)
|
|
908
|
+
continue
|
|
909
|
+
line = line_bytes.decode("utf-8", errors="replace").strip()
|
|
794
910
|
if not line:
|
|
795
911
|
continue
|
|
796
912
|
|