push from SNAPKITTYWEST/sovereign-engine-v2
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +12 -0
- .gitignore +21 -0
- ARCHITECTURE.md +206 -0
- LICENSE.tri +48 -0
- PRODUCTION_HARDENING.md +2149 -0
- README.md +511 -0
- SPRINT_PLAN.md +71 -0
- docs/CONFIGURATION.md +391 -0
- docs/GETTING_STARTED.md +242 -0
- ide/CMakeLists.txt +92 -0
- ide/native/bridge/bridge.h +140 -0
- ide/native/bridge/bridge_http.c +485 -0
- ide/native/bridge/example.c +86 -0
- ide/native/bridge/test_keys.c +90 -0
- ide/native/bridge/test_tools.c +84 -0
- ide/native/chat/pipe_client.c +105 -0
- ide/native/chat/pipe_client.h +18 -0
- ide/native/chat/protocol.c +17 -0
- ide/native/chat/protocol.h +42 -0
- ide/native/core/arena.c +68 -0
- ide/native/core/arena.h +28 -0
- ide/native/core/errors.h +27 -0
- ide/native/core/events.c +51 -0
- ide/native/core/events.h +49 -0
- ide/native/core/strings.c +73 -0
- ide/native/core/strings.h +22 -0
- ide/native/core/threading.h +31 -0
- ide/native/editor/buffer.c +482 -0
- ide/native/editor/buffer.h +27 -0
- ide/native/editor/document.c +151 -0
- ide/native/editor/document.h +26 -0
- ide/native/editor/editor_view.cpp +613 -0
- ide/native/editor/editor_view.h +27 -0
- ide/native/fcl/evaluator.c +54 -0
- ide/native/fcl/evaluator.h +22 -0
- ide/native/fcl/parser.h +44 -0
- ide/native/git/repository.c +101 -0
- ide/native/git/repository.h +20 -0
- ide/native/graphics/d2d_renderer.cpp +90 -0
- ide/native/graphics/d2d_renderer.h +17 -0
- ide/native/lsp/client.c +422 -0
- ide/native/lsp/client.h +40 -0
- ide/native/main.c +25 -0
- ide/native/platform/windows/application.c +70 -0
- ide/native/platform/windows/application.h +15 -0
- ide/native/platform/windows/shell.c +681 -0
- ide/native/platform/windows/shell.h +9 -0
- ide/native/platform/windows/window.c +175 -0
- ide/native/platform/windows/window.h +10 -0
- ide/native/terminal/conpty.c +149 -0
.env.example
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SOVEREIGN ENGINE CONFIGURATION
|
| 2 |
+
# Copy this to .env and add your keys
|
| 3 |
+
|
| 4 |
+
# OpenRouter API Key (free credits - rotate daily)
|
| 5 |
+
# Get from: https://openrouter.ai/keys
|
| 6 |
+
OPENROUTER_API_KEY=sk-or-v1-YOUR_KEY_HERE
|
| 7 |
+
|
| 8 |
+
# Ollama API Key (if using cloud Ollama, otherwise leave blank for local)
|
| 9 |
+
OLLAMA_API_KEY=
|
| 10 |
+
|
| 11 |
+
# Ollama Base URL (default: local)
|
| 12 |
+
OLLAMA_BASE_URL=http://localhost:11434
|
.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
*.egg-info/
|
| 5 |
+
dist/
|
| 6 |
+
build/
|
| 7 |
+
.eggs/
|
| 8 |
+
*.so
|
| 9 |
+
*.o
|
| 10 |
+
*.obj
|
| 11 |
+
.env
|
| 12 |
+
.env.local
|
| 13 |
+
*.worm
|
| 14 |
+
*.bin
|
| 15 |
+
/checkpoints/
|
| 16 |
+
/continuity/
|
| 17 |
+
.vscode/
|
| 18 |
+
.idea/
|
| 19 |
+
sovereign_gate/authority_sk.pem
|
| 20 |
+
sovereign_gate/.node_sk
|
| 21 |
+
sovereign_gate/*.pem.backup
|
ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SOVEREIGN PYTHON LLM ENGINE
|
| 2 |
+
|
| 3 |
+
**A production-grade LLM agent framework with custom MoE routing, binary WORM storage, four-paradigm continuity, and native IPC — generated from a single DSL prompt.**
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## Origin
|
| 8 |
+
|
| 9 |
+
This entire codebase was generated from scratch in a single prompt using the **HyperKittyConstraintDSL** — a deterministic constraint specification that orchestrates multi-agent code generation with proof-backed output.
|
| 10 |
+
|
| 11 |
+
One DSL. One prompt. Three coordinated agents. **16,784 lines of machine code.**
|
| 12 |
+
|
| 13 |
+
```xml
|
| 14 |
+
<HyperKittyConstraintDSL version="1.0">
|
| 15 |
+
<Meta>
|
| 16 |
+
<System>HK-OS</System>
|
| 17 |
+
<Mode>DETERMINISTIC-CONSTRAINT-BUILD</Mode>
|
| 18 |
+
<Output>PROOF_BACKED_ARTIFACT</Output>
|
| 19 |
+
</Meta>
|
| 20 |
+
|
| 21 |
+
<BooleanKernel>
|
| 22 |
+
<Primitive name="NAND">NAND(a,b)=1-ab</Primitive>
|
| 23 |
+
<Derived name="NOT">NAND(x,x)</Derived>
|
| 24 |
+
<Derived name="AND">NAND(NAND(a,b),NAND(a,b))</Derived>
|
| 25 |
+
<Derived name="OR">NAND(NAND(a,a),NAND(b,b))</Derived>
|
| 26 |
+
<Derived name="IMPLIES">OR(NOT(a),b)</Derived>
|
| 27 |
+
<Derived name="EQUAL">AND(IMPLIES(a,b),IMPLIES(b,a))</Derived>
|
| 28 |
+
</BooleanKernel>
|
| 29 |
+
|
| 30 |
+
<GlyphTypeSystem>
|
| 31 |
+
<Unit symbol="🧠" name="Cognition"/>
|
| 32 |
+
<Unit symbol="📚" name="Knowledge"/>
|
| 33 |
+
<Unit symbol="🔍" name="Search"/>
|
| 34 |
+
<Unit symbol="⚙" name="Transformation"/>
|
| 35 |
+
<Unit symbol="⚖" name="Constraint"/>
|
| 36 |
+
<Unit symbol="🔐" name="Proof"/>
|
| 37 |
+
<Unit symbol="🌐" name="Interface"/>
|
| 38 |
+
</GlyphTypeSystem>
|
| 39 |
+
|
| 40 |
+
<AgentModel>
|
| 41 |
+
<Invariant>active(I) => trusted(I)</Invariant>
|
| 42 |
+
<Invariant>entropy(I) <= 0.20</Invariant>
|
| 43 |
+
</AgentModel>
|
| 44 |
+
|
| 45 |
+
<QuantumConstraintLayer>
|
| 46 |
+
<Entropy>
|
| 47 |
+
<Bound>H <= 0.20</Bound>
|
| 48 |
+
</Entropy>
|
| 49 |
+
</QuantumConstraintLayer>
|
| 50 |
+
|
| 51 |
+
<ProofOutput>
|
| 52 |
+
<ProofStatus>PROOF_TRUE</ProofStatus>
|
| 53 |
+
</ProofOutput>
|
| 54 |
+
|
| 55 |
+
<FinalState>
|
| 56 |
+
<Artifact>PYTHON_C_BRIDGE_IR</Artifact>
|
| 57 |
+
<BinaryOutput>BBSTRING_MACHINEC_CODE</BinaryOutput>
|
| 58 |
+
<Orchestration>THREE_AGENT_WORKFLOW</Orchestration>
|
| 59 |
+
</FinalState>
|
| 60 |
+
</HyperKittyConstraintDSL>
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
The DSL defines:
|
| 64 |
+
- **Boolean kernel** — All logic gates derived from NAND (universal gate)
|
| 65 |
+
- **Glyph type system** — Semantic types for agent roles
|
| 66 |
+
- **Agent invariants** — Trust and entropy constraints enforced at build time
|
| 67 |
+
- **DAG structure** — Acyclic routing graph validated before code generation
|
| 68 |
+
- **Proof output** — Cryptographic hash commitment over all artifacts
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
## Architecture
|
| 73 |
+
|
| 74 |
+
```
|
| 75 |
+
┌─────────────────────────────────────────────────────────┐
|
| 76 |
+
│ SOVEREIGN ENGINE │
|
| 77 |
+
├──────────────┬──────────────┬───────────────────────────┤
|
| 78 |
+
│ Routing │ Continuity │ Machine Code Layer │
|
| 79 |
+
│ │ │ │
|
| 80 |
+
│ RegexParser │ EnvState │ BytecodeAssembler │
|
| 81 |
+
│ ASTBuilder │ SeedChain │ MarshalCodec │
|
| 82 |
+
│ SymbolicGr. │ InodeState │ BinaryIR │
|
| 83 |
+
│ JordanTrans │ SharedMem │ VMExecutor │
|
| 84 |
+
│ JacobianLen │ Manager │ MachineCodeGen (x86-64) │
|
| 85 |
+
│ Constraints │ │ CTypesBridge │
|
| 86 |
+
│ SparseActiv │ │ DSLValidator │
|
| 87 |
+
│ NANDFilter │ │ SovereignMachine │
|
| 88 |
+
│ Dispatch │ │ │
|
| 89 |
+
│ MergeOutput │ ├───────────────────────────┤
|
| 90 |
+
│ JordanMoE │ │ Native ASM (NASM x86-64) │
|
| 91 |
+
│ │ │ │
|
| 92 |
+
├──────────────┤ │ sovereign_runtime.asm │
|
| 93 |
+
│ Tools (34) │ │ qra_tensor.asm │
|
| 94 |
+
│ │ │ ipc_dispatcher.asm │
|
| 95 |
+
│ filesystem │ │ nand_kernel.asm │
|
| 96 |
+
│ code │ │ jordan_blocks.asm │
|
| 97 |
+
│ git │ │ entropy_gate.asm │
|
| 98 |
+
│ database │ │ sovereign_link.asm │
|
| 99 |
+
│ documents ├──────────────┤ │
|
| 100 |
+
│ web │ IPC Layer │ │
|
| 101 |
+
│ embeddings │ │ │
|
| 102 |
+
│ audio │ OpcodeReg │ │
|
| 103 |
+
│ pytorch │ IPCRouter │ │
|
| 104 |
+
│ │ ipc_core.c │ │
|
| 105 |
+
├──────────────┼──────────────┼───────────────────────────┤
|
| 106 |
+
│ Core │ Agents │ Daemon │
|
| 107 |
+
│ │ │ │
|
| 108 |
+
│ WORMFile │ ReActAgent │ PythonDaemon (TCP 19002) │
|
| 109 |
+
│ Checkpoint │ ShadowAgent │ Swarm (fan_out/race) │
|
| 110 |
+
│ Evidence │ Supervisor │ │
|
| 111 |
+
│ Storage │ │ │
|
| 112 |
+
└──────────────┴──────────────┴───────────────────────────┘
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
## Key Innovations
|
| 118 |
+
|
| 119 |
+
### Jordan Algebraic MoE Routing
|
| 120 |
+
|
| 121 |
+
Replaces softmax gating with Jordan algebra (Spin Factor) composition:
|
| 122 |
+
|
| 123 |
+
- **Non-associative** — Agent grouping topology changes routing output mathematically
|
| 124 |
+
- **Fixed-point convergence** — Jordan squaring converges to idempotents (stable routing attractors)
|
| 125 |
+
- **Spectral decomposition** — Provably unique expert assignment via eigenvalue separation
|
| 126 |
+
|
| 127 |
+
### Four-Paradigm Continuity
|
| 128 |
+
|
| 129 |
+
Agent state survives daemon restarts, crashes, and hot-reloads:
|
| 130 |
+
|
| 131 |
+
| Paradigm | Mechanism | Speed | Use Case |
|
| 132 |
+
|----------|-----------|-------|----------|
|
| 133 |
+
| Env bitmask | `os.environ` (64-bit) | Fast | Hot restart via `os.execv` |
|
| 134 |
+
| Seed chain | Blake2b derivation (24 bytes) | Fast | Deterministic replay |
|
| 135 |
+
| Inode flags | Zero-byte files (`stat()`) | Kernel | Boolean state gates |
|
| 136 |
+
| Shared memory | `ctypes` (4KB block) | RAM | Cross-process realtime |
|
| 137 |
+
|
| 138 |
+
### Binary WORM Storage
|
| 139 |
+
|
| 140 |
+
Append-only binary struct format — no JSON, no injection surface:
|
| 141 |
+
|
| 142 |
+
- 152-byte fixed headers (magic + version + type + timestamps + Blake2b hash + Ed25519 sig)
|
| 143 |
+
- Chain verification (each record hashes the previous)
|
| 144 |
+
- Zero text parsing — immune to JSONL injection, newline attacks, XXE
|
| 145 |
+
|
| 146 |
+
### Inverted AST with Payload Elimination
|
| 147 |
+
|
| 148 |
+
Routing tree is structurally inverted:
|
| 149 |
+
- Structural nodes (routing_weight=1.0) drive all decisions
|
| 150 |
+
- Payload leaves (routing_weight=0.0) cannot propagate upward
|
| 151 |
+
- Blocklist + XXE pattern detection at parse boundary
|
| 152 |
+
|
| 153 |
+
### Native IPC Multiplexer
|
| 154 |
+
|
| 155 |
+
Memory-mapped shared buffer with O(1) opcode dispatch:
|
| 156 |
+
|
| 157 |
+
- 34 static opcodes (tools) + dynamic assignment from 0x0100
|
| 158 |
+
- 50us polling loop (C core) or Python asyncio fallback
|
| 159 |
+
- Zero serialization — raw struct read/write via mmap
|
| 160 |
+
|
| 161 |
+
---
|
| 162 |
+
|
| 163 |
+
## Line Counts
|
| 164 |
+
|
| 165 |
+
| Component | Lines |
|
| 166 |
+
|-----------|-------|
|
| 167 |
+
| Python machine code layer | 9,251 |
|
| 168 |
+
| NASM x86-64 assembly | 7,019 |
|
| 169 |
+
| Routing pipeline | 1,900 |
|
| 170 |
+
| Continuity layer | 1,536 |
|
| 171 |
+
| Tool registration + IPC | 1,935 |
|
| 172 |
+
| Core (storage, evidence) | 570 |
|
| 173 |
+
| Agents (ReAct, Shadow, Supervisor) | 1,500 |
|
| 174 |
+
| Daemon + Retrieval | 1,500 |
|
| 175 |
+
| Sovereign machine wiring | 514 |
|
| 176 |
+
| **Total** | **~25,700** |
|
| 177 |
+
|
| 178 |
+
---
|
| 179 |
+
|
| 180 |
+
## Build Constraints
|
| 181 |
+
|
| 182 |
+
Every artifact in this repository satisfies:
|
| 183 |
+
|
| 184 |
+
```
|
| 185 |
+
ACTIVE(I) => TRUSTED(I) -- Trust axiom
|
| 186 |
+
ENTROPY(I) <= 0.20 -- Shannon bound (nats)
|
| 187 |
+
NAND truth table verified -- Boolean kernel complete
|
| 188 |
+
DAG acyclic -- No routing cycles
|
| 189 |
+
Glyph mapping injective -- No type collisions
|
| 190 |
+
ProofStatus = PROOF_TRUE -- All constraints pass
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
---
|
| 194 |
+
|
| 195 |
+
## Requirements
|
| 196 |
+
|
| 197 |
+
- Python 3.11+
|
| 198 |
+
- No external dependencies (pure stdlib)
|
| 199 |
+
- Optional: NASM for native assembly compilation
|
| 200 |
+
- Optional: C compiler for IPC core dispatcher
|
| 201 |
+
|
| 202 |
+
---
|
| 203 |
+
|
| 204 |
+
## License
|
| 205 |
+
|
| 206 |
+
Proprietary. Copyright SnapKitty / SNAPKITTYWEST.
|
LICENSE.tri
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TRI-LICENSE STRUCTURE
|
| 2 |
+
=====================
|
| 3 |
+
|
| 4 |
+
This project is available under THREE licensing options:
|
| 5 |
+
|
| 6 |
+
1. Business Source License 1.1 (BSL-1.1)
|
| 7 |
+
- Source-available with commercial restrictions
|
| 8 |
+
- No managed service offerings at enterprise scale
|
| 9 |
+
- Converts to AGPL-3.0 after transition period (Change Date: 2028-08-08)
|
| 10 |
+
|
| 11 |
+
2. GNU Affero General Public License v3.0 (AGPL-3.0)
|
| 12 |
+
- Strong network copyleft
|
| 13 |
+
- SaaS/network distribution triggers source disclosure
|
| 14 |
+
- All modifications must be AGPL-3.0
|
| 15 |
+
|
| 16 |
+
3. Mozilla Public License 2.0 (MPL-2.0) + Commercial Dual License
|
| 17 |
+
- Weak copyleft (file-level)
|
| 18 |
+
- Can combine with proprietary code
|
| 19 |
+
- Modified files must remain MPL-2.0
|
| 20 |
+
- Commercial license available for copyleft bypass
|
| 21 |
+
|
| 22 |
+
================================================================================
|
| 23 |
+
|
| 24 |
+
WHICH LICENSE APPLIES?
|
| 25 |
+
|
| 26 |
+
Use this engine itself to determine:
|
| 27 |
+
|
| 28 |
+
swipl -q -t halt -f license_policy.pl -- select <your_use_case>
|
| 29 |
+
|
| 30 |
+
Use cases:
|
| 31 |
+
- saas_wrapper → AGPL-3.0
|
| 32 |
+
- enterprise_restricted → BSL-1.1
|
| 33 |
+
- file_level_mod → MPL-2.0
|
| 34 |
+
- copyleft_bypass → Commercial
|
| 35 |
+
- open_source_redistribution → AGPL-3.0
|
| 36 |
+
|
| 37 |
+
================================================================================
|
| 38 |
+
|
| 39 |
+
COPYRIGHT HOLDER
|
| 40 |
+
|
| 41 |
+
Copyright (C) 2026 Ahmad Ali Parr
|
| 42 |
+
Bel Esprit D'Accord Irrevocable Trust
|
| 43 |
+
SnapKitty Collective Limited (FLP)
|
| 44 |
+
|
| 45 |
+
Contact: ahmedparr93@gmail.com
|
| 46 |
+
Web: https://github.com/SNAPKITTYWEST
|
| 47 |
+
|
| 48 |
+
================================================================================
|
PRODUCTION_HARDENING.md
ADDED
|
@@ -0,0 +1,2149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PRODUCTION HARDENING PLAN — SOVEREIGN PYTHON LLM ENGINE
|
| 2 |
+
|
| 3 |
+
**Target Audience:** Codex AI assistant (code implementation agent)
|
| 4 |
+
**Purpose:** Specification for hardening the Sovereign engine for production deployment
|
| 5 |
+
**Scope:** 8 major sections, ~200 actionable items
|
| 6 |
+
**Timeline Estimate:** 4-6 weeks at standard velocity
|
| 7 |
+
**Status:** Requirement specification (pre-implementation)
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## CRITICAL SUCCESS CRITERIA
|
| 12 |
+
|
| 13 |
+
All work must preserve or enhance these invariants:
|
| 14 |
+
|
| 15 |
+
```
|
| 16 |
+
✓ ACTIVE(I) ⟹ TRUSTED(I) — No untrusted agent execution
|
| 17 |
+
✓ ENTROPY(I) ≤ 0.20 nats — Determinism bound
|
| 18 |
+
✓ WORM append-only + tamper-detect — No log mutation
|
| 19 |
+
✓ PathJail + SSRFGuard hold all I/O — Zero filesystem/network escape
|
| 20 |
+
✓ ReAct max_steps enforced — Runaway prevention
|
| 21 |
+
✓ Tool approval + rate limit gates — Malicious tool calls blocked
|
| 22 |
+
✓ Continuity 4-paradigm restart cycle — No state loss on crash
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## 1. TERMINAL ACCESS IDE
|
| 28 |
+
|
| 29 |
+
### Goal
|
| 30 |
+
Rich terminal UI (pure Python curses) that displays live agent execution with routing visualization, tool latency, and continuity status.
|
| 31 |
+
|
| 32 |
+
### 1.1 Core Terminal UI (`src/terminal/ui.py` — ~500 lines)
|
| 33 |
+
|
| 34 |
+
**Deliverables:**
|
| 35 |
+
- Split-pane layout: left = agent output, right = tool/routing activity
|
| 36 |
+
- Live update loop (async, non-blocking)
|
| 37 |
+
- Event queue from agent → UI bridge
|
| 38 |
+
|
| 39 |
+
**Function Signatures:**
|
| 40 |
+
|
| 41 |
+
```python
|
| 42 |
+
class TerminalUI:
|
| 43 |
+
"""Rich terminal interface for Sovereign engine."""
|
| 44 |
+
|
| 45 |
+
def __init__(self, width: int = 200, height: int = 50):
|
| 46 |
+
"""Initialize curses screen and panes."""
|
| 47 |
+
pass
|
| 48 |
+
|
| 49 |
+
async def run(self, agent: ReActAgent) -> None:
|
| 50 |
+
"""Main loop: poll agent, update panes, handle input."""
|
| 51 |
+
pass
|
| 52 |
+
|
| 53 |
+
def render_left_pane(self, trajectory: AgentTrajectory) -> None:
|
| 54 |
+
"""
|
| 55 |
+
Display agent thought/action/observation stream.
|
| 56 |
+
|
| 57 |
+
Format per step:
|
| 58 |
+
[STEP 3]
|
| 59 |
+
💭 Thought: "Need to search for recent frameworks..."
|
| 60 |
+
⚙️ Action: tool_call(web_search, {"query": "LLM frameworks 2026"})
|
| 61 |
+
👁️ Observation: "Found 14 results..."
|
| 62 |
+
✅ Reflection: "Progress made, continue"
|
| 63 |
+
"""
|
| 64 |
+
pass
|
| 65 |
+
|
| 66 |
+
def render_right_pane(
|
| 67 |
+
self,
|
| 68 |
+
routing_state: dict[str, float],
|
| 69 |
+
tool_latencies: dict[str, float],
|
| 70 |
+
continuity_status: dict[str, Any]
|
| 71 |
+
) -> None:
|
| 72 |
+
"""
|
| 73 |
+
Display routing tree, tool performance, continuity flags.
|
| 74 |
+
|
| 75 |
+
Sections:
|
| 76 |
+
▶ Jordan MoE Routing
|
| 77 |
+
[Expert 0] weight=0.45 ████████░░
|
| 78 |
+
[Expert 1] weight=0.30 ██████░░░░
|
| 79 |
+
...
|
| 80 |
+
▶ Tool Performance (last 10 calls)
|
| 81 |
+
web_search avg=42ms max=105ms calls=8
|
| 82 |
+
db_query avg=15ms max=67ms calls=3
|
| 83 |
+
▶ Continuity Status
|
| 84 |
+
env_bitmask: 0x001f (seed restored)
|
| 85 |
+
shared_mem: OK (4095/4096 bytes)
|
| 86 |
+
worm_ledger: 127 records
|
| 87 |
+
"""
|
| 88 |
+
pass
|
| 89 |
+
|
| 90 |
+
def render_status_bar(self, current_step: int, max_steps: int) -> None:
|
| 91 |
+
"""Bottom status line: step count, elapsed time, agent state."""
|
| 92 |
+
pass
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
**Implementation Notes:**
|
| 96 |
+
- Use Python `curses` (stdlib, all platforms)
|
| 97 |
+
- Non-blocking input via `timeout(100)` (100ms refresh)
|
| 98 |
+
- Queue-based communication from agent to UI (thread-safe)
|
| 99 |
+
- WORM ledger tail displayed as a compact scrollable log
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
### 1.2 Reusable Curses Widgets (`src/terminal/widgets.py` — ~250 lines)
|
| 104 |
+
|
| 105 |
+
**Deliverables:**
|
| 106 |
+
- Pane container (split layout)
|
| 107 |
+
- Progress bar / bar chart
|
| 108 |
+
- Text scroller / log viewer
|
| 109 |
+
- Notification banner
|
| 110 |
+
|
| 111 |
+
**Class Signatures:**
|
| 112 |
+
|
| 113 |
+
```python
|
| 114 |
+
class Panel:
|
| 115 |
+
"""Named rectangular region with border and title."""
|
| 116 |
+
|
| 117 |
+
def __init__(self, x: int, y: int, width: int, height: int, title: str = ""):
|
| 118 |
+
pass
|
| 119 |
+
|
| 120 |
+
def write(self, x: int, y: int, text: str, color: int = 7) -> None:
|
| 121 |
+
"""Write text at (x, y) relative to panel origin."""
|
| 122 |
+
pass
|
| 123 |
+
|
| 124 |
+
def clear(self) -> None:
|
| 125 |
+
"""Erase all content."""
|
| 126 |
+
pass
|
| 127 |
+
|
| 128 |
+
def border(self, style: str = "box") -> None:
|
| 129 |
+
"""Draw border (box | rounded | none)."""
|
| 130 |
+
pass
|
| 131 |
+
|
| 132 |
+
def render(self, stdscr) -> None:
|
| 133 |
+
"""Write to curses screen."""
|
| 134 |
+
pass
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
class ProgressBar:
|
| 138 |
+
"""Horizontal bar showing percentage (0–100)."""
|
| 139 |
+
|
| 140 |
+
def __init__(self, width: int = 40, filled_char: str = "█", empty_char: str = "░"):
|
| 141 |
+
pass
|
| 142 |
+
|
| 143 |
+
def set_progress(self, value: float) -> None:
|
| 144 |
+
"""Update progress (0.0–1.0)."""
|
| 145 |
+
pass
|
| 146 |
+
|
| 147 |
+
def render(self) -> str:
|
| 148 |
+
"""Return string representation."""
|
| 149 |
+
pass
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class ScrollableLog:
|
| 153 |
+
"""Scrollable text buffer (vim-style navigation)."""
|
| 154 |
+
|
| 155 |
+
def __init__(self, max_lines: int = 1000):
|
| 156 |
+
pass
|
| 157 |
+
|
| 158 |
+
def append(self, line: str) -> None:
|
| 159 |
+
"""Add line to log."""
|
| 160 |
+
pass
|
| 161 |
+
|
| 162 |
+
def scroll(self, offset: int) -> None:
|
| 163 |
+
"""Scroll by offset lines (negative = up)."""
|
| 164 |
+
pass
|
| 165 |
+
|
| 166 |
+
def render(self, height: int) -> list[str]:
|
| 167 |
+
"""Return visible lines."""
|
| 168 |
+
pass
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
---
|
| 172 |
+
|
| 173 |
+
### 1.3 Vim-Style Keybindings (`src/terminal/keybinds.py` — ~150 lines)
|
| 174 |
+
|
| 175 |
+
**Deliverables:**
|
| 176 |
+
- Command mode (`:` prefix)
|
| 177 |
+
- Navigation (hjkl, Page Up/Down, gg/G)
|
| 178 |
+
- Pause/resume agent
|
| 179 |
+
|
| 180 |
+
**Function Signatures:**
|
| 181 |
+
|
| 182 |
+
```python
|
| 183 |
+
class KeybindRegistry:
|
| 184 |
+
"""Map terminal input to actions."""
|
| 185 |
+
|
| 186 |
+
def __init__(self):
|
| 187 |
+
self.bindings: dict[str, Callable] = {}
|
| 188 |
+
|
| 189 |
+
def register(self, key: str, action: Callable, mode: str = "normal") -> None:
|
| 190 |
+
"""
|
| 191 |
+
Register key binding.
|
| 192 |
+
|
| 193 |
+
Args:
|
| 194 |
+
key: Key code (e.g., "j", "Page_Down", "C-c")
|
| 195 |
+
action: Callable to invoke
|
| 196 |
+
mode: "normal" or "command"
|
| 197 |
+
"""
|
| 198 |
+
pass
|
| 199 |
+
|
| 200 |
+
def handle_input(self, ch: int, mode: str = "normal") -> tuple[str, Callable | None]:
|
| 201 |
+
"""
|
| 202 |
+
Convert curses input to action.
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
(mode, action_func) or (mode, None) if no binding
|
| 206 |
+
"""
|
| 207 |
+
pass
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
# Predefined bindings:
|
| 211 |
+
BINDINGS = {
|
| 212 |
+
"j": scroll_down,
|
| 213 |
+
"k": scroll_up,
|
| 214 |
+
"Page_Down": scroll_page_down,
|
| 215 |
+
"Page_Up": scroll_page_up,
|
| 216 |
+
"g": scroll_top,
|
| 217 |
+
"G": scroll_bottom,
|
| 218 |
+
"q": quit,
|
| 219 |
+
" ": pause_agent,
|
| 220 |
+
"r": resume_agent,
|
| 221 |
+
"C-c": emergency_stop,
|
| 222 |
+
}
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
---
|
| 226 |
+
|
| 227 |
+
## 2. ACCESSIBILITY
|
| 228 |
+
|
| 229 |
+
### Goal
|
| 230 |
+
All output (terminal, logs, errors) must support screen readers, high-contrast mode, and dyslexia-friendly rendering. Keyboard-only navigation. No color dependency.
|
| 231 |
+
|
| 232 |
+
### 2.1 Screen Reader Support (`src/accessibility/screen_reader.py` — ~200 lines)
|
| 233 |
+
|
| 234 |
+
**Deliverables:**
|
| 235 |
+
- Semantic text descriptions for all UI elements
|
| 236 |
+
- Status change announcements
|
| 237 |
+
- Error message plain English
|
| 238 |
+
|
| 239 |
+
**Function Signatures:**
|
| 240 |
+
|
| 241 |
+
```python
|
| 242 |
+
class AccessibilityBridge:
|
| 243 |
+
"""Convert UI events to screen-reader-friendly text."""
|
| 244 |
+
|
| 245 |
+
def __init__(self, output_file: Path | None = None):
|
| 246 |
+
"""
|
| 247 |
+
Args:
|
| 248 |
+
output_file: Optional file to log accessibility text.
|
| 249 |
+
If None, print to stdout for screen reader capture.
|
| 250 |
+
"""
|
| 251 |
+
pass
|
| 252 |
+
|
| 253 |
+
def announce(self, text: str, priority: str = "normal") -> None:
|
| 254 |
+
"""
|
| 255 |
+
Announce text immediately.
|
| 256 |
+
|
| 257 |
+
Args:
|
| 258 |
+
text: Plain English description (no emoji, no color codes)
|
| 259 |
+
priority: "normal" (queue) | "alert" (immediate) | "polite" (deferred)
|
| 260 |
+
|
| 261 |
+
Example:
|
| 262 |
+
announce("Agent entered THINKING state. Step 3 of 10.")
|
| 263 |
+
announce("ERROR: Database query timeout. Retry with longer timeout.", priority="alert")
|
| 264 |
+
"""
|
| 265 |
+
pass
|
| 266 |
+
|
| 267 |
+
def format_tool_call(self, tool_name: str, args: dict[str, Any]) -> str:
|
| 268 |
+
"""
|
| 269 |
+
Convert tool call to descriptive text.
|
| 270 |
+
|
| 271 |
+
Example output:
|
| 272 |
+
"Called web_search with query: 'LLM frameworks 2026', max_results: 10"
|
| 273 |
+
"""
|
| 274 |
+
pass
|
| 275 |
+
|
| 276 |
+
def format_routing_decision(self, experts: dict[str, float]) -> str:
|
| 277 |
+
"""
|
| 278 |
+
Describe which experts were activated.
|
| 279 |
+
|
| 280 |
+
Example output:
|
| 281 |
+
"Routing decision: Expert0 weighted 0.45, Expert1 weighted 0.30, others suppressed"
|
| 282 |
+
"""
|
| 283 |
+
pass
|
| 284 |
+
|
| 285 |
+
def format_error(self, error: Exception, recovery: str | None = None) -> str:
|
| 286 |
+
"""
|
| 287 |
+
Convert exception to plain English with recovery steps.
|
| 288 |
+
|
| 289 |
+
Example:
|
| 290 |
+
format_error(
|
| 291 |
+
PathJailError("Path escapes jail"),
|
| 292 |
+
recovery="Ensure file path is within /workspace"
|
| 293 |
+
)
|
| 294 |
+
# Output: "Error: Cannot access that file path. It is outside the allowed directory.
|
| 295 |
+
# Solution: Ensure file path is within /workspace"
|
| 296 |
+
"""
|
| 297 |
+
pass
|
| 298 |
+
```
|
| 299 |
+
|
| 300 |
+
---
|
| 301 |
+
|
| 302 |
+
### 2.2 Contrast & Dyslexia (`src/accessibility/contrast.py` — ~200 lines)
|
| 303 |
+
|
| 304 |
+
**Deliverables:**
|
| 305 |
+
- High-contrast color palette (WCAG AAA compatible)
|
| 306 |
+
- Dyslexia-friendly fonts (monospace, anti-aliased)
|
| 307 |
+
- No ambiguous characters (0/O, 1/l/I, 5/S confusion)
|
| 308 |
+
|
| 309 |
+
**Function Signatures:**
|
| 310 |
+
|
| 311 |
+
```python
|
| 312 |
+
class ContrastManager:
|
| 313 |
+
"""Ensure terminal colors meet accessibility standards."""
|
| 314 |
+
|
| 315 |
+
def __init__(self, mode: str = "normal"):
|
| 316 |
+
"""
|
| 317 |
+
Args:
|
| 318 |
+
mode: "normal" | "high_contrast" | "dyslexia_friendly"
|
| 319 |
+
"""
|
| 320 |
+
self.mode = mode
|
| 321 |
+
self.palette = self._init_palette()
|
| 322 |
+
|
| 323 |
+
def get_color_pair(self, role: str) -> tuple[int, int]:
|
| 324 |
+
"""
|
| 325 |
+
Get foreground, background curses color indices.
|
| 326 |
+
|
| 327 |
+
Roles:
|
| 328 |
+
"text" -> (fg, bg) for normal text
|
| 329 |
+
"thought" -> for agent thinking
|
| 330 |
+
"action" -> for tool calls
|
| 331 |
+
"observation" -> for tool results
|
| 332 |
+
"error" -> for errors
|
| 333 |
+
"success" -> for completed steps
|
| 334 |
+
"warning" -> for warnings
|
| 335 |
+
"status_active" -> for active components
|
| 336 |
+
"status_idle" -> for idle components
|
| 337 |
+
|
| 338 |
+
Returns:
|
| 339 |
+
(fg_color_code, bg_color_code) per curses
|
| 340 |
+
"""
|
| 341 |
+
pass
|
| 342 |
+
|
| 343 |
+
def sanitize_output(self, text: str) -> str:
|
| 344 |
+
"""
|
| 345 |
+
Remove ambiguous characters, add spacing.
|
| 346 |
+
|
| 347 |
+
Rules:
|
| 348 |
+
- Replace 0/O ambiguity: 0 → O (context-aware) or use distinct rendering
|
| 349 |
+
- Replace 1/l/I: use font distinguishing
|
| 350 |
+
- Increase line spacing in output
|
| 351 |
+
- Ensure monospace rendering
|
| 352 |
+
"""
|
| 353 |
+
pass
|
| 354 |
+
|
| 355 |
+
def validate_contrast(self, fg: int, bg: int) -> float:
|
| 356 |
+
"""
|
| 357 |
+
Return contrast ratio (1–21, target ≥7 for WCAG AAA).
|
| 358 |
+
|
| 359 |
+
Raises:
|
| 360 |
+
AccessibilityError if ratio < 7
|
| 361 |
+
"""
|
| 362 |
+
pass
|
| 363 |
+
```
|
| 364 |
+
|
| 365 |
+
---
|
| 366 |
+
|
| 367 |
+
### 2.3 Keyboard-Only Navigation
|
| 368 |
+
|
| 369 |
+
**Requirements (no implementation, validation only):**
|
| 370 |
+
- All UI features reachable via keyboard
|
| 371 |
+
- Tab order documented
|
| 372 |
+
- Vim keybindings supported (j/k/hjkl)
|
| 373 |
+
- Emergency stop (Ctrl+C) always works
|
| 374 |
+
- No mouse clicks required
|
| 375 |
+
|
| 376 |
+
**Test Cases:**
|
| 377 |
+
```python
|
| 378 |
+
# tests/accessibility/test_keyboard_nav.py
|
| 379 |
+
|
| 380 |
+
def test_tab_order():
|
| 381 |
+
"""Verify Tab cycles through all interactive elements."""
|
| 382 |
+
pass
|
| 383 |
+
|
| 384 |
+
def test_vim_navigation():
|
| 385 |
+
"""Verify j/k/hjkl/gg/G work without vim mode enabled."""
|
| 386 |
+
pass
|
| 387 |
+
|
| 388 |
+
def test_no_mouse_required():
|
| 389 |
+
"""Verify all features work with keyboard only."""
|
| 390 |
+
pass
|
| 391 |
+
```
|
| 392 |
+
|
| 393 |
+
---
|
| 394 |
+
|
| 395 |
+
## 3. EDGE CASES & ERROR HANDLING
|
| 396 |
+
|
| 397 |
+
### Goal
|
| 398 |
+
Gracefully handle all failure modes: disk full, corrupt files, path traversal attempts, DNS rebinding, hung tools, queue overflow, model timeouts, C frontend crash.
|
| 399 |
+
|
| 400 |
+
### 3.1 PathJail Extensions (`src/core/path_jail.py` — extend by ~100 lines)
|
| 401 |
+
|
| 402 |
+
**Add Functions:**
|
| 403 |
+
|
| 404 |
+
```python
|
| 405 |
+
def detect_junction(path: Path) -> bool:
|
| 406 |
+
"""
|
| 407 |
+
Detect Windows NTFS junction points (directory symlinks).
|
| 408 |
+
|
| 409 |
+
On Windows, junctions are not symlinks but still can escape jail.
|
| 410 |
+
Use Windows API reparse point detection.
|
| 411 |
+
|
| 412 |
+
Returns:
|
| 413 |
+
True if path is a junction, False otherwise
|
| 414 |
+
"""
|
| 415 |
+
pass
|
| 416 |
+
|
| 417 |
+
def safe_open_nofollow(path: Path, mode: str = 'r') -> io.IOBase:
|
| 418 |
+
"""
|
| 419 |
+
Open file with O_NOFOLLOW equivalent (no symlink dereference).
|
| 420 |
+
|
| 421 |
+
On POSIX: Use os.open with os.O_NOFOLLOW
|
| 422 |
+
On Windows: Check is_symlink() before opening
|
| 423 |
+
|
| 424 |
+
Raises:
|
| 425 |
+
PathJailError if file is a symlink
|
| 426 |
+
"""
|
| 427 |
+
pass
|
| 428 |
+
|
| 429 |
+
def prevent_toctou_race(path: Path, operation: Callable) -> Any:
|
| 430 |
+
"""
|
| 431 |
+
Execute operation on path with TOCTOU race prevention.
|
| 432 |
+
|
| 433 |
+
Pattern: Stat → resolve → stat-again → assert unchanged → execute
|
| 434 |
+
|
| 435 |
+
Args:
|
| 436 |
+
path: Path to operate on
|
| 437 |
+
operation: Callable(path) to execute
|
| 438 |
+
|
| 439 |
+
Returns:
|
| 440 |
+
Result of operation
|
| 441 |
+
|
| 442 |
+
Raises:
|
| 443 |
+
PathJailError if file changed between checks
|
| 444 |
+
"""
|
| 445 |
+
pass
|
| 446 |
+
```
|
| 447 |
+
|
| 448 |
+
**Add to PathJail class:**
|
| 449 |
+
|
| 450 |
+
```python
|
| 451 |
+
def set_max_file_size(self, max_bytes: int) -> None:
|
| 452 |
+
"""
|
| 453 |
+
Enforce max read size per operation.
|
| 454 |
+
|
| 455 |
+
Args:
|
| 456 |
+
max_bytes: Maximum bytes to read (e.g., 1GB)
|
| 457 |
+
|
| 458 |
+
Prevents: DoS by huge file read
|
| 459 |
+
"""
|
| 460 |
+
self.max_file_size = max_bytes
|
| 461 |
+
|
| 462 |
+
def read_safe(self, path: str | Path, max_size: int | None = None) -> bytes:
|
| 463 |
+
"""
|
| 464 |
+
Read file with size enforcement.
|
| 465 |
+
|
| 466 |
+
Checks available space before read.
|
| 467 |
+
|
| 468 |
+
Raises:
|
| 469 |
+
PathJailError if file exceeds max_size
|
| 470 |
+
"""
|
| 471 |
+
pass
|
| 472 |
+
```
|
| 473 |
+
|
| 474 |
+
---
|
| 475 |
+
|
| 476 |
+
### 3.2 SSRFGuard Extensions (`src/core/path_jail.py` — extend SSRFGuard by ~80 lines)
|
| 477 |
+
|
| 478 |
+
**Add Method:**
|
| 479 |
+
|
| 480 |
+
```python
|
| 481 |
+
class SSRFGuard:
|
| 482 |
+
"""Prevent Server-Side Request Forgery."""
|
| 483 |
+
|
| 484 |
+
async def resolve_and_check(self, hostname: str, port: int = 443) -> str:
|
| 485 |
+
"""
|
| 486 |
+
DNS rebinding prevention: resolve hostname, check IP, then connect.
|
| 487 |
+
|
| 488 |
+
Algorithm:
|
| 489 |
+
1. Resolve hostname to IP
|
| 490 |
+
2. Check IP against blocklist (private ranges, metadata, etc)
|
| 491 |
+
3. If OK, return IP
|
| 492 |
+
4. Return IP (not hostname) to caller for actual connection
|
| 493 |
+
5. On connect, verify IP matches resolved value
|
| 494 |
+
|
| 495 |
+
Args:
|
| 496 |
+
hostname: Domain name
|
| 497 |
+
port: Port (only used for validation, not connection)
|
| 498 |
+
|
| 499 |
+
Returns:
|
| 500 |
+
Verified IP address
|
| 501 |
+
|
| 502 |
+
Raises:
|
| 503 |
+
SSRFError if IP is blocked or doesn't resolve
|
| 504 |
+
|
| 505 |
+
Prevents:
|
| 506 |
+
- DNS rebinding: resolve → check → rebind → connect-to-different-IP
|
| 507 |
+
- Time-of-check-time-of-use: re-resolve before connect
|
| 508 |
+
"""
|
| 509 |
+
pass
|
| 510 |
+
|
| 511 |
+
def add_blocked_ip(self, ip: str) -> None:
|
| 512 |
+
"""Dynamically add blocked IP."""
|
| 513 |
+
pass
|
| 514 |
+
|
| 515 |
+
def is_blocked(self, ip: str) -> bool:
|
| 516 |
+
"""Check if IP is in blocklist."""
|
| 517 |
+
pass
|
| 518 |
+
```
|
| 519 |
+
|
| 520 |
+
---
|
| 521 |
+
|
| 522 |
+
### 3.3 Storage: Disk Full Handling (`src/core/storage.py` — extend by ~60 lines)
|
| 523 |
+
|
| 524 |
+
**Add Functions:**
|
| 525 |
+
|
| 526 |
+
```python
|
| 527 |
+
def check_disk_space_before_write(
|
| 528 |
+
file_path: Path,
|
| 529 |
+
estimated_size: int
|
| 530 |
+
) -> bool:
|
| 531 |
+
"""
|
| 532 |
+
Pre-check available disk space.
|
| 533 |
+
|
| 534 |
+
Args:
|
| 535 |
+
file_path: Target file path
|
| 536 |
+
estimated_size: Bytes to write
|
| 537 |
+
|
| 538 |
+
Returns:
|
| 539 |
+
True if space available, False if full
|
| 540 |
+
|
| 541 |
+
Raises:
|
| 542 |
+
OSError if path invalid
|
| 543 |
+
"""
|
| 544 |
+
pass
|
| 545 |
+
|
| 546 |
+
def write_with_space_check(
|
| 547 |
+
file_path: Path,
|
| 548 |
+
data: bytes,
|
| 549 |
+
min_free_space: int = 1_000_000 # 1MB minimum free
|
| 550 |
+
) -> None:
|
| 551 |
+
"""
|
| 552 |
+
Write to file, abort if disk nearly full.
|
| 553 |
+
|
| 554 |
+
Raises:
|
| 555 |
+
IOError if insufficient space
|
| 556 |
+
"""
|
| 557 |
+
pass
|
| 558 |
+
|
| 559 |
+
# Update WORMFile class:
|
| 560 |
+
class WORMFile:
|
| 561 |
+
def append_safe(self, record: WORMRecord) -> None:
|
| 562 |
+
"""
|
| 563 |
+
Append record with disk-full protection.
|
| 564 |
+
|
| 565 |
+
- Check space before write
|
| 566 |
+
- Write atomically (temp file + rename)
|
| 567 |
+
- On failure, truncate corrupt tail (not head)
|
| 568 |
+
"""
|
| 569 |
+
pass
|
| 570 |
+
```
|
| 571 |
+
|
| 572 |
+
---
|
| 573 |
+
|
| 574 |
+
### 3.4 Continuity: Shared Memory Cleanup (`src/continuity/shared_mem.py` — extend by ~100 lines)
|
| 575 |
+
|
| 576 |
+
**Add Functions:**
|
| 577 |
+
|
| 578 |
+
```python
|
| 579 |
+
def cleanup_stale_shm_segments(max_age_seconds: int = 3600) -> int:
|
| 580 |
+
"""
|
| 581 |
+
Clean up shared memory segments from dead processes.
|
| 582 |
+
|
| 583 |
+
Algorithm:
|
| 584 |
+
1. List all /dev/shm/* or Windows shared memory
|
| 585 |
+
2. Check mtime (modification time)
|
| 586 |
+
3. If older than max_age_seconds AND no process holding it, delete
|
| 587 |
+
4. Log deletion
|
| 588 |
+
|
| 589 |
+
Args:
|
| 590 |
+
max_age_seconds: Time threshold (default 1 hour)
|
| 591 |
+
|
| 592 |
+
Returns:
|
| 593 |
+
Number of segments cleaned
|
| 594 |
+
|
| 595 |
+
Raises:
|
| 596 |
+
OSError if cannot read /dev/shm
|
| 597 |
+
"""
|
| 598 |
+
pass
|
| 599 |
+
|
| 600 |
+
def detect_orphaned_shm(agent_id: str) -> bool:
|
| 601 |
+
"""
|
| 602 |
+
Detect if agent has orphaned shared memory from crash.
|
| 603 |
+
|
| 604 |
+
Returns:
|
| 605 |
+
True if orphaned SHM found, False if clean or in use
|
| 606 |
+
"""
|
| 607 |
+
pass
|
| 608 |
+
|
| 609 |
+
# Update SharedMemory class:
|
| 610 |
+
class SharedMemory:
|
| 611 |
+
def __init__(self, agent_id: str, size: int = 4096):
|
| 612 |
+
"""
|
| 613 |
+
Initialize shared memory with crash-recovery.
|
| 614 |
+
|
| 615 |
+
- Detect orphaned segment from prior crash
|
| 616 |
+
- Zero-out if corrupted
|
| 617 |
+
- Initialize checksum
|
| 618 |
+
"""
|
| 619 |
+
pass
|
| 620 |
+
|
| 621 |
+
def validate_checksum(self) -> bool:
|
| 622 |
+
"""
|
| 623 |
+
Verify ring buffer header checksum.
|
| 624 |
+
|
| 625 |
+
Returns:
|
| 626 |
+
True if checksum valid, False if corrupted
|
| 627 |
+
"""
|
| 628 |
+
pass
|
| 629 |
+
|
| 630 |
+
def recover_from_corruption(self) -> None:
|
| 631 |
+
"""
|
| 632 |
+
Reset SHM to safe state if checksum fails.
|
| 633 |
+
|
| 634 |
+
Logs recovery action.
|
| 635 |
+
"""
|
| 636 |
+
pass
|
| 637 |
+
```
|
| 638 |
+
|
| 639 |
+
---
|
| 640 |
+
|
| 641 |
+
### 3.5 IPC: Mmap Corruption Detection (`src/tools/ipc_router.py` — extend by ~80 lines)
|
| 642 |
+
|
| 643 |
+
**Add Functions:**
|
| 644 |
+
|
| 645 |
+
```python
|
| 646 |
+
def validate_ring_buffer_header(mmap_obj: mmap.mmap) -> tuple[bool, str]:
|
| 647 |
+
"""
|
| 648 |
+
Verify ring buffer header integrity.
|
| 649 |
+
|
| 650 |
+
Returns:
|
| 651 |
+
(is_valid, error_message)
|
| 652 |
+
|
| 653 |
+
Checks:
|
| 654 |
+
- Magic bytes at offset 0
|
| 655 |
+
- Version byte at offset 4
|
| 656 |
+
- CRC32 at offset 8
|
| 657 |
+
- Write pointer in valid range
|
| 658 |
+
- Read pointer in valid range
|
| 659 |
+
"""
|
| 660 |
+
pass
|
| 661 |
+
|
| 662 |
+
def repair_corrupted_ring_buffer(mmap_obj: mmap.mmap) -> None:
|
| 663 |
+
"""
|
| 664 |
+
Reset ring buffer to safe state.
|
| 665 |
+
|
| 666 |
+
- Zero-out entire header
|
| 667 |
+
- Reset pointers to 0
|
| 668 |
+
- Recalculate checksum
|
| 669 |
+
- Log repair event
|
| 670 |
+
"""
|
| 671 |
+
pass
|
| 672 |
+
|
| 673 |
+
# Update IPCRouter class:
|
| 674 |
+
class IPCRouter:
|
| 675 |
+
async def dispatch_with_corruption_check(self, opcode: int, payload: bytes) -> Any:
|
| 676 |
+
"""
|
| 677 |
+
Dispatch opcode, detecting and recovering from mmap corruption.
|
| 678 |
+
|
| 679 |
+
Raises:
|
| 680 |
+
IPCCorruptionError if corruption detected and unrecoverable
|
| 681 |
+
"""
|
| 682 |
+
pass
|
| 683 |
+
```
|
| 684 |
+
|
| 685 |
+
---
|
| 686 |
+
|
| 687 |
+
### 3.6 Routing: All-Experts-Masked Fallback (`src/routing/jordan_moe.py` — extend by ~40 lines)
|
| 688 |
+
|
| 689 |
+
**Add Method:**
|
| 690 |
+
|
| 691 |
+
```python
|
| 692 |
+
class JordanMoERouter:
|
| 693 |
+
def route_with_fallback(self, signal: dict[str, float]) -> dict[str, float]:
|
| 694 |
+
"""
|
| 695 |
+
Route signal to experts, with fallback if all masked.
|
| 696 |
+
|
| 697 |
+
Algorithm:
|
| 698 |
+
1. Compute routing normally
|
| 699 |
+
2. If all weights = 0 (all masked):
|
| 700 |
+
a. Log warning
|
| 701 |
+
b. Fall back to uniform random distribution
|
| 702 |
+
c. OR use prior distribution (if available)
|
| 703 |
+
d. Return fallback weights
|
| 704 |
+
|
| 705 |
+
Returns:
|
| 706 |
+
Expert → weight mapping (always sums to 1.0)
|
| 707 |
+
"""
|
| 708 |
+
pass
|
| 709 |
+
|
| 710 |
+
def get_current_masks(self) -> dict[str, bool]:
|
| 711 |
+
"""Return which experts are currently masked."""
|
| 712 |
+
pass
|
| 713 |
+
|
| 714 |
+
def set_emergency_fallback_mode(self, enabled: bool) -> None:
|
| 715 |
+
"""
|
| 716 |
+
Enable/disable emergency fallback routing.
|
| 717 |
+
|
| 718 |
+
When enabled and all experts masked, use uniform random.
|
| 719 |
+
When disabled, raise RoutingError.
|
| 720 |
+
"""
|
| 721 |
+
pass
|
| 722 |
+
```
|
| 723 |
+
|
| 724 |
+
---
|
| 725 |
+
|
| 726 |
+
### 3.7 Agent: Model Timeout & Backoff (`src/agents/react.py` — extend by ~100 lines)
|
| 727 |
+
|
| 728 |
+
**Add to ReActAgent:**
|
| 729 |
+
|
| 730 |
+
```python
|
| 731 |
+
class ReActAgent:
|
| 732 |
+
def __init__(self, ..., timeout_config: TimeoutConfig | None = None):
|
| 733 |
+
"""
|
| 734 |
+
Args:
|
| 735 |
+
timeout_config: Timeout and retry strategy
|
| 736 |
+
"""
|
| 737 |
+
self.timeout_config = timeout_config or TimeoutConfig()
|
| 738 |
+
|
| 739 |
+
async def call_model_with_retry(
|
| 740 |
+
self,
|
| 741 |
+
prompt: str,
|
| 742 |
+
max_retries: int = 3
|
| 743 |
+
) -> str:
|
| 744 |
+
"""
|
| 745 |
+
Call LLM with exponential backoff on timeout/rate-limit.
|
| 746 |
+
|
| 747 |
+
Algorithm:
|
| 748 |
+
- Attempt 1: 30s timeout
|
| 749 |
+
- Attempt 2: 60s timeout, wait 2s before retry
|
| 750 |
+
- Attempt 3: 90s timeout, wait 4s before retry
|
| 751 |
+
- Raise ModelTimeoutError if all attempts fail
|
| 752 |
+
|
| 753 |
+
Detects:
|
| 754 |
+
- Timeout (socket/asyncio timeout)
|
| 755 |
+
- Rate limit (429 HTTP status)
|
| 756 |
+
- Server error (500–599 HTTP)
|
| 757 |
+
|
| 758 |
+
Args:
|
| 759 |
+
prompt: Input to model
|
| 760 |
+
max_retries: Max attempts
|
| 761 |
+
|
| 762 |
+
Returns:
|
| 763 |
+
Model output
|
| 764 |
+
|
| 765 |
+
Raises:
|
| 766 |
+
ModelTimeoutError
|
| 767 |
+
ModelRateLimitError
|
| 768 |
+
"""
|
| 769 |
+
pass
|
| 770 |
+
```
|
| 771 |
+
|
| 772 |
+
**Add TimeoutConfig dataclass:**
|
| 773 |
+
|
| 774 |
+
```python
|
| 775 |
+
@dataclass
|
| 776 |
+
class TimeoutConfig:
|
| 777 |
+
initial_timeout_sec: float = 30
|
| 778 |
+
max_timeout_sec: float = 120
|
| 779 |
+
backoff_multiplier: float = 2.0
|
| 780 |
+
initial_retry_delay_sec: float = 2
|
| 781 |
+
max_retry_delay_sec: float = 60
|
| 782 |
+
jitter_enabled: bool = True # Add random noise to prevent thundering herd
|
| 783 |
+
```
|
| 784 |
+
|
| 785 |
+
---
|
| 786 |
+
|
| 787 |
+
### 3.8 Shadow: Queue Overflow (`src/agents/shadow.py` — extend by ~60 lines)
|
| 788 |
+
|
| 789 |
+
**Add to ShadowAgent:**
|
| 790 |
+
|
| 791 |
+
```python
|
| 792 |
+
class ShadowAgent:
|
| 793 |
+
def __init__(self, ..., max_queue_size: int = 10000):
|
| 794 |
+
"""
|
| 795 |
+
Args:
|
| 796 |
+
max_queue_size: Max observations in queue before dropping
|
| 797 |
+
"""
|
| 798 |
+
self.max_queue_size = max_queue_size
|
| 799 |
+
self.queue_depth_warn_threshold = 0.8 * max_queue_size
|
| 800 |
+
|
| 801 |
+
async def append_observation(self, obs: str) -> None:
|
| 802 |
+
"""
|
| 803 |
+
Append observation, dropping oldest if queue full.
|
| 804 |
+
|
| 805 |
+
- If queue at 80% capacity, log warning
|
| 806 |
+
- If queue at 100% capacity, drop oldest, log error
|
| 807 |
+
- Track drop count for metrics
|
| 808 |
+
"""
|
| 809 |
+
pass
|
| 810 |
+
|
| 811 |
+
def get_queue_stats(self) -> dict[str, int]:
|
| 812 |
+
"""
|
| 813 |
+
Return queue stats.
|
| 814 |
+
|
| 815 |
+
Returns:
|
| 816 |
+
{
|
| 817 |
+
"size": current_size,
|
| 818 |
+
"max": max_size,
|
| 819 |
+
"dropped": total_dropped,
|
| 820 |
+
"utilization_percent": 100 * size / max
|
| 821 |
+
}
|
| 822 |
+
"""
|
| 823 |
+
pass
|
| 824 |
+
```
|
| 825 |
+
|
| 826 |
+
---
|
| 827 |
+
|
| 828 |
+
### 3.9 Tools: Per-Tool Timeout (`src/tools/registry.py` — extend by ~50 lines)
|
| 829 |
+
|
| 830 |
+
**Add to ToolDefinition:**
|
| 831 |
+
|
| 832 |
+
```python
|
| 833 |
+
@dataclass
|
| 834 |
+
class ToolDefinition:
|
| 835 |
+
# ... existing fields ...
|
| 836 |
+
|
| 837 |
+
timeout_sec: float = 60 # Default 60s per tool call
|
| 838 |
+
max_retries: int = 1
|
| 839 |
+
retry_backoff_sec: float = 2.0
|
| 840 |
+
```
|
| 841 |
+
|
| 842 |
+
**Add function:**
|
| 843 |
+
|
| 844 |
+
```python
|
| 845 |
+
async def call_tool_with_timeout(
|
| 846 |
+
tool: ToolDefinition,
|
| 847 |
+
args: dict[str, Any],
|
| 848 |
+
logger: logging.Logger
|
| 849 |
+
) -> Any:
|
| 850 |
+
"""
|
| 851 |
+
Call tool with per-tool timeout enforcement.
|
| 852 |
+
|
| 853 |
+
- Set asyncio timeout to tool.timeout_sec
|
| 854 |
+
- If timeout, cancel task and log
|
| 855 |
+
- Raise ToolTimeoutError
|
| 856 |
+
|
| 857 |
+
Raises:
|
| 858 |
+
ToolTimeoutError
|
| 859 |
+
asyncio.TimeoutError (caught and converted)
|
| 860 |
+
"""
|
| 861 |
+
pass
|
| 862 |
+
```
|
| 863 |
+
|
| 864 |
+
---
|
| 865 |
+
|
| 866 |
+
### 3.10 Bridge: C Frontend Heartbeat (`src/bridge/http_server.py` — extend by ~80 lines)
|
| 867 |
+
|
| 868 |
+
**Add heartbeat mechanism:**
|
| 869 |
+
|
| 870 |
+
```python
|
| 871 |
+
class HTTPBridge:
|
| 872 |
+
def __init__(self, ..., heartbeat_interval_sec: float = 5.0):
|
| 873 |
+
"""
|
| 874 |
+
Args:
|
| 875 |
+
heartbeat_interval_sec: Interval to send heartbeats to C frontend
|
| 876 |
+
"""
|
| 877 |
+
self.heartbeat_interval = heartbeat_interval_sec
|
| 878 |
+
self.last_frontend_ping: float | None = None
|
| 879 |
+
|
| 880 |
+
async def heartbeat_monitor(self) -> None:
|
| 881 |
+
"""
|
| 882 |
+
Periodically send heartbeat to frontend.
|
| 883 |
+
|
| 884 |
+
- Send HTTP GET /health every heartbeat_interval seconds
|
| 885 |
+
- If no response for 3x interval, assume frontend dead
|
| 886 |
+
- Clean up resources (close WebSocket, free memory)
|
| 887 |
+
- Log frontend crash
|
| 888 |
+
"""
|
| 889 |
+
pass
|
| 890 |
+
|
| 891 |
+
async def handle_frontend_disconnect(self) -> None:
|
| 892 |
+
"""
|
| 893 |
+
Called when frontend heartbeat fails.
|
| 894 |
+
|
| 895 |
+
- Cancel any in-progress requests
|
| 896 |
+
- Save agent state (WORM checkpoint)
|
| 897 |
+
- Release mmap/IPC resources
|
| 898 |
+
- Log event
|
| 899 |
+
"""
|
| 900 |
+
pass
|
| 901 |
+
|
| 902 |
+
@property
|
| 903 |
+
def frontend_alive(self) -> bool:
|
| 904 |
+
"""True if frontend responded to recent heartbeat."""
|
| 905 |
+
pass
|
| 906 |
+
```
|
| 907 |
+
|
| 908 |
+
---
|
| 909 |
+
|
| 910 |
+
## 4. SECURITY HARDENING
|
| 911 |
+
|
| 912 |
+
### Goal
|
| 913 |
+
Extend PathJail/SSRFGuard, add rate limiting, cost tracking, emergency kill switch, validate inputs, prevent regex DoS.
|
| 914 |
+
|
| 915 |
+
### 4.1 PathJail & SSRFGuard (See Section 3.1–3.2)
|
| 916 |
+
|
| 917 |
+
---
|
| 918 |
+
|
| 919 |
+
### 4.2 Rate Limiting (`src/tools/approval.py` — extend by ~150 lines)
|
| 920 |
+
|
| 921 |
+
**Add class:**
|
| 922 |
+
|
| 923 |
+
```python
|
| 924 |
+
@dataclass
|
| 925 |
+
class RateLimitPolicy:
|
| 926 |
+
"""Per-tool rate limiting configuration."""
|
| 927 |
+
tool_name: str
|
| 928 |
+
max_calls_per_minute: int
|
| 929 |
+
max_calls_per_hour: int
|
| 930 |
+
burst_allowed: int = 5 # Allow burst up to 5 calls
|
| 931 |
+
|
| 932 |
+
@dataclass
|
| 933 |
+
class RateLimitViolation:
|
| 934 |
+
"""Result of rate limit check."""
|
| 935 |
+
violated: bool
|
| 936 |
+
calls_made: int
|
| 937 |
+
calls_remaining: int
|
| 938 |
+
reset_time_sec: float
|
| 939 |
+
|
| 940 |
+
|
| 941 |
+
class RateLimiter:
|
| 942 |
+
"""Token bucket rate limiter per tool."""
|
| 943 |
+
|
| 944 |
+
def __init__(self, policy: RateLimitPolicy):
|
| 945 |
+
pass
|
| 946 |
+
|
| 947 |
+
def check_rate_limit(self) -> RateLimitViolation:
|
| 948 |
+
"""
|
| 949 |
+
Check if tool call allowed.
|
| 950 |
+
|
| 951 |
+
Uses token bucket algorithm:
|
| 952 |
+
- Bucket has capacity = max_calls_per_minute
|
| 953 |
+
- Tokens regenerate at rate = capacity / 60 per second
|
| 954 |
+
- Each call consumes 1 token
|
| 955 |
+
- Burst = allow 5 calls above steady-state
|
| 956 |
+
|
| 957 |
+
Returns:
|
| 958 |
+
RateLimitViolation with status
|
| 959 |
+
"""
|
| 960 |
+
pass
|
| 961 |
+
|
| 962 |
+
def get_stats(self) -> dict[str, int]:
|
| 963 |
+
"""Return current rate limit stats."""
|
| 964 |
+
pass
|
| 965 |
+
|
| 966 |
+
|
| 967 |
+
# Update ApprovalEngine:
|
| 968 |
+
class ApprovalEngine:
|
| 969 |
+
def __init__(self, ..., rate_limit_policies: dict[str, RateLimitPolicy] | None = None):
|
| 970 |
+
"""
|
| 971 |
+
Args:
|
| 972 |
+
rate_limit_policies: Tool name → RateLimitPolicy mapping
|
| 973 |
+
"""
|
| 974 |
+
self.rate_limiters = {
|
| 975 |
+
name: RateLimiter(policy)
|
| 976 |
+
for name, policy in (rate_limit_policies or {}).items()
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
async def check_rate_limit(self, tool_name: str) -> RateLimitViolation:
|
| 980 |
+
"""Check if tool call violates rate limit."""
|
| 981 |
+
if tool_name not in self.rate_limiters:
|
| 982 |
+
return RateLimitViolation(violated=False, ...)
|
| 983 |
+
return self.rate_limiters[tool_name].check_rate_limit()
|
| 984 |
+
|
| 985 |
+
async def check_approval(self, ...) -> ApprovalResult:
|
| 986 |
+
"""Extended to also check rate limit."""
|
| 987 |
+
# Check rate limit first
|
| 988 |
+
rate_check = await self.check_rate_limit(tool.name)
|
| 989 |
+
if rate_check.violated:
|
| 990 |
+
return ApprovalResult(
|
| 991 |
+
approved=False,
|
| 992 |
+
reason=f"Rate limit exceeded: {rate_check.calls_remaining} calls remaining",
|
| 993 |
+
...
|
| 994 |
+
)
|
| 995 |
+
# Then check existing approval logic
|
| 996 |
+
pass
|
| 997 |
+
```
|
| 998 |
+
|
| 999 |
+
---
|
| 1000 |
+
|
| 1001 |
+
### 4.3 Cost Tracking (`src/tools/approval.py` — extend by ~120 lines)
|
| 1002 |
+
|
| 1003 |
+
**Add class:**
|
| 1004 |
+
|
| 1005 |
+
```python
|
| 1006 |
+
@dataclass
|
| 1007 |
+
class ToolCost:
|
| 1008 |
+
"""Cost of a single tool call."""
|
| 1009 |
+
tool_name: str
|
| 1010 |
+
cost_usd: float
|
| 1011 |
+
timestamp: str
|
| 1012 |
+
actor: str
|
| 1013 |
+
duration_sec: float
|
| 1014 |
+
tokens_used: int | None = None # For LLM calls
|
| 1015 |
+
|
| 1016 |
+
|
| 1017 |
+
class CostTracker:
|
| 1018 |
+
"""Track cumulative cost of tool calls."""
|
| 1019 |
+
|
| 1020 |
+
def __init__(self, ledger: WORMLedger):
|
| 1021 |
+
self.ledger = ledger
|
| 1022 |
+
|
| 1023 |
+
def log_cost(self, cost: ToolCost) -> None:
|
| 1024 |
+
"""
|
| 1025 |
+
Log tool call cost to WORM.
|
| 1026 |
+
|
| 1027 |
+
Records:
|
| 1028 |
+
- tool_name
|
| 1029 |
+
- cost_usd (e.g., 0.0012)
|
| 1030 |
+
- duration
|
| 1031 |
+
- tokens (if applicable)
|
| 1032 |
+
- timestamp
|
| 1033 |
+
"""
|
| 1034 |
+
pass
|
| 1035 |
+
|
| 1036 |
+
async def get_total_cost(self, actor: str | None = None, since: datetime | None = None) -> float:
|
| 1037 |
+
"""
|
| 1038 |
+
Compute total cost incurred.
|
| 1039 |
+
|
| 1040 |
+
Args:
|
| 1041 |
+
actor: Filter by actor (None = all)
|
| 1042 |
+
since: Filter by time (None = all time)
|
| 1043 |
+
|
| 1044 |
+
Returns:
|
| 1045 |
+
Total cost in USD
|
| 1046 |
+
"""
|
| 1047 |
+
pass
|
| 1048 |
+
|
| 1049 |
+
async def get_cost_by_tool(self) -> dict[str, float]:
|
| 1050 |
+
"""Break down cost by tool."""
|
| 1051 |
+
pass
|
| 1052 |
+
|
| 1053 |
+
def set_monthly_budget_usd(self, budget: float) -> None:
|
| 1054 |
+
"""Set max monthly spending."""
|
| 1055 |
+
pass
|
| 1056 |
+
|
| 1057 |
+
async def check_budget(self, actor: str) -> tuple[bool, str]:
|
| 1058 |
+
"""
|
| 1059 |
+
Check if remaining budget allows next tool call.
|
| 1060 |
+
|
| 1061 |
+
Returns:
|
| 1062 |
+
(budget_ok, reason)
|
| 1063 |
+
"""
|
| 1064 |
+
pass
|
| 1065 |
+
|
| 1066 |
+
|
| 1067 |
+
# Update ApprovalEngine:
|
| 1068 |
+
class ApprovalEngine:
|
| 1069 |
+
def __init__(self, ..., cost_tracker: CostTracker | None = None):
|
| 1070 |
+
self.cost_tracker = cost_tracker
|
| 1071 |
+
|
| 1072 |
+
async def check_approval(self, tool: ToolDefinition, ...) -> ApprovalResult:
|
| 1073 |
+
"""Extended to check cost budget."""
|
| 1074 |
+
if self.cost_tracker:
|
| 1075 |
+
budget_ok, reason = await self.cost_tracker.check_budget(actor)
|
| 1076 |
+
if not budget_ok:
|
| 1077 |
+
return ApprovalResult(approved=False, reason=reason, ...)
|
| 1078 |
+
# Continue with existing checks
|
| 1079 |
+
pass
|
| 1080 |
+
```
|
| 1081 |
+
|
| 1082 |
+
---
|
| 1083 |
+
|
| 1084 |
+
### 4.4 Emergency Kill Switch (`src/tools/supervisor.py` — extend by ~80 lines)
|
| 1085 |
+
|
| 1086 |
+
**Add class:**
|
| 1087 |
+
|
| 1088 |
+
```python
|
| 1089 |
+
class EmergencyKillSwitch:
|
| 1090 |
+
"""
|
| 1091 |
+
Emergency stop: disable all tools immediately.
|
| 1092 |
+
"""
|
| 1093 |
+
|
| 1094 |
+
def __init__(self, approval_engine: ApprovalEngine):
|
| 1095 |
+
self.approval_engine = approval_engine
|
| 1096 |
+
self._kill_switch_engaged = False
|
| 1097 |
+
|
| 1098 |
+
def engage(self, reason: str, actor: str) -> None:
|
| 1099 |
+
"""
|
| 1100 |
+
Engage kill switch: disable all tool execution.
|
| 1101 |
+
|
| 1102 |
+
Args:
|
| 1103 |
+
reason: Why kill switch engaged (logged)
|
| 1104 |
+
actor: Who engaged it (logged)
|
| 1105 |
+
|
| 1106 |
+
Effect:
|
| 1107 |
+
- All subsequent tool calls return ApprovalResult(approved=False)
|
| 1108 |
+
- WORM record created
|
| 1109 |
+
- Alert sent to admin
|
| 1110 |
+
"""
|
| 1111 |
+
pass
|
| 1112 |
+
|
| 1113 |
+
def disengage(self, reason: str, actor: str) -> None:
|
| 1114 |
+
"""Disable kill switch (requires auth)."""
|
| 1115 |
+
pass
|
| 1116 |
+
|
| 1117 |
+
def is_engaged(self) -> bool:
|
| 1118 |
+
"""Check current state."""
|
| 1119 |
+
pass
|
| 1120 |
+
|
| 1121 |
+
def force_stop_all_agents(self) -> int:
|
| 1122 |
+
"""
|
| 1123 |
+
Stop all running agents immediately.
|
| 1124 |
+
|
| 1125 |
+
Returns:
|
| 1126 |
+
Number of agents stopped
|
| 1127 |
+
"""
|
| 1128 |
+
pass
|
| 1129 |
+
|
| 1130 |
+
|
| 1131 |
+
# Add to main engine init:
|
| 1132 |
+
def __init__(self, ...):
|
| 1133 |
+
self.kill_switch = EmergencyKillSwitch(self.approval_engine)
|
| 1134 |
+
```
|
| 1135 |
+
|
| 1136 |
+
---
|
| 1137 |
+
|
| 1138 |
+
### 4.5 Input Validation (`src/core/types.py` — new file, ~150 lines)
|
| 1139 |
+
|
| 1140 |
+
**Add validators:**
|
| 1141 |
+
|
| 1142 |
+
```python
|
| 1143 |
+
class InputValidator:
|
| 1144 |
+
"""Validate all untrusted input."""
|
| 1145 |
+
|
| 1146 |
+
MAX_MESSAGE_LENGTH = 100_000 # 100KB
|
| 1147 |
+
MAX_QUERY_LENGTH = 10_000 # 10KB
|
| 1148 |
+
REGEX_TIMEOUT_MS = 100
|
| 1149 |
+
|
| 1150 |
+
@staticmethod
|
| 1151 |
+
def validate_message_length(msg: str) -> None:
|
| 1152 |
+
"""Enforce max message length."""
|
| 1153 |
+
if len(msg) > InputValidator.MAX_MESSAGE_LENGTH:
|
| 1154 |
+
raise ValueError(
|
| 1155 |
+
f"Message exceeds max length: {len(msg)} > {InputValidator.MAX_MESSAGE_LENGTH}"
|
| 1156 |
+
)
|
| 1157 |
+
|
| 1158 |
+
@staticmethod
|
| 1159 |
+
def validate_unicode_normalization(text: str) -> str:
|
| 1160 |
+
"""
|
| 1161 |
+
Normalize Unicode to prevent homograph attacks.
|
| 1162 |
+
|
| 1163 |
+
- Decompose combining characters
|
| 1164 |
+
- Detect confusable scripts (Latin + Cyrillic)
|
| 1165 |
+
- Raise error if mixing detected
|
| 1166 |
+
|
| 1167 |
+
Returns:
|
| 1168 |
+
Normalized text
|
| 1169 |
+
"""
|
| 1170 |
+
import unicodedata
|
| 1171 |
+
nfc = unicodedata.normalize('NFC', text)
|
| 1172 |
+
# TODO: Homograph detection
|
| 1173 |
+
return nfc
|
| 1174 |
+
|
| 1175 |
+
@staticmethod
|
| 1176 |
+
def validate_regex_safe(pattern: str) -> bool:
|
| 1177 |
+
"""
|
| 1178 |
+
Check regex for potential DoS (catastrophic backtracking).
|
| 1179 |
+
|
| 1180 |
+
Algorithm:
|
| 1181 |
+
- Use timeout-based check: compile pattern with timeout
|
| 1182 |
+
- Test against sample inputs
|
| 1183 |
+
- Raise error if timeout
|
| 1184 |
+
|
| 1185 |
+
Or: Require patterns to match RE2 spec (no backtracking).
|
| 1186 |
+
|
| 1187 |
+
Returns:
|
| 1188 |
+
True if safe
|
| 1189 |
+
|
| 1190 |
+
Raises:
|
| 1191 |
+
RegexDoSError if pattern dangerous
|
| 1192 |
+
"""
|
| 1193 |
+
pass
|
| 1194 |
+
|
| 1195 |
+
@staticmethod
|
| 1196 |
+
def validate_json_safe(json_str: str) -> Any:
|
| 1197 |
+
"""
|
| 1198 |
+
Parse JSON with size limits.
|
| 1199 |
+
|
| 1200 |
+
- Enforce max nesting depth (10)
|
| 1201 |
+
- Enforce max string length (1MB)
|
| 1202 |
+
- Reject duplicate keys (potential collision attack)
|
| 1203 |
+
"""
|
| 1204 |
+
pass
|
| 1205 |
+
|
| 1206 |
+
@staticmethod
|
| 1207 |
+
def sanitize_sql_parameter(param: str) -> str:
|
| 1208 |
+
"""
|
| 1209 |
+
Sanitize SQL parameter (for parameterized queries).
|
| 1210 |
+
|
| 1211 |
+
Note: Should use parameterized queries, not escaping!
|
| 1212 |
+
This is fallback only.
|
| 1213 |
+
"""
|
| 1214 |
+
pass
|
| 1215 |
+
```
|
| 1216 |
+
|
| 1217 |
+
---
|
| 1218 |
+
|
| 1219 |
+
### 4.6 Crypto Verification (`src/core/crypto.py` — extend by ~100 lines)
|
| 1220 |
+
|
| 1221 |
+
**Verify existing functionality and extend:**
|
| 1222 |
+
|
| 1223 |
+
```python
|
| 1224 |
+
class CryptoManager:
|
| 1225 |
+
"""Manage cryptographic keys and operations."""
|
| 1226 |
+
|
| 1227 |
+
def __init__(self, master_secret: bytes | None = None):
|
| 1228 |
+
"""
|
| 1229 |
+
Args:
|
| 1230 |
+
master_secret: 32-byte master secret (or load from secure store)
|
| 1231 |
+
"""
|
| 1232 |
+
self.master_secret = master_secret
|
| 1233 |
+
|
| 1234 |
+
def derive_key(self, context: str, length: int = 32) -> bytes:
|
| 1235 |
+
"""
|
| 1236 |
+
Derive key from master secret using HKDF.
|
| 1237 |
+
|
| 1238 |
+
Args:
|
| 1239 |
+
context: String context (e.g., "worm_signing", "aes_encryption")
|
| 1240 |
+
length: Byte length of derived key
|
| 1241 |
+
|
| 1242 |
+
Returns:
|
| 1243 |
+
Derived key
|
| 1244 |
+
|
| 1245 |
+
Algorithm:
|
| 1246 |
+
HKDF-SHA256(master_secret, context.encode(), length)
|
| 1247 |
+
"""
|
| 1248 |
+
pass
|
| 1249 |
+
|
| 1250 |
+
def rotate_key(self, old_key_id: str) -> str:
|
| 1251 |
+
"""
|
| 1252 |
+
Rotate signing key: generate new key, mark old as retired.
|
| 1253 |
+
|
| 1254 |
+
Args:
|
| 1255 |
+
old_key_id: ID of key to retire
|
| 1256 |
+
|
| 1257 |
+
Returns:
|
| 1258 |
+
ID of new key
|
| 1259 |
+
|
| 1260 |
+
Records rotation in WORM for audit trail.
|
| 1261 |
+
"""
|
| 1262 |
+
pass
|
| 1263 |
+
|
| 1264 |
+
def verify_signature_on_read(self, data: bytes, signature: bytes, key_id: str) -> bool:
|
| 1265 |
+
"""
|
| 1266 |
+
Verify Ed25519 signature on data read from storage.
|
| 1267 |
+
|
| 1268 |
+
Args:
|
| 1269 |
+
data: Data to verify
|
| 1270 |
+
signature: Signature bytes
|
| 1271 |
+
key_id: ID of key to use for verification
|
| 1272 |
+
|
| 1273 |
+
Returns:
|
| 1274 |
+
True if signature valid
|
| 1275 |
+
|
| 1276 |
+
Raises:
|
| 1277 |
+
CryptoError if key_id not found
|
| 1278 |
+
"""
|
| 1279 |
+
pass
|
| 1280 |
+
|
| 1281 |
+
def get_key_metadata(self, key_id: str) -> dict[str, Any]:
|
| 1282 |
+
"""Get metadata about a key (created_at, retired_at, etc)."""
|
| 1283 |
+
pass
|
| 1284 |
+
```
|
| 1285 |
+
|
| 1286 |
+
---
|
| 1287 |
+
|
| 1288 |
+
## 5. TESTING
|
| 1289 |
+
|
| 1290 |
+
### Goal
|
| 1291 |
+
Comprehensive pytest suite (~2000 lines) covering unit, integration, and property-based tests.
|
| 1292 |
+
|
| 1293 |
+
### 5.1 Unit Tests
|
| 1294 |
+
|
| 1295 |
+
#### `tests/test_routing.py` (~400 lines)
|
| 1296 |
+
|
| 1297 |
+
```python
|
| 1298 |
+
"""Test Jordan algebra MoE routing properties."""
|
| 1299 |
+
|
| 1300 |
+
import pytest
|
| 1301 |
+
from src.routing.jordan_moe import SpinFactor, JordanMoERouter
|
| 1302 |
+
|
| 1303 |
+
class TestSpinFactorAlgebra:
|
| 1304 |
+
"""Verify Jordan algebra axioms."""
|
| 1305 |
+
|
| 1306 |
+
def test_commutativity(self):
|
| 1307 |
+
"""A ∘ B = B ∘ A"""
|
| 1308 |
+
a = SpinFactor(2.0, [1.0, 0.0])
|
| 1309 |
+
b = SpinFactor(3.0, [0.0, 1.0])
|
| 1310 |
+
assert a.compose(b) == b.compose(a)
|
| 1311 |
+
|
| 1312 |
+
def test_non_associativity(self):
|
| 1313 |
+
"""(A ∘ B) ∘ C ≠ A ∘ (B ∘ C) in general"""
|
| 1314 |
+
a = SpinFactor(1.0, [1.0, 0.0])
|
| 1315 |
+
b = SpinFactor(1.0, [0.0, 1.0])
|
| 1316 |
+
c = SpinFactor(1.0, [1.0, 1.0])
|
| 1317 |
+
|
| 1318 |
+
left = a.compose(b).compose(c)
|
| 1319 |
+
right = a.compose(b.compose(c))
|
| 1320 |
+
# Should differ (non-associative)
|
| 1321 |
+
assert left != right
|
| 1322 |
+
|
| 1323 |
+
def test_idempotent_convergence(self):
|
| 1324 |
+
"""x ∘ x ∘ x... converges to idempotent e where e ∘ e = e"""
|
| 1325 |
+
x = SpinFactor(0.5, [0.2, 0.3])
|
| 1326 |
+
|
| 1327 |
+
for _ in range(100):
|
| 1328 |
+
x_new = x.compose(x)
|
| 1329 |
+
# Eventually x ≈ e and e ∘ e ≈ e
|
| 1330 |
+
if abs(x_new.scalar - x.scalar) < 1e-6:
|
| 1331 |
+
# Converged, verify idempotency
|
| 1332 |
+
e_squared = x_new.compose(x_new)
|
| 1333 |
+
assert abs(e_squared.scalar - x_new.scalar) < 1e-6
|
| 1334 |
+
break
|
| 1335 |
+
x = x_new
|
| 1336 |
+
```
|
| 1337 |
+
|
| 1338 |
+
#### `tests/test_storage.py` (~300 lines)
|
| 1339 |
+
|
| 1340 |
+
```python
|
| 1341 |
+
"""Test WORM append-only and tamper detection."""
|
| 1342 |
+
|
| 1343 |
+
import pytest
|
| 1344 |
+
from src.core.storage import WORMFile, WORMRecord
|
| 1345 |
+
from pathlib import Path
|
| 1346 |
+
|
| 1347 |
+
class TestWORMAppendOnly:
|
| 1348 |
+
"""WORM records cannot be modified."""
|
| 1349 |
+
|
| 1350 |
+
def test_append_writes_record(self, tmp_path):
|
| 1351 |
+
"""Appending writes record correctly."""
|
| 1352 |
+
worm = WORMFile(tmp_path / "test.worm")
|
| 1353 |
+
rec = WORMRecord(event_type="test", payload=b"hello")
|
| 1354 |
+
|
| 1355 |
+
worm.append(rec)
|
| 1356 |
+
|
| 1357 |
+
records = list(worm.read())
|
| 1358 |
+
assert len(records) == 1
|
| 1359 |
+
assert records[0].payload == b"hello"
|
| 1360 |
+
|
| 1361 |
+
def test_chain_verification(self, tmp_path):
|
| 1362 |
+
"""Each record hashes the previous."""
|
| 1363 |
+
worm = WORMFile(tmp_path / "test.worm")
|
| 1364 |
+
rec1 = WORMRecord(event_type="evt1", payload=b"data1")
|
| 1365 |
+
rec2 = WORMRecord(event_type="evt2", payload=b"data2")
|
| 1366 |
+
|
| 1367 |
+
worm.append(rec1)
|
| 1368 |
+
worm.append(rec2)
|
| 1369 |
+
|
| 1370 |
+
records = list(worm.read())
|
| 1371 |
+
# rec2.prev_hash should match rec1's content hash
|
| 1372 |
+
assert records[1].prev_hash == records[0].content_hash
|
| 1373 |
+
|
| 1374 |
+
def test_tamper_detection(self, tmp_path):
|
| 1375 |
+
"""Corrupt file fails verification."""
|
| 1376 |
+
worm = WORMFile(tmp_path / "test.worm")
|
| 1377 |
+
rec = WORMRecord(event_type="test", payload=b"data")
|
| 1378 |
+
worm.append(rec)
|
| 1379 |
+
|
| 1380 |
+
# Corrupt payload byte
|
| 1381 |
+
with open(worm.path, 'r+b') as f:
|
| 1382 |
+
f.seek(152 + 10) # Skip header
|
| 1383 |
+
f.write(b'X')
|
| 1384 |
+
|
| 1385 |
+
# Reading should detect corruption
|
| 1386 |
+
with pytest.raises(Exception): # WORM corruption error
|
| 1387 |
+
list(worm.read())
|
| 1388 |
+
```
|
| 1389 |
+
|
| 1390 |
+
#### `tests/test_continuity.py` (~250 lines)
|
| 1391 |
+
|
| 1392 |
+
```python
|
| 1393 |
+
"""Test 4-paradigm state save/restore/restart."""
|
| 1394 |
+
|
| 1395 |
+
import pytest
|
| 1396 |
+
import os
|
| 1397 |
+
from src.continuity.manager import ContinuityManager
|
| 1398 |
+
|
| 1399 |
+
class TestContinuityRestart:
|
| 1400 |
+
"""Agent state survives restart."""
|
| 1401 |
+
|
| 1402 |
+
def test_env_state_preserved(self, tmp_path, monkeypatch):
|
| 1403 |
+
"""Environment bitmask restored on hot restart."""
|
| 1404 |
+
base_dir = tmp_path / "continuity"
|
| 1405 |
+
|
| 1406 |
+
# First session
|
| 1407 |
+
mgr1 = ContinuityManager(base_dir=base_dir, agent_id="agent1")
|
| 1408 |
+
mgr1.set_env_bitmask(0x001f)
|
| 1409 |
+
mgr1.checkpoint()
|
| 1410 |
+
|
| 1411 |
+
# Simulate restart
|
| 1412 |
+
monkeypatch.delenv("SOVEREIGN_STEP", raising=False)
|
| 1413 |
+
|
| 1414 |
+
# Second session
|
| 1415 |
+
mgr2 = ContinuityManager(base_dir=base_dir, agent_id="agent1")
|
| 1416 |
+
assert mgr2.was_restarted
|
| 1417 |
+
assert mgr2.get_env_bitmask() == 0x001f
|
| 1418 |
+
|
| 1419 |
+
def test_seed_chain_deterministic_replay(self, tmp_path):
|
| 1420 |
+
"""Seed-derived randomness is deterministic."""
|
| 1421 |
+
base_dir = tmp_path / "continuity"
|
| 1422 |
+
|
| 1423 |
+
# Session 1: record sequence
|
| 1424 |
+
mgr1 = ContinuityManager(base_dir=base_dir, agent_id="agent1")
|
| 1425 |
+
mgr1.set_seed_chain(b'test_seed')
|
| 1426 |
+
values1 = [mgr1.derive_random() for _ in range(5)]
|
| 1427 |
+
|
| 1428 |
+
# Session 2: replay
|
| 1429 |
+
mgr2 = ContinuityManager(base_dir=base_dir, agent_id="agent1")
|
| 1430 |
+
mgr2.set_seed_chain(b'test_seed')
|
| 1431 |
+
values2 = [mgr2.derive_random() for _ in range(5)]
|
| 1432 |
+
|
| 1433 |
+
assert values1 == values2
|
| 1434 |
+
```
|
| 1435 |
+
|
| 1436 |
+
#### `tests/test_path_jail.py` (~250 lines)
|
| 1437 |
+
|
| 1438 |
+
```python
|
| 1439 |
+
"""Test PathJail directory traversal prevention."""
|
| 1440 |
+
|
| 1441 |
+
import pytest
|
| 1442 |
+
from src.core.path_jail import PathJail, PathJailError
|
| 1443 |
+
|
| 1444 |
+
class TestPathJail:
|
| 1445 |
+
"""Verify all escapes blocked."""
|
| 1446 |
+
|
| 1447 |
+
def test_relative_traversal_blocked(self, tmp_path):
|
| 1448 |
+
"""../.. escapes rejected."""
|
| 1449 |
+
jail = PathJail([tmp_path])
|
| 1450 |
+
|
| 1451 |
+
with pytest.raises(PathJailError, match="escapes jail"):
|
| 1452 |
+
jail.resolve(tmp_path / "subdir" / ".." / ".." / "etc" / "passwd")
|
| 1453 |
+
|
| 1454 |
+
def test_symlink_escape_blocked(self, tmp_path):
|
| 1455 |
+
"""Symlink outside root rejected."""
|
| 1456 |
+
jail = PathJail([tmp_path])
|
| 1457 |
+
(tmp_path / "etc").mkdir()
|
| 1458 |
+
(tmp_path / "workspace").mkdir()
|
| 1459 |
+
|
| 1460 |
+
# Create symlink inside jail pointing outside
|
| 1461 |
+
import os
|
| 1462 |
+
os.symlink("/etc/passwd", tmp_path / "workspace" / "link")
|
| 1463 |
+
|
| 1464 |
+
with pytest.raises(PathJailError, match="escapes jail"):
|
| 1465 |
+
jail.resolve(tmp_path / "workspace" / "link")
|
| 1466 |
+
|
| 1467 |
+
def test_null_byte_injection_blocked(self, tmp_path):
|
| 1468 |
+
"""Null byte in path rejected."""
|
| 1469 |
+
jail = PathJail([tmp_path])
|
| 1470 |
+
|
| 1471 |
+
with pytest.raises(PathJailError, match="Null byte"):
|
| 1472 |
+
jail.resolve(str(tmp_path) + "\x00/etc/passwd")
|
| 1473 |
+
|
| 1474 |
+
def test_safe_path_allowed(self, tmp_path):
|
| 1475 |
+
"""Valid path inside root allowed."""
|
| 1476 |
+
jail = PathJail([tmp_path])
|
| 1477 |
+
(tmp_path / "safe.txt").touch()
|
| 1478 |
+
|
| 1479 |
+
result = jail.resolve(tmp_path / "safe.txt")
|
| 1480 |
+
assert result.exists()
|
| 1481 |
+
```
|
| 1482 |
+
|
| 1483 |
+
#### `tests/test_ipc.py` (~200 lines)
|
| 1484 |
+
|
| 1485 |
+
```python
|
| 1486 |
+
"""Test IPC ring buffer and opcode dispatch."""
|
| 1487 |
+
|
| 1488 |
+
import pytest
|
| 1489 |
+
from src.tools.ipc_router import IPCRouter
|
| 1490 |
+
|
| 1491 |
+
class TestIPCDispatch:
|
| 1492 |
+
"""Verify opcode routing and concurrency."""
|
| 1493 |
+
|
| 1494 |
+
async def test_opcode_dispatch(self):
|
| 1495 |
+
"""Opcode routes to correct handler."""
|
| 1496 |
+
router = IPCRouter()
|
| 1497 |
+
handler_called = False
|
| 1498 |
+
|
| 1499 |
+
async def mock_handler(payload):
|
| 1500 |
+
nonlocal handler_called
|
| 1501 |
+
handler_called = True
|
| 1502 |
+
return b"result"
|
| 1503 |
+
|
| 1504 |
+
router.register_opcode(0x10, mock_handler)
|
| 1505 |
+
result = await router.dispatch(0x10, b"test")
|
| 1506 |
+
|
| 1507 |
+
assert handler_called
|
| 1508 |
+
assert result == b"result"
|
| 1509 |
+
|
| 1510 |
+
def test_ring_buffer_overflow(self):
|
| 1511 |
+
"""Ring buffer wraps at boundary."""
|
| 1512 |
+
router = IPCRouter()
|
| 1513 |
+
# Write beyond buffer size
|
| 1514 |
+
# Verify wrap-around works correctly
|
| 1515 |
+
pass
|
| 1516 |
+
|
| 1517 |
+
async def test_concurrent_dispatch(self):
|
| 1518 |
+
"""Multiple concurrent dispatch calls work."""
|
| 1519 |
+
import asyncio
|
| 1520 |
+
router = IPCRouter()
|
| 1521 |
+
|
| 1522 |
+
async def handler(payload):
|
| 1523 |
+
await asyncio.sleep(0.01)
|
| 1524 |
+
return payload
|
| 1525 |
+
|
| 1526 |
+
router.register_opcode(0x20, handler)
|
| 1527 |
+
|
| 1528 |
+
# Launch 10 concurrent calls
|
| 1529 |
+
tasks = [router.dispatch(0x20, f"msg{i}".encode()) for i in range(10)]
|
| 1530 |
+
results = await asyncio.gather(*tasks)
|
| 1531 |
+
|
| 1532 |
+
assert len(results) == 10
|
| 1533 |
+
```
|
| 1534 |
+
|
| 1535 |
+
#### `tests/test_agent.py` (~200 lines)
|
| 1536 |
+
|
| 1537 |
+
```python
|
| 1538 |
+
"""Test ReAct agent loop and continuity."""
|
| 1539 |
+
|
| 1540 |
+
import pytest
|
| 1541 |
+
from src.agents.react import ReActAgent, ReActConfig
|
| 1542 |
+
|
| 1543 |
+
class TestReActLoop:
|
| 1544 |
+
"""Verify agent reasoning loop."""
|
| 1545 |
+
|
| 1546 |
+
async def test_thought_action_observation_cycle(self, mock_model, mock_tool_registry):
|
| 1547 |
+
"""Agent loops through thought → action → observation."""
|
| 1548 |
+
agent = ReActAgent(mock_model, mock_tool_registry)
|
| 1549 |
+
trajectory = await agent.run(task="test task")
|
| 1550 |
+
|
| 1551 |
+
assert len(trajectory.steps) > 0
|
| 1552 |
+
assert any(step.action_type == "tool_call" for step in trajectory.steps)
|
| 1553 |
+
|
| 1554 |
+
async def test_max_steps_enforced(self, mock_model, mock_tool_registry):
|
| 1555 |
+
"""Agent stops at max_steps."""
|
| 1556 |
+
config = ReActConfig(max_steps=3)
|
| 1557 |
+
agent = ReActAgent(mock_model, mock_tool_registry, config=config)
|
| 1558 |
+
|
| 1559 |
+
trajectory = await agent.run(task="test task")
|
| 1560 |
+
|
| 1561 |
+
assert len(trajectory.steps) <= 3
|
| 1562 |
+
|
| 1563 |
+
async def test_error_reflection(self, mock_model, mock_tool_registry):
|
| 1564 |
+
"""Agent reflects on errors."""
|
| 1565 |
+
# Configure mock to return error on first tool call
|
| 1566 |
+
mock_tool_registry.get_tool("test").side_effect = Exception("Tool error")
|
| 1567 |
+
|
| 1568 |
+
config = ReActConfig(reflection_on_error=True)
|
| 1569 |
+
agent = ReActAgent(mock_model, mock_tool_registry, config=config)
|
| 1570 |
+
|
| 1571 |
+
trajectory = await agent.run(task="test")
|
| 1572 |
+
|
| 1573 |
+
# Should include reflection step after error
|
| 1574 |
+
assert any("reflection" in str(step).lower() for step in trajectory.steps)
|
| 1575 |
+
```
|
| 1576 |
+
|
| 1577 |
+
### 5.2 Integration Tests
|
| 1578 |
+
|
| 1579 |
+
#### `tests/integration/test_full_pipeline.py` (~250 lines)
|
| 1580 |
+
|
| 1581 |
+
```python
|
| 1582 |
+
"""End-to-end task execution."""
|
| 1583 |
+
|
| 1584 |
+
import pytest
|
| 1585 |
+
|
| 1586 |
+
class TestFullPipeline:
|
| 1587 |
+
"""Task → agent → tools → result."""
|
| 1588 |
+
|
| 1589 |
+
async def test_simple_task_execution(self, engine_fixture):
|
| 1590 |
+
"""Execute simple task end-to-end."""
|
| 1591 |
+
task = "Search for 'LLM frameworks' and summarize top 3 results"
|
| 1592 |
+
|
| 1593 |
+
result = await engine_fixture.run_task(task)
|
| 1594 |
+
|
| 1595 |
+
assert result is not None
|
| 1596 |
+
assert "framework" in result.lower() or "llm" in result.lower()
|
| 1597 |
+
|
| 1598 |
+
async def test_tool_chain_execution(self, engine_fixture):
|
| 1599 |
+
"""Execute multi-tool task."""
|
| 1600 |
+
task = "Download paper from arXiv, summarize key findings"
|
| 1601 |
+
|
| 1602 |
+
result = await engine_fixture.run_task(task)
|
| 1603 |
+
|
| 1604 |
+
# Verify multiple tools were used
|
| 1605 |
+
assert engine_fixture.tool_call_count > 1
|
| 1606 |
+
|
| 1607 |
+
async def test_failure_recovery(self, engine_fixture):
|
| 1608 |
+
"""Agent handles tool failure gracefully."""
|
| 1609 |
+
# Simulate tool failure
|
| 1610 |
+
engine_fixture.tools["web_search"].fail_next_call()
|
| 1611 |
+
|
| 1612 |
+
task = "Search for something"
|
| 1613 |
+
result = await engine_fixture.run_task(task)
|
| 1614 |
+
|
| 1615 |
+
# Should succeed despite failure (agent reflects and recovers)
|
| 1616 |
+
assert result is not None
|
| 1617 |
+
```
|
| 1618 |
+
|
| 1619 |
+
#### `tests/integration/test_bridge.py` (~200 lines)
|
| 1620 |
+
|
| 1621 |
+
```python
|
| 1622 |
+
"""Test HTTP bridge request/response cycle."""
|
| 1623 |
+
|
| 1624 |
+
import pytest
|
| 1625 |
+
from src.bridge.http_server import HTTPBridge
|
| 1626 |
+
|
| 1627 |
+
class TestHTTPBridge:
|
| 1628 |
+
"""Bridge between C frontend and Python engine."""
|
| 1629 |
+
|
| 1630 |
+
async def test_request_response_cycle(self, bridge_fixture):
|
| 1631 |
+
"""Send request, receive response."""
|
| 1632 |
+
request = {
|
| 1633 |
+
"method": "run_task",
|
| 1634 |
+
"params": {"task": "test"}
|
| 1635 |
+
}
|
| 1636 |
+
|
| 1637 |
+
response = await bridge_fixture.send(request)
|
| 1638 |
+
|
| 1639 |
+
assert "result" in response or "error" in response
|
| 1640 |
+
|
| 1641 |
+
async def test_heartbeat_keeps_bridge_alive(self, bridge_fixture):
|
| 1642 |
+
"""Heartbeat prevents timeout."""
|
| 1643 |
+
# Simulate frontend not sending requests
|
| 1644 |
+
import asyncio
|
| 1645 |
+
await asyncio.sleep(10)
|
| 1646 |
+
|
| 1647 |
+
# Bridge should still be alive
|
| 1648 |
+
assert bridge_fixture.is_alive()
|
| 1649 |
+
|
| 1650 |
+
async def test_frontend_crash_cleanup(self, bridge_fixture):
|
| 1651 |
+
"""Frontend crash triggers cleanup."""
|
| 1652 |
+
# Simulate frontend crash
|
| 1653 |
+
bridge_fixture.frontend_connection.close()
|
| 1654 |
+
|
| 1655 |
+
# Wait for heartbeat to detect
|
| 1656 |
+
await asyncio.sleep(bridge_fixture.heartbeat_interval * 3)
|
| 1657 |
+
|
| 1658 |
+
# Cleanup should have occurred
|
| 1659 |
+
assert bridge_fixture.agent_state_saved()
|
| 1660 |
+
```
|
| 1661 |
+
|
| 1662 |
+
### 5.3 Property-Based Tests (Hypothesis)
|
| 1663 |
+
|
| 1664 |
+
#### `tests/property/test_jordan.py` (~150 lines)
|
| 1665 |
+
|
| 1666 |
+
```python
|
| 1667 |
+
"""Property-based testing of Jordan algebra."""
|
| 1668 |
+
|
| 1669 |
+
from hypothesis import given, strategies as st
|
| 1670 |
+
import pytest
|
| 1671 |
+
|
| 1672 |
+
class TestJordanProperties:
|
| 1673 |
+
"""Verify Jordan axioms hold for random inputs."""
|
| 1674 |
+
|
| 1675 |
+
@given(
|
| 1676 |
+
scalar1=st.floats(min_value=-1e6, max_value=1e6),
|
| 1677 |
+
vector1=st.lists(st.floats(min_value=-100, max_value=100), min_size=3, max_size=3),
|
| 1678 |
+
scalar2=st.floats(min_value=-1e6, max_value=1e6),
|
| 1679 |
+
vector2=st.lists(st.floats(min_value=-100, max_value=100), min_size=3, max_size=3),
|
| 1680 |
+
)
|
| 1681 |
+
def test_commutativity_holds(self, scalar1, vector1, scalar2, vector2):
|
| 1682 |
+
"""A ∘ B = B ∘ A for all A, B"""
|
| 1683 |
+
from src.routing.jordan_moe import SpinFactor
|
| 1684 |
+
|
| 1685 |
+
a = SpinFactor(scalar1, vector1)
|
| 1686 |
+
b = SpinFactor(scalar2, vector2)
|
| 1687 |
+
|
| 1688 |
+
assert a.compose(b) == b.compose(a)
|
| 1689 |
+
|
| 1690 |
+
@given(
|
| 1691 |
+
scalars=st.lists(st.floats(min_value=-10, max_value=10), min_size=3, max_size=5)
|
| 1692 |
+
)
|
| 1693 |
+
def test_convergence_to_idempotent(self, scalars):
|
| 1694 |
+
"""Repeated squaring converges to idempotent."""
|
| 1695 |
+
# TODO: Implement
|
| 1696 |
+
pass
|
| 1697 |
+
```
|
| 1698 |
+
|
| 1699 |
+
#### `tests/property/test_worm.py` (~150 lines)
|
| 1700 |
+
|
| 1701 |
+
```python
|
| 1702 |
+
"""Property-based testing of WORM append-only."""
|
| 1703 |
+
|
| 1704 |
+
from hypothesis import given, strategies as st
|
| 1705 |
+
|
| 1706 |
+
class TestWORMInvariants:
|
| 1707 |
+
"""WORM append-only invariant never violated."""
|
| 1708 |
+
|
| 1709 |
+
@given(
|
| 1710 |
+
payloads=st.lists(st.binary(min_size=1, max_size=1000), min_size=1, max_size=100)
|
| 1711 |
+
)
|
| 1712 |
+
def test_append_only_never_loses_data(self, payloads, tmp_path):
|
| 1713 |
+
"""Every appended record remains readable."""
|
| 1714 |
+
from src.core.storage import WORMFile, WORMRecord
|
| 1715 |
+
|
| 1716 |
+
worm = WORMFile(tmp_path / "test.worm")
|
| 1717 |
+
|
| 1718 |
+
for i, payload in enumerate(payloads):
|
| 1719 |
+
rec = WORMRecord(event_type=f"event_{i}", payload=payload)
|
| 1720 |
+
worm.append(rec)
|
| 1721 |
+
|
| 1722 |
+
# Read all and verify
|
| 1723 |
+
records = list(worm.read())
|
| 1724 |
+
|
| 1725 |
+
assert len(records) == len(payloads)
|
| 1726 |
+
for i, rec in enumerate(records):
|
| 1727 |
+
assert rec.payload == payloads[i]
|
| 1728 |
+
```
|
| 1729 |
+
|
| 1730 |
+
---
|
| 1731 |
+
|
| 1732 |
+
## 6. PERFORMANCE OPTIMIZATION
|
| 1733 |
+
|
| 1734 |
+
### Goal
|
| 1735 |
+
Profile hot paths, optimize to targets, lazy-load modules, cache results, use memory-mapping for large files.
|
| 1736 |
+
|
| 1737 |
+
### 6.1 Routing Latency (`src/routing/jordan_moe.py` — profile + optimize)
|
| 1738 |
+
|
| 1739 |
+
**Target:** <10ms for 8 experts
|
| 1740 |
+
|
| 1741 |
+
```python
|
| 1742 |
+
# Add to JordanMoERouter:
|
| 1743 |
+
|
| 1744 |
+
def profile_routing_latency(self, iterations: int = 1000) -> dict[str, float]:
|
| 1745 |
+
"""
|
| 1746 |
+
Profile routing latency.
|
| 1747 |
+
|
| 1748 |
+
Returns:
|
| 1749 |
+
{
|
| 1750 |
+
"mean_ms": average latency,
|
| 1751 |
+
"p50_ms": 50th percentile,
|
| 1752 |
+
"p99_ms": 99th percentile,
|
| 1753 |
+
"max_ms": maximum,
|
| 1754 |
+
}
|
| 1755 |
+
"""
|
| 1756 |
+
pass
|
| 1757 |
+
|
| 1758 |
+
# Optimization: Cache fixed-point results
|
| 1759 |
+
class JordanMoERouterCached:
|
| 1760 |
+
def __init__(self):
|
| 1761 |
+
self.cache = {} # signal_hash -> weights
|
| 1762 |
+
|
| 1763 |
+
def route_cached(self, signal: dict[str, float]) -> dict[str, float]:
|
| 1764 |
+
"""Route with memoization."""
|
| 1765 |
+
signal_hash = hash(frozenset(signal.items()))
|
| 1766 |
+
|
| 1767 |
+
if signal_hash in self.cache:
|
| 1768 |
+
return self.cache[signal_hash]
|
| 1769 |
+
|
| 1770 |
+
result = self.route(signal)
|
| 1771 |
+
self.cache[signal_hash] = result
|
| 1772 |
+
return result
|
| 1773 |
+
```
|
| 1774 |
+
|
| 1775 |
+
---
|
| 1776 |
+
|
| 1777 |
+
### 6.2 IPC Round-Trip (`src/tools/ipc_router.py` — profile + optimize)
|
| 1778 |
+
|
| 1779 |
+
**Target:** <100μs mmap, <1ms HTTP fallback
|
| 1780 |
+
|
| 1781 |
+
```python
|
| 1782 |
+
async def profile_ipc_latency(router: IPCRouter) -> dict[str, float]:
|
| 1783 |
+
"""
|
| 1784 |
+
Profile IPC round-trip latency.
|
| 1785 |
+
|
| 1786 |
+
Returns:
|
| 1787 |
+
{
|
| 1788 |
+
"mmap_mean_us": microseconds,
|
| 1789 |
+
"mmap_p99_us": 99th percentile,
|
| 1790 |
+
"http_mean_ms": milliseconds,
|
| 1791 |
+
}
|
| 1792 |
+
"""
|
| 1793 |
+
pass
|
| 1794 |
+
```
|
| 1795 |
+
|
| 1796 |
+
---
|
| 1797 |
+
|
| 1798 |
+
### 6.3 Connection Pooling (`src/runtime/providers/*.py`)
|
| 1799 |
+
|
| 1800 |
+
**Add to all providers:**
|
| 1801 |
+
|
| 1802 |
+
```python
|
| 1803 |
+
class ModelProvider:
|
| 1804 |
+
def __init__(self, pool_size: int = 10):
|
| 1805 |
+
self.pool = asyncio.Queue(maxsize=pool_size)
|
| 1806 |
+
# Pre-populate with connections
|
| 1807 |
+
for _ in range(pool_size):
|
| 1808 |
+
self.pool.put_nowait(self._create_connection())
|
| 1809 |
+
|
| 1810 |
+
async def get_connection(self):
|
| 1811 |
+
"""Get connection from pool."""
|
| 1812 |
+
return await self.pool.get()
|
| 1813 |
+
|
| 1814 |
+
async def release_connection(self, conn):
|
| 1815 |
+
"""Return connection to pool."""
|
| 1816 |
+
await self.pool.put(conn)
|
| 1817 |
+
```
|
| 1818 |
+
|
| 1819 |
+
---
|
| 1820 |
+
|
| 1821 |
+
### 6.4 Lazy Module Loading (`src/tools/loader.py` — new/extend)
|
| 1822 |
+
|
| 1823 |
+
```python
|
| 1824 |
+
class ToolLoader:
|
| 1825 |
+
"""Lazy-load tools on demand."""
|
| 1826 |
+
|
| 1827 |
+
def __init__(self):
|
| 1828 |
+
self.loaded_tools = {} # tool_name -> module
|
| 1829 |
+
self.loading_lock = asyncio.Lock()
|
| 1830 |
+
|
| 1831 |
+
async def load_tool(self, tool_name: str):
|
| 1832 |
+
"""Load tool module on first use."""
|
| 1833 |
+
async with self.loading_lock:
|
| 1834 |
+
if tool_name in self.loaded_tools:
|
| 1835 |
+
return self.loaded_tools[tool_name]
|
| 1836 |
+
|
| 1837 |
+
module = __import__(f"src.tools.{tool_name}")
|
| 1838 |
+
self.loaded_tools[tool_name] = module
|
| 1839 |
+
return module
|
| 1840 |
+
```
|
| 1841 |
+
|
| 1842 |
+
---
|
| 1843 |
+
|
| 1844 |
+
### 6.5 Binary IR Memory-Mapping (`src/runtime/machine/binary_ir.py` — extend)
|
| 1845 |
+
|
| 1846 |
+
```python
|
| 1847 |
+
class BinaryIRExecutor:
|
| 1848 |
+
def execute_large_file_with_mmap(self, file_path: Path) -> Any:
|
| 1849 |
+
"""
|
| 1850 |
+
Load large binary files via mmap instead of full read.
|
| 1851 |
+
|
| 1852 |
+
Benefit: O(1) memory regardless of file size.
|
| 1853 |
+
"""
|
| 1854 |
+
import mmap
|
| 1855 |
+
|
| 1856 |
+
with open(file_path, 'rb') as f:
|
| 1857 |
+
with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
|
| 1858 |
+
# Execute against mmap view
|
| 1859 |
+
return self.execute_ir(mm)
|
| 1860 |
+
```
|
| 1861 |
+
|
| 1862 |
+
---
|
| 1863 |
+
|
| 1864 |
+
## 7. DOCUMENTATION
|
| 1865 |
+
|
| 1866 |
+
### Goal
|
| 1867 |
+
Complete docstrings, architecture diagrams, API reference, deployment guide, configuration reference.
|
| 1868 |
+
|
| 1869 |
+
### 7.1 Docstring Convention
|
| 1870 |
+
|
| 1871 |
+
**All public methods must include:**
|
| 1872 |
+
|
| 1873 |
+
```python
|
| 1874 |
+
def my_function(param1: str, param2: int) -> dict[str, Any]:
|
| 1875 |
+
"""
|
| 1876 |
+
One-line summary of what this does.
|
| 1877 |
+
|
| 1878 |
+
Longer description if needed. Mention any important side effects.
|
| 1879 |
+
|
| 1880 |
+
Args:
|
| 1881 |
+
param1: Description of param1
|
| 1882 |
+
param2: Description of param2 (default: X)
|
| 1883 |
+
|
| 1884 |
+
Returns:
|
| 1885 |
+
Description of return value
|
| 1886 |
+
|
| 1887 |
+
Raises:
|
| 1888 |
+
MyError: When condition occurs
|
| 1889 |
+
OtherError: When other condition occurs
|
| 1890 |
+
|
| 1891 |
+
Example:
|
| 1892 |
+
>>> result = my_function("test", 42)
|
| 1893 |
+
>>> print(result)
|
| 1894 |
+
{...}
|
| 1895 |
+
"""
|
| 1896 |
+
pass
|
| 1897 |
+
```
|
| 1898 |
+
|
| 1899 |
+
### 7.2 Architecture Diagram (in README)
|
| 1900 |
+
|
| 1901 |
+
Mermaid diagram showing:
|
| 1902 |
+
- Routing pipeline (input → JordanMoE → expert selection)
|
| 1903 |
+
- Continuity layer (4 paradigms, checkpoint/restore)
|
| 1904 |
+
- Tool dispatch (opcode registry → IPC)
|
| 1905 |
+
- Agent loop (ReAct + Shadow)
|
| 1906 |
+
- Bridge (HTTP server + C frontend)
|
| 1907 |
+
|
| 1908 |
+
---
|
| 1909 |
+
|
| 1910 |
+
### 7.3 API Reference
|
| 1911 |
+
|
| 1912 |
+
Auto-generated from docstrings:
|
| 1913 |
+
|
| 1914 |
+
```
|
| 1915 |
+
src/routing/
|
| 1916 |
+
- jordan_moe.py
|
| 1917 |
+
- SpinFactor
|
| 1918 |
+
- JordanMoERouter
|
| 1919 |
+
src/agents/
|
| 1920 |
+
- react.py
|
| 1921 |
+
- ReActAgent
|
| 1922 |
+
- ReActConfig
|
| 1923 |
+
src/tools/
|
| 1924 |
+
- registry.py
|
| 1925 |
+
- ToolRegistry
|
| 1926 |
+
- ToolDefinition
|
| 1927 |
+
- approval.py
|
| 1928 |
+
- ApprovalEngine
|
| 1929 |
+
- RateLimiter
|
| 1930 |
+
...
|
| 1931 |
+
```
|
| 1932 |
+
|
| 1933 |
+
### 7.4 Deployment Guide
|
| 1934 |
+
|
| 1935 |
+
Sections:
|
| 1936 |
+
- Prerequisites (Python 3.11+)
|
| 1937 |
+
- Installation (pip, virtual env)
|
| 1938 |
+
- Configuration (EngineConfig options)
|
| 1939 |
+
- Starting daemon (systemd / Docker / bare metal)
|
| 1940 |
+
- Health checks (HTTP /health endpoint)
|
| 1941 |
+
- Monitoring (WORM ledger queries, metrics)
|
| 1942 |
+
- Troubleshooting (common errors + recovery)
|
| 1943 |
+
|
| 1944 |
+
### 7.5 Configuration Reference
|
| 1945 |
+
|
| 1946 |
+
```python
|
| 1947 |
+
# All options documented:
|
| 1948 |
+
@dataclass
|
| 1949 |
+
class EngineConfig:
|
| 1950 |
+
"""Complete engine configuration."""
|
| 1951 |
+
|
| 1952 |
+
model_provider: str = "bedrock" # Which model provider
|
| 1953 |
+
model_name: str = "claude-opus"
|
| 1954 |
+
|
| 1955 |
+
continuity_base_dir: Path = Path.home() / ".sovereign" / "continuity"
|
| 1956 |
+
enable_continuity: bool = True
|
| 1957 |
+
|
| 1958 |
+
worm_ledger_path: Path | None = None
|
| 1959 |
+
|
| 1960 |
+
approval_required_for_risky: bool = True
|
| 1961 |
+
rate_limit_policies: dict[str, RateLimitPolicy] = field(default_factory=dict)
|
| 1962 |
+
|
| 1963 |
+
# ... all documented with explanations
|
| 1964 |
+
```
|
| 1965 |
+
|
| 1966 |
+
---
|
| 1967 |
+
|
| 1968 |
+
## 8. CI/CD PIPELINE
|
| 1969 |
+
|
| 1970 |
+
### Goal
|
| 1971 |
+
GitHub Actions workflow with tests, type checking, security scan, coverage reporting.
|
| 1972 |
+
|
| 1973 |
+
### 8.1 `.github/workflows/ci.yml` (~150 lines)
|
| 1974 |
+
|
| 1975 |
+
```yaml
|
| 1976 |
+
name: CI
|
| 1977 |
+
|
| 1978 |
+
on:
|
| 1979 |
+
push:
|
| 1980 |
+
branches: [main, develop]
|
| 1981 |
+
pull_request:
|
| 1982 |
+
branches: [main]
|
| 1983 |
+
|
| 1984 |
+
jobs:
|
| 1985 |
+
test:
|
| 1986 |
+
runs-on: ubuntu-latest
|
| 1987 |
+
strategy:
|
| 1988 |
+
matrix:
|
| 1989 |
+
python-version: ["3.11", "3.12", "3.13"]
|
| 1990 |
+
steps:
|
| 1991 |
+
- uses: actions/checkout@v3
|
| 1992 |
+
|
| 1993 |
+
- name: Set up Python ${{ matrix.python-version }}
|
| 1994 |
+
uses: actions/setup-python@v4
|
| 1995 |
+
with:
|
| 1996 |
+
python-version: ${{ matrix.python-version }}
|
| 1997 |
+
|
| 1998 |
+
- name: Install dependencies
|
| 1999 |
+
run: |
|
| 2000 |
+
pip install -r requirements.txt
|
| 2001 |
+
pip install pytest pytest-cov pyright bandit
|
| 2002 |
+
|
| 2003 |
+
- name: Run pytest
|
| 2004 |
+
run: pytest tests/ --cov=src --cov-report=xml
|
| 2005 |
+
|
| 2006 |
+
- name: Upload coverage
|
| 2007 |
+
uses: codecov/codecov-action@v3
|
| 2008 |
+
|
| 2009 |
+
typecheck:
|
| 2010 |
+
runs-on: ubuntu-latest
|
| 2011 |
+
steps:
|
| 2012 |
+
- uses: actions/checkout@v3
|
| 2013 |
+
- uses: actions/setup-python@v4
|
| 2014 |
+
with:
|
| 2015 |
+
python-version: "3.11"
|
| 2016 |
+
|
| 2017 |
+
- name: Install pyright
|
| 2018 |
+
run: pip install pyright
|
| 2019 |
+
|
| 2020 |
+
- name: Type check
|
| 2021 |
+
run: pyright src/
|
| 2022 |
+
|
| 2023 |
+
nasm_check:
|
| 2024 |
+
runs-on: ubuntu-latest
|
| 2025 |
+
steps:
|
| 2026 |
+
- uses: actions/checkout@v3
|
| 2027 |
+
|
| 2028 |
+
- name: Install NASM
|
| 2029 |
+
run: sudo apt-get install -y nasm
|
| 2030 |
+
|
| 2031 |
+
- name: Syntax check
|
| 2032 |
+
run: nasm -f elf64 -o /dev/null native/*.asm
|
| 2033 |
+
|
| 2034 |
+
c_compile:
|
| 2035 |
+
runs-on: ubuntu-latest
|
| 2036 |
+
steps:
|
| 2037 |
+
- uses: actions/checkout@v3
|
| 2038 |
+
|
| 2039 |
+
- name: Compile C core
|
| 2040 |
+
run: gcc -c -o /dev/null native/ipc_core.c -Wall -Wextra
|
| 2041 |
+
|
| 2042 |
+
security:
|
| 2043 |
+
runs-on: ubuntu-latest
|
| 2044 |
+
steps:
|
| 2045 |
+
- uses: actions/checkout@v3
|
| 2046 |
+
- uses: actions/setup-python@v4
|
| 2047 |
+
with:
|
| 2048 |
+
python-version: "3.11"
|
| 2049 |
+
|
| 2050 |
+
- name: Bandit security scan
|
| 2051 |
+
run: |
|
| 2052 |
+
pip install bandit
|
| 2053 |
+
bandit -r src/ -f json -o bandit-report.json
|
| 2054 |
+
|
| 2055 |
+
- name: Upload security report
|
| 2056 |
+
uses: actions/upload-artifact@v3
|
| 2057 |
+
with:
|
| 2058 |
+
name: bandit-report
|
| 2059 |
+
path: bandit-report.json
|
| 2060 |
+
```
|
| 2061 |
+
|
| 2062 |
+
### 8.2 Pre-Commit Hooks (`.pre-commit-config.yaml`)
|
| 2063 |
+
|
| 2064 |
+
```yaml
|
| 2065 |
+
repos:
|
| 2066 |
+
- repo: https://github.com/psf/black
|
| 2067 |
+
rev: 23.1.0
|
| 2068 |
+
hooks:
|
| 2069 |
+
- id: black
|
| 2070 |
+
language_version: python3.11
|
| 2071 |
+
|
| 2072 |
+
- repo: https://github.com/PyCQA/isort
|
| 2073 |
+
rev: 5.12.0
|
| 2074 |
+
hooks:
|
| 2075 |
+
- id: isort
|
| 2076 |
+
args: [--profile=black]
|
| 2077 |
+
|
| 2078 |
+
- repo: https://github.com/RobertCraigie/pyright-python
|
| 2079 |
+
rev: v1.1.280
|
| 2080 |
+
hooks:
|
| 2081 |
+
- id: pyright
|
| 2082 |
+
```
|
| 2083 |
+
|
| 2084 |
+
---
|
| 2085 |
+
|
| 2086 |
+
## IMPLEMENTATION PRIORITY
|
| 2087 |
+
|
| 2088 |
+
### Phase 1: Crash Prevention (Week 1–2)
|
| 2089 |
+
1. Edge cases (3.1–3.10) — all error handling
|
| 2090 |
+
2. Input validation (4.5)
|
| 2091 |
+
3. Unit tests (5.1) — at least 50% coverage
|
| 2092 |
+
|
| 2093 |
+
### Phase 2: Security (Week 2–3)
|
| 2094 |
+
1. PathJail extensions (3.1)
|
| 2095 |
+
2. SSRFGuard extensions (3.2)
|
| 2096 |
+
3. Rate limiting (4.2)
|
| 2097 |
+
4. Cost tracking (4.3)
|
| 2098 |
+
5. Emergency kill switch (4.4)
|
| 2099 |
+
6. Security tests
|
| 2100 |
+
|
| 2101 |
+
### Phase 3: Features (Week 3–4)
|
| 2102 |
+
1. Terminal IDE (1.1–1.3)
|
| 2103 |
+
2. Accessibility (2.1–2.3)
|
| 2104 |
+
3. Integration tests (5.2)
|
| 2105 |
+
|
| 2106 |
+
### Phase 4: Polish (Week 4–6)
|
| 2107 |
+
1. Performance optimization (6)
|
| 2108 |
+
2. Property-based tests (5.3)
|
| 2109 |
+
3. Documentation (7)
|
| 2110 |
+
4. CI/CD pipeline (8)
|
| 2111 |
+
|
| 2112 |
+
---
|
| 2113 |
+
|
| 2114 |
+
## DEFINITION OF DONE
|
| 2115 |
+
|
| 2116 |
+
- [ ] All code passes pytest (2000+ lines coverage)
|
| 2117 |
+
- [ ] All public methods have docstrings (format: § 7.1)
|
| 2118 |
+
- [ ] CI/CD pipeline green on all Python 3.11–3.13
|
| 2119 |
+
- [ ] Security scan (bandit) passes
|
| 2120 |
+
- [ ] Type check (pyright) passes with 0 errors
|
| 2121 |
+
- [ ] WORM append-only invariant holds (proven by tests)
|
| 2122 |
+
- [ ] Routing latency < 10ms (profiled)
|
| 2123 |
+
- [ ] PathJail blocks all 20 traversal attacks (test cases)
|
| 2124 |
+
- [ ] Terminal IDE renders without flicker
|
| 2125 |
+
- [ ] Accessibility complies with WCAG 2.1 AA (screen reader compatible)
|
| 2126 |
+
- [ ] No external dependencies beyond Python 3.11 stdlib
|
| 2127 |
+
- [ ] Deployment guide tested on 3 platforms (Linux/macOS/Windows)
|
| 2128 |
+
|
| 2129 |
+
---
|
| 2130 |
+
|
| 2131 |
+
## CRITICAL NOTES FOR CODEX
|
| 2132 |
+
|
| 2133 |
+
1. **No external deps** — Use only Python stdlib. No pip packages except dev tools (pytest, pyright, etc).
|
| 2134 |
+
2. **WORM immutability** — Every test must verify chain integrity. If one byte changes, chain breaks.
|
| 2135 |
+
3. **Continuity restart** — Test that agent survives daemon restart and can resume from exact step.
|
| 2136 |
+
4. **PathJail escape** — Try 50+ creative escapes (UNC paths, drive letters, junctions on Windows). All must fail.
|
| 2137 |
+
5. **Async/await** — All I/O must be async. No blocking calls in agent loop.
|
| 2138 |
+
6. **Error messages** — Plain English. No jargon. Include recovery steps.
|
| 2139 |
+
7. **Performance** — Profile before optimizing. Targets are hard (10ms routing, 100us IPC).
|
| 2140 |
+
8. **Docstrings** — Non-negotiable. Every public function must be documented.
|
| 2141 |
+
|
| 2142 |
+
---
|
| 2143 |
+
|
| 2144 |
+
**End of Specification**
|
| 2145 |
+
|
| 2146 |
+
Document version: 1.0
|
| 2147 |
+
Generated: 2026-08-06
|
| 2148 |
+
Scope: Full production hardening of Sovereign Python LLM Engine
|
| 2149 |
+
Target audience: Codex AI code implementation agent
|
README.md
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```
|
| 2 |
+
____ _ ______ _
|
| 3 |
+
/ ___| _____ _____ _ __ ___(_) __ _ _ __ | ____| _ __ __ _(_)_ __ ___
|
| 4 |
+
\___ \ / _ \ \ / / _ \ '__/ _ \ |/ _` | '_ \ | _| | '_ \ / _` | | '_ \ / _ \
|
| 5 |
+
___) | (_) \ V / __/ | | __/ | (_| | | | | | |___| | | | (_| | | | | | __/
|
| 6 |
+
|____/ \___/ \_/ \___|_| \___|_|\__, |_| |_| |_____|_| |_|\__, |_|_| |_|\___|
|
| 7 |
+
|___/ |___/ v2.0
|
| 8 |
+
```
|
| 9 |
+
|
| 10 |
+

|
| 11 |
+

|
| 12 |
+

|
| 13 |
+

|
| 14 |
+

|
| 15 |
+

|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## What This Is
|
| 20 |
+
|
| 21 |
+
A **complete LLM agent development environment**: native C desktop IDE connected to a Python inference engine via memory-mapped IPC.
|
| 22 |
+
|
| 23 |
+
This is not a wrapper. Not a LangChain clone. Not vibe-coded. This is:
|
| 24 |
+
|
| 25 |
+
- A **C Win32 IDE** with text editor, ConPTY terminal, LSP, DAP debugging, Direct2D rendering, git integration — 47 source files, 4,459 lines of C
|
| 26 |
+
- A **Python LLM engine** with custom MoE routing, binary storage, four-paradigm continuity, 34 tools, ReAct agents — 87 modules, 38,576 lines
|
| 27 |
+
- A **NASM x86-64 assembly layer** with native runtime, QRA tensor ops, Jordan block computation, NAND kernel, IPC dispatcher — 7 files, 7,019 lines
|
| 28 |
+
- A **C IPC dispatcher** with mmap ring buffer, opcode jump table, 50μs polling — 390 lines
|
| 29 |
+
|
| 30 |
+
**50,444 lines of production code. 142 source files. Pure stdlib Python + raw C + NASM.**
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## Table of Contents
|
| 35 |
+
|
| 36 |
+
- [The System](#the-system)
|
| 37 |
+
- [Quick Start](#quick-start)
|
| 38 |
+
- [The IDE](#the-ide)
|
| 39 |
+
- [The Engine](#the-engine)
|
| 40 |
+
- [Machine Code Layer](#machine-code-layer)
|
| 41 |
+
- [Routing Pipeline](#routing-pipeline)
|
| 42 |
+
- [Continuity Layer](#continuity-layer)
|
| 43 |
+
- [Tool System](#tool-system)
|
| 44 |
+
- [Security Architecture](#security-architecture)
|
| 45 |
+
- [Configuration](#configuration)
|
| 46 |
+
- [The Mathematics](#the-mathematics)
|
| 47 |
+
- [Origin: The DSL](#origin-the-dsl)
|
| 48 |
+
- [Papers](#papers)
|
| 49 |
+
- [Line Count](#line-count)
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
## The System
|
| 54 |
+
|
| 55 |
+
```mermaid
|
| 56 |
+
graph TB
|
| 57 |
+
subgraph "C Win32 IDE (4,459 lines)"
|
| 58 |
+
EDITOR[Editor + Buffer]
|
| 59 |
+
TERM[ConPTY Terminal]
|
| 60 |
+
LSP[LSP Client]
|
| 61 |
+
DAP[DAP Debugger]
|
| 62 |
+
UI[UI Layout + Direct2D]
|
| 63 |
+
GIT[Git Integration]
|
| 64 |
+
FCL[FCL Evaluator]
|
| 65 |
+
end
|
| 66 |
+
|
| 67 |
+
subgraph "IPC Bridge"
|
| 68 |
+
HTTP[HTTP Bridge :19000]
|
| 69 |
+
MMAP[mmap Ring Buffer]
|
| 70 |
+
PIPE[Named Pipe Protocol]
|
| 71 |
+
end
|
| 72 |
+
|
| 73 |
+
subgraph "Python Engine (38,576 lines)"
|
| 74 |
+
ROUTE[11-Stage Routing Pipeline]
|
| 75 |
+
AGENT[ReAct Agent + Shadow Observer]
|
| 76 |
+
TOOLS[34 Tools x 9 Namespaces]
|
| 77 |
+
CONT[4-Paradigm Continuity]
|
| 78 |
+
WORM[Binary WORM Storage]
|
| 79 |
+
QRA[QRA Router - 6 Glyphs]
|
| 80 |
+
end
|
| 81 |
+
|
| 82 |
+
subgraph "Native Layer (7,409 lines)"
|
| 83 |
+
ASM[NASM x86-64 Runtime]
|
| 84 |
+
IPC_C[C IPC Dispatcher]
|
| 85 |
+
VM[Stack VM + NAND Opcodes]
|
| 86 |
+
JORDAN_ASM[Jordan Blocks SSE2/AVX2]
|
| 87 |
+
end
|
| 88 |
+
|
| 89 |
+
EDITOR --> HTTP
|
| 90 |
+
TERM --> PIPE
|
| 91 |
+
HTTP --> ROUTE
|
| 92 |
+
MMAP --> IPC_C
|
| 93 |
+
ROUTE --> AGENT
|
| 94 |
+
AGENT --> TOOLS
|
| 95 |
+
AGENT --> CONT
|
| 96 |
+
AGENT --> WORM
|
| 97 |
+
IPC_C --> ASM
|
| 98 |
+
ASM --> JORDAN_ASM
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
## Quick Start
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
git clone https://github.com/SNAPKITTYWEST/sovereign-engine-v2.git
|
| 107 |
+
cd sovereign-engine-v2
|
| 108 |
+
|
| 109 |
+
# Run the engine (zero dependencies)
|
| 110 |
+
python -c "
|
| 111 |
+
import asyncio
|
| 112 |
+
from src.sovereign import SovereignEngine, EngineConfig
|
| 113 |
+
|
| 114 |
+
async def main():
|
| 115 |
+
engine = SovereignEngine(EngineConfig())
|
| 116 |
+
result = await engine.run('Write a fibonacci function')
|
| 117 |
+
print(result)
|
| 118 |
+
engine.shutdown()
|
| 119 |
+
|
| 120 |
+
asyncio.run(main())
|
| 121 |
+
"
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
# Build the C IDE (Windows — requires CMake + MSVC)
|
| 126 |
+
cd ide/native
|
| 127 |
+
cmake -B build -G "Visual Studio 17 2022"
|
| 128 |
+
cmake --build build --config Release
|
| 129 |
+
./build/Release/sovereign-ide.exe
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
## The IDE
|
| 135 |
+
|
| 136 |
+
**Location:** `ide/native/` — 47 C source files, 4,459 lines
|
| 137 |
+
|
| 138 |
+
This is a native Win32 application. No Electron. No JavaScript. No web view. Direct2D GPU rendering, ConPTY pseudo-terminal, Win32 message loop.
|
| 139 |
+
|
| 140 |
+
| Directory | Purpose | Key Files |
|
| 141 |
+
|-----------|---------|-----------|
|
| 142 |
+
| `core/` | Memory arena, event system, strings | `arena.c` (pool allocator), `events.c` (pub/sub) |
|
| 143 |
+
| `editor/` | Gap buffer text editor | `buffer.c` (insert/delete O(1)), `document.c` (file model) |
|
| 144 |
+
| `terminal/` | Embedded terminal | `conpty.c` (Windows ConPTY API) |
|
| 145 |
+
| `ui/` | Layout engine | `layout.c` (split panes), `status_bar.h`, `project_tree.h` |
|
| 146 |
+
| `bridge/` | Engine connection | `bridge_http.c` (HTTP client to Python :19000) |
|
| 147 |
+
| `chat/` | Agent interaction | `pipe_client.c` (named pipe), `protocol.c` (message framing) |
|
| 148 |
+
| `lsp/` | Language intelligence | `client.c` (Language Server Protocol) |
|
| 149 |
+
| `dap/` | Debugging | Debug Adapter Protocol integration |
|
| 150 |
+
| `git/` | Version control | `repository.c` (status, diff, commit) |
|
| 151 |
+
| `fcl/` | Command language | `evaluator.c` (Formal Command Language interpreter) |
|
| 152 |
+
| `graphics/` | Rendering | `d2d_renderer.h` (Direct2D hardware-accelerated) |
|
| 153 |
+
| `platform/windows/` | OS layer | `application.c`, `window.c`, `shell.c` |
|
| 154 |
+
| `build/` | Build system | `cmake_runner.c` (invoke cmake from IDE) |
|
| 155 |
+
|
| 156 |
+
### How the IDE Talks to the Engine
|
| 157 |
+
|
| 158 |
+
```
|
| 159 |
+
┌────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
| 160 |
+
│ C IDE │ HTTP │ Python Bridge │ │ Engine │
|
| 161 |
+
│ │────────>│ :19000 │───────>│ Routing │
|
| 162 |
+
│ bridge_http.c │ JSON │ http_server.py │ │ Agent │
|
| 163 |
+
│ │<────────│ │<───────│ Tools │
|
| 164 |
+
└────────────────┘ └──────────────────┘ └─────────────────┘
|
| 165 |
+
|
| 166 |
+
┌────────────────┐ ┌──────────────────┐
|
| 167 |
+
│ C IDE │ mmap │ C IPC Core │ (native tools — no HTTP, no JSON)
|
| 168 |
+
│ │────────>│ ipc_core.c │ Latency: ~100μs round trip
|
| 169 |
+
│ (direct call) │<────────│ opcode dispatch │
|
| 170 |
+
└────────────────┘ └──────────────────┘
|
| 171 |
+
|
| 172 |
+
┌────────────────┐ ┌──────────────────┐
|
| 173 |
+
│ C IDE │ pipe │ Agent Chat │ (streaming agent responses)
|
| 174 |
+
│ pipe_client.c │────────>│ python_daemon │
|
| 175 |
+
│ │<────────│ :19002 TCP │
|
| 176 |
+
└────────────────┘ └──────────────────┘
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
---
|
| 180 |
+
|
| 181 |
+
## The Engine
|
| 182 |
+
|
| 183 |
+
**Location:** `src/` — 87 Python modules, 38,576 lines
|
| 184 |
+
|
| 185 |
+
Every module is **pure Python 3.11+ stdlib**. Zero external dependencies. No pip install needed.
|
| 186 |
+
|
| 187 |
+
| Package | Lines | What It Does |
|
| 188 |
+
|---------|-------|-------------|
|
| 189 |
+
| `src/routing/` | 2,800 | 11-stage MoE pipeline: regex → AST → symbolic graph → Jordan transform → Jacobian → constraints → sparse activation → NAND filter → dispatch → merge |
|
| 190 |
+
| `src/runtime/machine/` | 9,251 | CPython bytecode assembler, .pyc marshal codec, ctypes C bridge, SOVEREIGN_IR binary format, stack VM with NAND opcodes, x86-64 machine code generator |
|
| 191 |
+
| `src/tools/` | 3,500 | 34 tools (filesystem, code, git, database, documents, web, embeddings, audio, pytorch), IPC router, opcode registry, approval engine, supervisor |
|
| 192 |
+
| `src/continuity/` | 1,536 | Env bitmask, seed chain, inode flags, shared memory, unified manager |
|
| 193 |
+
| `src/agents/` | 1,500 | ReAct loop (thought→action→observation→reflection), shadow observer, MCTS |
|
| 194 |
+
| `src/retrieval/` | 1,500 | Semantic chunker (6 strategies), vector store, RAG pipeline, parallel ingest |
|
| 195 |
+
| `src/daemon/` | 823 | Asyncio TCP daemon (:19002), swarm orchestration (fan_out, map_reduce, race) |
|
| 196 |
+
| `src/bridge/` | 900 | HTTP server (:19000), stdio JSON-RPC, routing trace collector, key manager |
|
| 197 |
+
| `src/core/` | 700 | Binary WORM storage, evidence ledger, Ed25519 crypto, path jail, SSRF guard |
|
| 198 |
+
| `src/runtime/providers/` | 600 | Bedrock, OpenRouter, Ollama, Anthropic, OpenAI adapters + QRA router |
|
| 199 |
+
| `src/models/` | 500 | Pydantic entities, state machines |
|
| 200 |
+
| `src/scanner/` | 400 | AST analyzer, dependency graph builder |
|
| 201 |
+
| `src/inference/` | 300 | Quantum MoE (SpinFactor composition) |
|
| 202 |
+
| `src/mcp/` | 250 | Model Context Protocol server |
|
| 203 |
+
| `src/cli/` | 200 | Command-line interface |
|
| 204 |
+
|
| 205 |
+
---
|
| 206 |
+
|
| 207 |
+
## Machine Code Layer
|
| 208 |
+
|
| 209 |
+
**Location:** `src/runtime/machine/` (Python) + `native/asm/` (NASM)
|
| 210 |
+
|
| 211 |
+
This is not a toy abstraction. These are real implementations:
|
| 212 |
+
|
| 213 |
+
### Python Machine Code (`src/runtime/machine/` — 9,251 lines)
|
| 214 |
+
|
| 215 |
+
| Module | Lines | What It Actually Does |
|
| 216 |
+
|--------|-------|----------------------|
|
| 217 |
+
| `bytecode_assembler.py` | 1,711 | Emits real CPython opcodes (LOAD_FAST, CALL_FUNCTION, etc). Produces executable code objects. |
|
| 218 |
+
| `marshal_codec.py` | 1,435 | Reads and writes actual .pyc binary format (magic number, flags, code objects, consts table). |
|
| 219 |
+
| `ctypes_bridge.py` | 970 | Builds C struct definitions from Python, manages MemoryArena for native allocations. |
|
| 220 |
+
| `binary_ir.py` | 1,492 | SOVEREIGN_IR format: 32-byte fixed-width node records. Opcode + flags + operands + type tag. Scannable without parsing. |
|
| 221 |
+
| `vm_executor.py` | 1,680 | Stack-based virtual machine. 40+ opcodes including custom NAND, JORDAN_MUL, ENTROPY_CHECK. Runs SOVEREIGN_IR bytecode. |
|
| 222 |
+
| `machine_code_gen.py` | 1,104 | Emits raw x86-64 bytes. REX prefixes, ModR/M encoding, register allocation. Produces executable buffers via mmap+mprotect. |
|
| 223 |
+
| `dsl_validator.py` | 658 | Validates all DSL constraints: Boolean kernel (NAND truth table), entropy ≤ 0.20, trust axiom, glyph injectivity, DAG acyclicity. Blake2b proof hash. |
|
| 224 |
+
|
| 225 |
+
### NASM x86-64 Assembly (`native/asm/` — 7,019 lines)
|
| 226 |
+
|
| 227 |
+
| File | Lines | What It Actually Does |
|
| 228 |
+
|------|-------|----------------------|
|
| 229 |
+
| `sovereign_runtime.asm` | 2,459 | Main runtime: WORM append (struct pack → write syscall), ring buffer management, signal handling, mmap allocation |
|
| 230 |
+
| `ipc_dispatcher.asm` | 1,071 | Polls mmap region, reads 16-bit opcode, jump table dispatch (34 entries), writes response struct back |
|
| 231 |
+
| `qra_tensor.asm` | 905 | 6x6 matrix multiply (packed SSE2), QLG balance check (x₀²+x₁²+x₂²=1 over ℤ), glyph classification |
|
| 232 |
+
| `nand_kernel.asm` | 790 | NAND gate truth table in SIMD, builds NOT→AND→OR→IMPLIES→EQUAL per DSL BooleanKernel spec |
|
| 233 |
+
| `jordan_blocks.asm` | 770 | SpinFactor product in SSE2/AVX2: scalar×scalar + dot product, scalar×vector + scalar×vector. Batch 4 elements. |
|
| 234 |
+
| `entropy_gate.asm` | 552 | Shannon entropy H=-Σp·ln(p) via x87 FPU. Compare against 0.20 threshold. Set carry flag on violation. |
|
| 235 |
+
| `sovereign_link.asm` | 472 | Public symbol table: exports all above as callable from C (System V ABI on Linux, MS x64 on Windows) |
|
| 236 |
+
|
| 237 |
+
---
|
| 238 |
+
|
| 239 |
+
## Routing Pipeline
|
| 240 |
+
|
| 241 |
+
11 stages. Every stage has a mathematical role. This is not a prompt chain.
|
| 242 |
+
|
| 243 |
+
```
|
| 244 |
+
User Input
|
| 245 |
+
│
|
| 246 |
+
├─── Stage 1: Regex Parser ──────── Tokenize. Strip dangerous patterns.
|
| 247 |
+
│ Blocklist: eval, exec, import, __,
|
| 248 |
+
│ base64, <script, <!ENTITY, SYSTEM
|
| 249 |
+
│
|
| 250 |
+
├─── Stage 2: AST Builder ───────── Build INVERTED syntax tree.
|
| 251 |
+
│ Structural nodes: routing_weight=1.0
|
| 252 |
+
│ Payload leaves: routing_weight=0.0
|
| 253 |
+
│ Payloads CANNOT propagate upward.
|
| 254 |
+
│
|
| 255 |
+
├─── Stage 3: Symbolic Graph ────── Adjacency matrix of signal flow.
|
| 256 |
+
│ Nodes = intent categories.
|
| 257 |
+
│ Edges = co-occurrence weights.
|
| 258 |
+
│
|
| 259 |
+
├─── Stage 4: Jordan Transform ──── SpinFactor composition.
|
| 260 |
+
│ (α,v) ∘ (β,w) = (αβ+⟨v,w⟩, αw+βv)
|
| 261 |
+
│ Non-associative: topology matters.
|
| 262 |
+
│ Gershgorin eigenvalue bounds.
|
| 263 |
+
│
|
| 264 |
+
├─── Stage 5: Jacobian Lens ─────── ∂routing/∂signal via finite diffs.
|
| 265 |
+
│ Sensitivity analysis: which input
|
| 266 |
+
│ features drive which expert weights.
|
| 267 |
+
│
|
| 268 |
+
├─── Stage 6: Constraint Eval ───── Boolean expert mask.
|
| 269 |
+
│ Spectral radius must be < 10.
|
| 270 |
+
│ Entropy must be ≤ 0.20 nats.
|
| 271 |
+
│
|
| 272 |
+
├─── Stage 7: Sparse Activation ─── Top-k expert selection.
|
| 273 |
+
│ Only k experts get nonzero weight.
|
| 274 |
+
│ Rest are zeroed (not softmaxed).
|
| 275 |
+
│
|
| 276 |
+
├─── Stage 8: NAND Filter ──────── Conflict suppression.
|
| 277 |
+
│ NAND(A,B) = suppress lower-weight
|
| 278 |
+
│ when both experts claim same input.
|
| 279 |
+
│
|
| 280 |
+
├─── Stage 9: Agent Dispatch ────── Concurrent asyncio execution.
|
| 281 |
+
│ Each active expert runs in parallel.
|
| 282 |
+
│ Timeout per expert (configurable).
|
| 283 |
+
│
|
| 284 |
+
├─── Stage 10: Merge Output ─────── 4 strategies: concatenate, vote,
|
| 285 |
+
│ weighted_sum, first_success.
|
| 286 |
+
│
|
| 287 |
+
└─── Stage 11: WORM Seal ────────── Blake2b hash of routing decision.
|
| 288 |
+
Ed25519 signature. Append to chain.
|
| 289 |
+
Decision is now immutable.
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
---
|
| 293 |
+
|
| 294 |
+
## Continuity Layer
|
| 295 |
+
|
| 296 |
+
The agent doesn't lose state. Ever. Four independent persistence mechanisms sync on every state transition:
|
| 297 |
+
|
| 298 |
+
| # | Paradigm | Storage | Speed | What Survives |
|
| 299 |
+
|---|----------|---------|-------|---------------|
|
| 300 |
+
| 1 | **Env bitmask** | `os.environ` (64-bit packed) | Instant | `os.execv` hot restart — same PID, new binary |
|
| 301 |
+
| 2 | **Seed chain** | Blake2b derivation (24 bytes total) | Instant | Full history compressed to one hash. Deterministic replay from any point. |
|
| 302 |
+
| 3 | **Inode flags** | Zero-byte files + `stat()` | Kernel cache | Process crash. Kernel dcache survives OOM kill. |
|
| 303 |
+
| 4 | **Shared memory** | ctypes struct (4KB mmap block) | RAM speed | Cross-process visibility. No serialization. |
|
| 304 |
+
|
| 305 |
+
If ANY of the four has state, the agent resumes. `ContinuityManager` unifies all four behind one API:
|
| 306 |
+
|
| 307 |
+
```python
|
| 308 |
+
cm = ContinuityManager(base_dir=Path("~/.sovereign"), agent_id="react_1")
|
| 309 |
+
cm.transition("IDLE", "THINKING") # updates all 4 backends
|
| 310 |
+
cm.advance_op("THINK:plan task") # advances seed chain
|
| 311 |
+
cm.increment_step() # syncs step across all 4
|
| 312 |
+
snapshot = cm.snapshot() # reads from fastest available
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
---
|
| 316 |
+
|
| 317 |
+
## Tool System
|
| 318 |
+
|
| 319 |
+
34 tools with native opcode dispatch:
|
| 320 |
+
|
| 321 |
+
<details>
|
| 322 |
+
<summary><strong>All 34 tools with opcodes (click to expand)</strong></summary>
|
| 323 |
+
|
| 324 |
+
| Opcode | Namespace | Tool | Description |
|
| 325 |
+
|--------|-----------|------|-------------|
|
| 326 |
+
| `0x0001` | filesystem | read | Read file contents (PathJail enforced) |
|
| 327 |
+
| `0x0002` | filesystem | write | Write file (PathJail enforced) |
|
| 328 |
+
| `0x0003` | filesystem | list | List directory |
|
| 329 |
+
| `0x0004` | filesystem | delete | Remove file |
|
| 330 |
+
| `0x0005` | filesystem | move | Move/rename |
|
| 331 |
+
| `0x0006` | filesystem | search | Pattern search (ripgrep-style) |
|
| 332 |
+
| `0x0007` | code | execute | Run code in sandbox |
|
| 333 |
+
| `0x0008` | code | analyze | AST analysis + symbol extraction |
|
| 334 |
+
| `0x0009` | git | status | Repository status |
|
| 335 |
+
| `0x000A` | git | diff | Show changes |
|
| 336 |
+
| `0x000B` | git | commit | Create commit |
|
| 337 |
+
| `0x000C` | git | log | Commit history |
|
| 338 |
+
| `0x000D` | git | branch | Branch operations |
|
| 339 |
+
| `0x000E` | git | checkout | Switch branch |
|
| 340 |
+
| `0x000F` | git | merge | Merge branches |
|
| 341 |
+
| `0x0010` | git | stash | Stash changes |
|
| 342 |
+
| `0x0011` | git | remote | Remote operations |
|
| 343 |
+
| `0x0012` | git | tag | Tag management |
|
| 344 |
+
| `0x0013` | database | query | Execute SQL (parameterized) |
|
| 345 |
+
| `0x0014` | database | schema | Schema introspection |
|
| 346 |
+
| `0x0015` | database | migrate | Run migrations |
|
| 347 |
+
| `0x0016` | documents | parse_pdf | Extract PDF text |
|
| 348 |
+
| `0x0017` | documents | parse_docx | Extract DOCX content |
|
| 349 |
+
| `0x0018` | documents | render_md | Render Markdown |
|
| 350 |
+
| `0x0019` | documents | parse_html | Extract HTML text |
|
| 351 |
+
| `0x001A` | web | search | Web search (SSRFGuard enforced) |
|
| 352 |
+
| `0x001B` | web | fetch | HTTP fetch (SSRFGuard enforced) |
|
| 353 |
+
| `0x001C` | web | scrape | Page scraping |
|
| 354 |
+
| `0x001D` | embeddings | encode | Generate embeddings |
|
| 355 |
+
| `0x001E` | embeddings | search | Similarity search |
|
| 356 |
+
| `0x001F` | audio | transcribe | Speech-to-text |
|
| 357 |
+
| `0x0020` | audio | synthesize | Text-to-speech |
|
| 358 |
+
| `0x0021` | pytorch | inference | Model inference |
|
| 359 |
+
| `0x0022` | pytorch | check_cuda | GPU availability |
|
| 360 |
+
|
| 361 |
+
</details>
|
| 362 |
+
|
| 363 |
+
**Two dispatch paths:**
|
| 364 |
+
- **HTTP** (universal): IDE → JSON POST → parse → handler → JSON response (~5ms)
|
| 365 |
+
- **Native mmap** (local): IDE → write opcode to ring buffer → C dispatcher reads → handler → write response (~100μs)
|
| 366 |
+
|
| 367 |
+
The IPC ring buffer is a 64KB mmap region shared between the C IDE and the Python engine. The C dispatcher (`native/dispatcher/ipc_core.c`) polls at 50μs intervals and dispatches via a jump table indexed by the 16-bit opcode.
|
| 368 |
+
|
| 369 |
+
---
|
| 370 |
+
|
| 371 |
+
## Security Architecture
|
| 372 |
+
|
| 373 |
+
| Layer | Threat | Defense |
|
| 374 |
+
|-------|--------|---------|
|
| 375 |
+
| **PathJail** | `../../etc/passwd`, symlink escape, null bytes | Resolve path → check against allowed roots → reject if outside |
|
| 376 |
+
| **SSRFGuard** | `http://169.254.169.254/metadata`, `http://10.0.0.1` | Block all private IPs, link-local, metadata endpoints |
|
| 377 |
+
| **Inverted AST** | Payload injection into routing decisions | Structural nodes (weight=1) route. Payload leaves (weight=0) can NEVER propagate upward. |
|
| 378 |
+
| **NAND Filter** | Two experts both claiming same input (conflict) | NAND suppresses the lower-weight expert. Prevents split-brain. |
|
| 379 |
+
| **Binary WORM** | JSONL injection, newline attacks, log corruption | 152-byte struct headers. No text parsing anywhere. Append-only file mode. |
|
| 380 |
+
| **ERE Gates** | Secret leakage, eval injection, infinite loops, telemetry | P1: no hardcoded secrets. P2: no eval/exec/import. P3: no `while True` without break. P4: no analytics. P5: SHA-256 audit seal. |
|
| 381 |
+
| **Approval Engine** | Unauthorized tool execution | Risk-level classification. High-risk tools require explicit approval. |
|
| 382 |
+
| **Chain Verification** | Tampered ledger entries | Every WORM record hashes the previous record. Break one → break all downstream. |
|
| 383 |
+
|
| 384 |
+
---
|
| 385 |
+
|
| 386 |
+
## Configuration
|
| 387 |
+
|
| 388 |
+
```python
|
| 389 |
+
from src.sovereign import SovereignEngine, EngineConfig
|
| 390 |
+
from pathlib import Path
|
| 391 |
+
|
| 392 |
+
engine = SovereignEngine(EngineConfig(
|
| 393 |
+
allowed_roots=[Path.cwd(), Path("/data")],
|
| 394 |
+
ledger_path=Path("./sovereign.worm"),
|
| 395 |
+
continuity_dir=Path.home() / ".sovereign" / "continuity",
|
| 396 |
+
max_steps=15,
|
| 397 |
+
enable_shadow=True,
|
| 398 |
+
enable_ipc=True,
|
| 399 |
+
agent_id="my_agent",
|
| 400 |
+
))
|
| 401 |
+
```
|
| 402 |
+
|
| 403 |
+
| Option | Type | Default | Effect |
|
| 404 |
+
|--------|------|---------|--------|
|
| 405 |
+
| `allowed_roots` | `list[Path]` | `[cwd()]` | PathJail boundaries — filesystem tools cannot access anything outside |
|
| 406 |
+
| `ledger_path` | `Path` | `./sovereign.worm` | Binary WORM ledger location |
|
| 407 |
+
| `continuity_dir` | `Path` | `~/.sovereign/continuity` | Where the 4 paradigms store state |
|
| 408 |
+
| `max_steps` | `int` | `15` | Hard limit on agent steps (prevents runaway) |
|
| 409 |
+
| `enable_shadow` | `bool` | `True` | Shadow observer watches for anomalies (cost spikes, loops, failures) |
|
| 410 |
+
| `enable_ipc` | `bool` | `True` | Enable native mmap dispatch (disable for pure-HTTP mode) |
|
| 411 |
+
| `agent_id` | `str` | `"sovereign_main"` | Identity for continuity (different IDs = separate state) |
|
| 412 |
+
|
| 413 |
+
---
|
| 414 |
+
|
| 415 |
+
## The Mathematics
|
| 416 |
+
|
| 417 |
+
### Jordan Algebra — SpinFactor J(n)
|
| 418 |
+
|
| 419 |
+
The routing gate uses Jordan algebra instead of softmax. This is the same algebra Pascual Jordan developed in 1933 to formalize quantum mechanics observables.
|
| 420 |
+
|
| 421 |
+
**Elements:** `(α, v)` where `α ∈ ℝ` (scalar confidence), `v ∈ ℝⁿ` (signal vector)
|
| 422 |
+
|
| 423 |
+
**Product:** `(α,v) ∘ (β,w) = (αβ + ⟨v,w⟩, αw + βv)`
|
| 424 |
+
|
| 425 |
+
**Why this matters for routing:**
|
| 426 |
+
|
| 427 |
+
1. **Non-associative composition**: `(A∘B)∘C ≠ A∘(B∘C)` — different agent grouping topologies produce mathematically different routing outcomes. We search over topologies.
|
| 428 |
+
2. **Fixed-point convergence**: Iterating `x ↦ x∘x` converges to idempotents (`e∘e = e`). These ARE the stable routing attractors.
|
| 429 |
+
3. **Spectral decomposition**: `x = λ₊c₊ + λ₋c₋` where `λ± = α ± ‖v‖`. Provably unique expert assignment.
|
| 430 |
+
4. **Spectral gap** = `2‖v‖` = separation between top experts. Bigger gap = more decisive routing.
|
| 431 |
+
|
| 432 |
+
### QRA Tensor — Quantum Routing Algebra
|
| 433 |
+
|
| 434 |
+
6×6 deterministic tensor. Shannon entropy H = 0 nats. No randomness.
|
| 435 |
+
|
| 436 |
+
| Glyph | Maps To | Signal |
|
| 437 |
+
|-------|---------|--------|
|
| 438 |
+
| Π (Pi) | Reasoning models | "explain", "why", "analyze" |
|
| 439 |
+
| Γ (Gamma) | Fast generation | "write", "create", "draft" |
|
| 440 |
+
| Δ (Delta) | Domain-specific | "sql", "medical", "legal" |
|
| 441 |
+
| Λ (Lambda) | Code models | "function", "implement", "debug" |
|
| 442 |
+
| Ω (Omega) | Orchestration | "plan", "coordinate", "multi-step" |
|
| 443 |
+
| Ψ (Psi) | Verification | "prove", "verify", "test" |
|
| 444 |
+
|
| 445 |
+
---
|
| 446 |
+
|
| 447 |
+
## Origin: The DSL
|
| 448 |
+
|
| 449 |
+
The Python engine (38,576 lines) + NASM assembly (7,019 lines) were generated from a single **HyperKittyConstraintDSL** prompt. Three agents coordinated by the DSL constraints. See [ARCHITECTURE.md](ARCHITECTURE.md) for the display template.
|
| 450 |
+
|
| 451 |
+
The C IDE was hand-built separately.
|
| 452 |
+
|
| 453 |
+
The DSL defines:
|
| 454 |
+
- Boolean kernel (all gates from NAND)
|
| 455 |
+
- Glyph type system (7 semantic types)
|
| 456 |
+
- Agent invariants (trust + entropy bounds)
|
| 457 |
+
- DAG structure (acyclic routing graph)
|
| 458 |
+
- Proof output (Blake2b hash commitment)
|
| 459 |
+
- Transformation engine (regex patterns + validation)
|
| 460 |
+
|
| 461 |
+
---
|
| 462 |
+
|
| 463 |
+
## Papers
|
| 464 |
+
|
| 465 |
+
| DOI | Title |
|
| 466 |
+
|-----|-------|
|
| 467 |
+
| [10.5281/zenodo.20678420](https://doi.org/10.5281/zenodo.20678420) | Attention Exhaustion Attacks — 0% detection rate |
|
| 468 |
+
| [10.5281/zenodo.21144425](https://doi.org/10.5281/zenodo.21144425) | Resonance Block Trust Deeds — capture spectrum |
|
| 469 |
+
| [10.5281/zenodo.21132094](https://doi.org/10.5281/zenodo.21132094) | Sovereign Compute Architecture |
|
| 470 |
+
| [10.5281/zenodo.21349277](https://doi.org/10.5281/zenodo.21349277) | Gates Normalization Constraint — simplex is structural |
|
| 471 |
+
| [10.5281/zenodo.21351461](https://doi.org/10.5281/zenodo.21351461) | NAND Decomposition — attention is NAND-complete |
|
| 472 |
+
| [10.5281/zenodo.21443609](https://doi.org/10.5281/zenodo.21443609) | Jordan Spectral Transformer — phi-weighted routing |
|
| 473 |
+
| [10.5281/zenodo.21727363](https://doi.org/10.5281/zenodo.21727363) | PAR-011 Jacobian via Jordan Algebras |
|
| 474 |
+
| [10.5281/zenodo.21268911](https://doi.org/10.5281/zenodo.21268911) | GKN I4 Quartic Invariant and E7 Symmetry |
|
| 475 |
+
|
| 476 |
+
Unified: [The Sovereign Stack](https://snapkittywest.github.io/hyperkitty/papers/sovereign-stack-unified.pdf) — 26 pages, Lean 4.
|
| 477 |
+
|
| 478 |
+
---
|
| 479 |
+
|
| 480 |
+
## Line Count
|
| 481 |
+
|
| 482 |
+
| Component | Language | Files | Lines |
|
| 483 |
+
|-----------|----------|-------|-------|
|
| 484 |
+
| Engine core | Python 3.11 | 87 | 38,576 |
|
| 485 |
+
| NASM runtime | x86-64 Assembly | 7 | 7,019 |
|
| 486 |
+
| C Win32 IDE | C (Win32 API) | 47 | 4,459 |
|
| 487 |
+
| C IPC dispatcher | C | 1 | 390 |
|
| 488 |
+
| **Total** | **3 languages** | **142** | **50,444** |
|
| 489 |
+
|
| 490 |
+
---
|
| 491 |
+
|
| 492 |
+
## Documentation
|
| 493 |
+
|
| 494 |
+
| Guide | What You Learn |
|
| 495 |
+
|-------|---------------|
|
| 496 |
+
| [Architecture](ARCHITECTURE.md) | DSL proof-of-concept, how the engine was generated |
|
| 497 |
+
| [Getting Started](docs/GETTING_STARTED.md) | Install, configure, first task |
|
| 498 |
+
| [Configuration](docs/CONFIGURATION.md) | Every option explained with examples |
|
| 499 |
+
| [Routing](docs/ROUTING.md) | 11-stage pipeline deep dive, tuning, custom experts |
|
| 500 |
+
| [Tools](docs/TOOLS.md) | All 34 tools, custom registration, IPC opcodes |
|
| 501 |
+
| [Continuity](docs/CONTINUITY.md) | 4 paradigms, crash recovery, replay |
|
| 502 |
+
| [Security](docs/SECURITY.md) | PathJail, SSRF, WORM, ERE gates |
|
| 503 |
+
| [Production Hardening](PRODUCTION_HARDENING.md) | Terminal IDE, accessibility, edge cases, testing, CI |
|
| 504 |
+
|
| 505 |
+
---
|
| 506 |
+
|
| 507 |
+
## License
|
| 508 |
+
|
| 509 |
+
BSL 1.1 → MIT 2029-01-01
|
| 510 |
+
|
| 511 |
+
SnapKitty / SNAPKITTYWEST / 2026
|
SPRINT_PLAN.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 3-Phase Sprint Plan — Sovereign Engine v2
|
| 2 |
+
|
| 3 |
+
## Current State (Audit)
|
| 4 |
+
|
| 5 |
+
**Tracked:** 137 files on remote
|
| 6 |
+
**Untracked (needs commit):** ide/, docs/, routing_trace.py, qra_router.py, tests/
|
| 7 |
+
**Broken:** ere.py had syntax error (FIXED locally, not pushed)
|
| 8 |
+
**Missing docs:** ROUTING.md, TOOLS.md, CONTINUITY.md, SECURITY.md
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## PHASE 1: Structural Integrity (This Sprint)
|
| 13 |
+
|
| 14 |
+
Goal: Every file compiles, imports resolve, repo structure is clean.
|
| 15 |
+
|
| 16 |
+
- [x] Fix ere.py syntax error (smart quotes → ASCII)
|
| 17 |
+
- [ ] Full syntax check — 0 errors across all .py files
|
| 18 |
+
- [ ] Commit IDE (ide/native/ — 47 C files, 4,459 lines)
|
| 19 |
+
- [ ] Commit Gap 2 (qra_router.py — QRA 6-glyph routing)
|
| 20 |
+
- [ ] Commit Gap 5 (routing_trace.py — trace collector for IDE bridge)
|
| 21 |
+
- [ ] Commit docs/ (GETTING_STARTED.md, CONFIGURATION.md)
|
| 22 |
+
- [ ] Remove Codex planning artifacts (AGENT_BRIEF, gaps/, architecture/)
|
| 23 |
+
- [ ] Verify all __init__.py exports resolve
|
| 24 |
+
- [ ] Push to remote — single clean commit
|
| 25 |
+
|
| 26 |
+
**Acceptance:** `python -c "from src import SovereignEngine"` works. Zero syntax errors. No stale files.
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## PHASE 2: Documentation + User Guides (Next Sprint)
|
| 31 |
+
|
| 32 |
+
Goal: Someone can clone this repo and understand + use it without asking.
|
| 33 |
+
|
| 34 |
+
- [ ] docs/ROUTING.md — 11-stage pipeline explained, tuning, custom experts
|
| 35 |
+
- [ ] docs/TOOLS.md — All 34 tools, registration, IPC, supervisor
|
| 36 |
+
- [ ] docs/CONTINUITY.md — 4 paradigms, crash recovery, replay
|
| 37 |
+
- [ ] docs/SECURITY.md — PathJail, SSRF, WORM, ERE, inverted AST
|
| 38 |
+
- [ ] docs/IDE.md — How to build the C IDE, connect to engine, use ConPTY
|
| 39 |
+
- [ ] docs/MACHINE_CODE.md — What the bytecode assembler / VM / x86 gen actually do
|
| 40 |
+
- [ ] README cross-references all docs (already done in new README)
|
| 41 |
+
- [ ] Each doc has runnable code examples
|
| 42 |
+
|
| 43 |
+
**Acceptance:** A developer can read docs/ top-to-bottom and set up the full system.
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## PHASE 3: Version Control + Release + Packaging (Final Sprint)
|
| 48 |
+
|
| 49 |
+
Goal: Proper tagged release with installable package.
|
| 50 |
+
|
| 51 |
+
- [ ] pyproject.toml verified (already exists)
|
| 52 |
+
- [ ] `python -m build` produces .whl
|
| 53 |
+
- [ ] GitHub Release v2.0.0 with changelog
|
| 54 |
+
- [ ] Git tag v2.0.0
|
| 55 |
+
- [ ] LICENSE file (BSL 1.1)
|
| 56 |
+
- [ ] CHANGELOG.md
|
| 57 |
+
- [ ] .github/workflows/ci.yml (Python 3.11-3.13 matrix, pytest, pyright)
|
| 58 |
+
- [ ] requirements.txt is empty (stdlib only — but document optional deps)
|
| 59 |
+
- [ ] Verify `pip install .` works from clean clone
|
| 60 |
+
|
| 61 |
+
**Acceptance:** `pip install .` → `sovereign --help` works. Tagged release on GitHub.
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## Rules
|
| 66 |
+
|
| 67 |
+
1. No rushing. Every file gets syntax-checked before commit.
|
| 68 |
+
2. No planning docs in the repo. Only code, docs, and config.
|
| 69 |
+
3. Line counts in README must be verified (`wc -l`) not guessed.
|
| 70 |
+
4. If an agent writes something broken, I fix it or rewrite it. No shipping broken code.
|
| 71 |
+
5. One clean commit per phase. Not 50 micro-commits.
|
docs/CONFIGURATION.md
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Configuration Reference
|
| 2 |
+
|
| 3 |
+
Complete guide to all Sovereign Engine configuration options.
|
| 4 |
+
|
| 5 |
+
## EngineConfig Dataclass
|
| 6 |
+
|
| 7 |
+
The `EngineConfig` controls all engine behavior. Pass it to `SovereignEngine()`:
|
| 8 |
+
|
| 9 |
+
```python
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from src.sovereign import EngineConfig, SovereignEngine
|
| 12 |
+
|
| 13 |
+
config = EngineConfig(
|
| 14 |
+
allowed_roots=[Path("/safe/paths")],
|
| 15 |
+
ledger_path=Path("./sovereign.worm"),
|
| 16 |
+
continuity_dir=Path.home() / ".sovereign" / "continuity",
|
| 17 |
+
max_steps=15,
|
| 18 |
+
enable_shadow=True,
|
| 19 |
+
enable_ipc=True,
|
| 20 |
+
agent_id="sovereign_main"
|
| 21 |
+
)
|
| 22 |
+
engine = SovereignEngine(config)
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
### Configuration Options
|
| 26 |
+
|
| 27 |
+
| Option | Type | Default | Description |
|
| 28 |
+
|--------|------|---------|-------------|
|
| 29 |
+
| `allowed_roots` | `list[Path]` | `[Path.cwd()]` | Filesystem roots the engine can access. PathJail blocks all others. |
|
| 30 |
+
| `ledger_path` | `Path` | `./sovereign.worm` | Binary WORM ledger file for cryptographic audit trail. |
|
| 31 |
+
| `continuity_dir` | `Path` | `~/.sovereign/continuity` | Directory for all four continuity backends. Auto-created. |
|
| 32 |
+
| `max_steps` | `int` | `15` | Max reasoning steps in ReActAgent loop before timeout. |
|
| 33 |
+
| `enable_shadow` | `bool` | `True` | Enable ShadowAgent for non-blocking observation. |
|
| 34 |
+
| `enable_ipc` | `bool` | `True` | Enable native IPC multiplexer (O(1) tool dispatch). |
|
| 35 |
+
| `agent_id` | `str` | `"sovereign_main"` | Unique agent identifier (for multi-agent coordination). |
|
| 36 |
+
|
| 37 |
+
## Environment Variables
|
| 38 |
+
|
| 39 |
+
Override config via environment variables (prefixed with `SOVEREIGN_`):
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
export SOVEREIGN_ALLOWED_ROOTS="/home/user/projects:/tmp"
|
| 43 |
+
export SOVEREIGN_MAX_STEPS=25
|
| 44 |
+
export SOVEREIGN_ENABLE_SHADOW=false
|
| 45 |
+
export SOVEREIGN_AGENT_ID="batch_processor_1"
|
| 46 |
+
|
| 47 |
+
python your_script.py
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
Supported environment variables:
|
| 51 |
+
|
| 52 |
+
| Variable | Type | Maps To |
|
| 53 |
+
|----------|------|---------|
|
| 54 |
+
| `SOVEREIGN_ALLOWED_ROOTS` | CSV paths | `allowed_roots` (split on `:` or `;`) |
|
| 55 |
+
| `SOVEREIGN_LEDGER_PATH` | path | `ledger_path` |
|
| 56 |
+
| `SOVEREIGN_CONTINUITY_DIR` | path | `continuity_dir` |
|
| 57 |
+
| `SOVEREIGN_MAX_STEPS` | int | `max_steps` |
|
| 58 |
+
| `SOVEREIGN_ENABLE_SHADOW` | bool | `enable_shadow` (true/false/1/0) |
|
| 59 |
+
| `SOVEREIGN_ENABLE_IPC` | bool | `enable_ipc` |
|
| 60 |
+
| `SOVEREIGN_AGENT_ID` | str | `agent_id` |
|
| 61 |
+
|
| 62 |
+
Example:
|
| 63 |
+
|
| 64 |
+
```python
|
| 65 |
+
import os
|
| 66 |
+
from src.sovereign import SovereignEngine
|
| 67 |
+
|
| 68 |
+
# Environment variables take precedence
|
| 69 |
+
if "SOVEREIGN_MAX_STEPS" in os.environ:
|
| 70 |
+
max_steps = int(os.environ["SOVEREIGN_MAX_STEPS"])
|
| 71 |
+
else:
|
| 72 |
+
max_steps = 15
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
## ReActAgent Config
|
| 76 |
+
|
| 77 |
+
The ReAct reasoning agent has its own config:
|
| 78 |
+
|
| 79 |
+
```python
|
| 80 |
+
from src.agents.react import ReActConfig, ReActAgent
|
| 81 |
+
|
| 82 |
+
react_config = ReActConfig(
|
| 83 |
+
max_steps=20,
|
| 84 |
+
timeout_ms=60000,
|
| 85 |
+
model_provider="bedrock", # bedrock, openrouter, ollama, anthropic
|
| 86 |
+
temperature=0.7,
|
| 87 |
+
top_p=0.9,
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
agent = ReActAgent(
|
| 91 |
+
agent_id="code_agent",
|
| 92 |
+
config=react_config,
|
| 93 |
+
registry=tool_registry,
|
| 94 |
+
ledger=worm_ledger,
|
| 95 |
+
)
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
| Option | Type | Default | Description |
|
| 99 |
+
|--------|------|---------|-------------|
|
| 100 |
+
| `max_steps` | `int` | `15` | Max thinking/action loops. |
|
| 101 |
+
| `timeout_ms` | `int` | `30000` | Total execution timeout. |
|
| 102 |
+
| `model_provider` | `str` | `"bedrock"` | LLM backend: bedrock, openrouter, ollama, anthropic. |
|
| 103 |
+
| `temperature` | `float` | `0.7` | Sampling temperature (0.0-1.0). |
|
| 104 |
+
| `top_p` | `float` | `0.9` | Nucleus sampling parameter. |
|
| 105 |
+
|
| 106 |
+
## Continuity Directory Structure
|
| 107 |
+
|
| 108 |
+
The engine uses 4 paradigms for state recovery. Structure:
|
| 109 |
+
|
| 110 |
+
```
|
| 111 |
+
~/.sovereign/continuity/
|
| 112 |
+
├── env_state.bin # Paradigm 1: Env bitmask (for hot-restart)
|
| 113 |
+
├── seed.bin # Paradigm 2: Seed for deterministic replay
|
| 114 |
+
├── agent_id_seed.bin # Paradigm 2 per-agent seed
|
| 115 |
+
├── inode/
|
| 116 |
+
│ ├── flags.db # Paradigm 3: File handles mapped to boolean gates
|
| 117 |
+
│ ├── agent_id.lock
|
| 118 |
+
│ └── agent_id.state
|
| 119 |
+
├── shm/
|
| 120 |
+
│ └── block_0.bin # Paradigm 4: Shared memory block (4KB)
|
| 121 |
+
└── checkpoints/
|
| 122 |
+
├── checkpoint_001.ckpt # Full state snapshot with signature
|
| 123 |
+
├── checkpoint_002.ckpt
|
| 124 |
+
└── manifest.json
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
**Paradigm 1: Env State** — 64-bit bitmask in `os.environ["SOVEREIGN_STATE"]`:
|
| 128 |
+
- Fastest (RAM)
|
| 129 |
+
- Lost on process exit
|
| 130 |
+
- Use for: hot-restart via `os.execv()`
|
| 131 |
+
|
| 132 |
+
**Paradigm 2: Seed Chain** — Blake2b seed derivation:
|
| 133 |
+
- Deterministic replay
|
| 134 |
+
- Store operation log in binary
|
| 135 |
+
- Use for: replay testing, timeline forking
|
| 136 |
+
|
| 137 |
+
**Paradigm 3: Inode State** — Zero-byte files as gates:
|
| 138 |
+
- Kernel-durable
|
| 139 |
+
- Fast stat() reads
|
| 140 |
+
- Use for: boolean flags, pause/resume
|
| 141 |
+
|
| 142 |
+
**Paradigm 4: Shared Memory** — ctypes mmap block:
|
| 143 |
+
- Cross-process coordination
|
| 144 |
+
- 4KB block
|
| 145 |
+
- Use for: multi-agent sync, realtime state
|
| 146 |
+
|
| 147 |
+
## PathJail Configuration
|
| 148 |
+
|
| 149 |
+
Restrict filesystem access to approved roots:
|
| 150 |
+
|
| 151 |
+
```python
|
| 152 |
+
from src.core.path_jail import PathJail
|
| 153 |
+
from pathlib import Path
|
| 154 |
+
|
| 155 |
+
jail = PathJail(roots=[
|
| 156 |
+
Path("/home/user/projects"),
|
| 157 |
+
Path("/tmp/scratch"),
|
| 158 |
+
Path.home() / "Downloads"
|
| 159 |
+
])
|
| 160 |
+
|
| 161 |
+
# This is OK
|
| 162 |
+
jail.check("/home/user/projects/code.py") # ✓
|
| 163 |
+
|
| 164 |
+
# This is BLOCKED
|
| 165 |
+
jail.check("/etc/passwd") # ✗ PathJailError
|
| 166 |
+
jail.check("/root/.ssh/id_rsa") # ✗ PathJailError
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
Pass to engine:
|
| 170 |
+
|
| 171 |
+
```python
|
| 172 |
+
config = EngineConfig(
|
| 173 |
+
allowed_roots=[
|
| 174 |
+
Path("/home/user/projects"),
|
| 175 |
+
Path("/tmp/scratch"),
|
| 176 |
+
]
|
| 177 |
+
)
|
| 178 |
+
engine = SovereignEngine(config)
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
## Model Provider Configuration
|
| 182 |
+
|
| 183 |
+
Choose which LLM backend to use. Defaults to **AWS Bedrock** (all models via unified API).
|
| 184 |
+
|
| 185 |
+
### Bedrock (Recommended)
|
| 186 |
+
|
| 187 |
+
```python
|
| 188 |
+
from src.sovereign import EngineConfig
|
| 189 |
+
from src.runtime.providers.bedrock import BedrockProvider
|
| 190 |
+
|
| 191 |
+
config = EngineConfig()
|
| 192 |
+
provider = BedrockProvider(
|
| 193 |
+
region="us-west-2",
|
| 194 |
+
model="anthropic.claude-3-opus-20240229-v1:0",
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
# Engine automatically uses it
|
| 198 |
+
engine = SovereignEngine(config)
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
Requires `~/.aws/credentials`:
|
| 202 |
+
```
|
| 203 |
+
[default]
|
| 204 |
+
aws_access_key_id = YOUR_KEY
|
| 205 |
+
aws_secret_access_key = YOUR_SECRET
|
| 206 |
+
region = us-west-2
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
### OpenRouter (Open-source + proprietary models)
|
| 210 |
+
|
| 211 |
+
```python
|
| 212 |
+
from src.runtime.providers.openrouter import OpenRouterProvider
|
| 213 |
+
|
| 214 |
+
provider = OpenRouterProvider(api_key="sk-or-...")
|
| 215 |
+
# Uses OpenRouter API for Mistral, Llama, Qwen, etc.
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
Requires `OPENROUTER_API_KEY` environment variable.
|
| 219 |
+
|
| 220 |
+
### Ollama (Local models)
|
| 221 |
+
|
| 222 |
+
```python
|
| 223 |
+
from src.runtime.providers.ollama import OllamaProvider
|
| 224 |
+
|
| 225 |
+
provider = OllamaProvider(base_url="http://localhost:11434")
|
| 226 |
+
# Connect to local Ollama instance (ollama serve)
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
Run locally first:
|
| 230 |
+
```bash
|
| 231 |
+
ollama run mistral
|
| 232 |
+
# or
|
| 233 |
+
ollama run llama2
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
### Anthropic API (Direct)
|
| 237 |
+
|
| 238 |
+
```python
|
| 239 |
+
from src.runtime.providers.anthropic import AnthropicProvider
|
| 240 |
+
|
| 241 |
+
provider = AnthropicProvider(api_key="sk-ant-...")
|
| 242 |
+
```
|
| 243 |
+
|
| 244 |
+
## Example Configurations
|
| 245 |
+
|
| 246 |
+
### Development Mode
|
| 247 |
+
|
| 248 |
+
Minimal logging, fast iteration:
|
| 249 |
+
|
| 250 |
+
```python
|
| 251 |
+
from src.sovereign import EngineConfig
|
| 252 |
+
from pathlib import Path
|
| 253 |
+
import logging
|
| 254 |
+
|
| 255 |
+
logging.basicConfig(level=logging.DEBUG)
|
| 256 |
+
|
| 257 |
+
config = EngineConfig(
|
| 258 |
+
allowed_roots=[Path.cwd()],
|
| 259 |
+
ledger_path=Path("/tmp/dev.worm"),
|
| 260 |
+
continuity_dir=Path("/tmp/continuity"),
|
| 261 |
+
max_steps=10, # Fast iteration
|
| 262 |
+
enable_shadow=False,
|
| 263 |
+
)
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
### Production Mode
|
| 267 |
+
|
| 268 |
+
Full logging, durability, multi-agent:
|
| 269 |
+
|
| 270 |
+
```python
|
| 271 |
+
config = EngineConfig(
|
| 272 |
+
allowed_roots=[
|
| 273 |
+
Path("/var/app/data"),
|
| 274 |
+
Path("/var/app/cache"),
|
| 275 |
+
],
|
| 276 |
+
ledger_path=Path("/var/lib/sovereign/ledger.worm"),
|
| 277 |
+
continuity_dir=Path("/var/lib/sovereign/continuity"),
|
| 278 |
+
max_steps=25, # More thinking
|
| 279 |
+
enable_shadow=True, # Async observation
|
| 280 |
+
enable_ipc=True, # Native IPC
|
| 281 |
+
agent_id="prod_reactor_1",
|
| 282 |
+
)
|
| 283 |
+
```
|
| 284 |
+
|
| 285 |
+
With Bedrock:
|
| 286 |
+
|
| 287 |
+
```python
|
| 288 |
+
from src.runtime.providers.bedrock import BedrockProvider
|
| 289 |
+
|
| 290 |
+
provider = BedrockProvider(
|
| 291 |
+
region="us-east-1",
|
| 292 |
+
model="anthropic.claude-3-sonnet-20240229-v1:0",
|
| 293 |
+
timeout_ms=120000,
|
| 294 |
+
)
|
| 295 |
+
```
|
| 296 |
+
|
| 297 |
+
### CI Testing Mode
|
| 298 |
+
|
| 299 |
+
Fast, deterministic, no external calls:
|
| 300 |
+
|
| 301 |
+
```python
|
| 302 |
+
config = EngineConfig(
|
| 303 |
+
allowed_roots=[Path("/tmp/ci_test")],
|
| 304 |
+
ledger_path=Path("/tmp/ci.worm"),
|
| 305 |
+
continuity_dir=Path("/tmp/ci_continuity"),
|
| 306 |
+
max_steps=5,
|
| 307 |
+
enable_shadow=False,
|
| 308 |
+
enable_ipc=False, # Use pure Python
|
| 309 |
+
agent_id=f"ci_test_{os.getenv('CI_BUILD_ID')}",
|
| 310 |
+
)
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
Use mock provider for testing:
|
| 314 |
+
|
| 315 |
+
```python
|
| 316 |
+
from src.runtime.providers.multi import MultiProvider
|
| 317 |
+
|
| 318 |
+
# Fallback chain: try each in order
|
| 319 |
+
provider = MultiProvider([
|
| 320 |
+
MockProvider(), # Return fixed responses
|
| 321 |
+
OllamaProvider(), # Fall back to local if available
|
| 322 |
+
])
|
| 323 |
+
```
|
| 324 |
+
|
| 325 |
+
## Logging Configuration
|
| 326 |
+
|
| 327 |
+
Control verbosity via Python logging:
|
| 328 |
+
|
| 329 |
+
```python
|
| 330 |
+
import logging
|
| 331 |
+
|
| 332 |
+
# Engine logs
|
| 333 |
+
logging.getLogger("sovereign.engine").setLevel(logging.INFO)
|
| 334 |
+
|
| 335 |
+
# Routing pipeline
|
| 336 |
+
logging.getLogger("sovereign.routing").setLevel(logging.DEBUG)
|
| 337 |
+
|
| 338 |
+
# Tool registry
|
| 339 |
+
logging.getLogger("sovereign.tools").setLevel(logging.WARNING)
|
| 340 |
+
|
| 341 |
+
# Everything
|
| 342 |
+
logging.basicConfig(
|
| 343 |
+
level=logging.DEBUG,
|
| 344 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 345 |
+
)
|
| 346 |
+
```
|
| 347 |
+
|
| 348 |
+
## Dynamic Configuration Updates
|
| 349 |
+
|
| 350 |
+
Update config without restarting:
|
| 351 |
+
|
| 352 |
+
```python
|
| 353 |
+
engine = SovereignEngine(config)
|
| 354 |
+
|
| 355 |
+
# Update max_steps
|
| 356 |
+
engine.config.max_steps = 25
|
| 357 |
+
|
| 358 |
+
# Restart agent with new config
|
| 359 |
+
from src.agents.react import ReActConfig
|
| 360 |
+
engine.agent.config.max_steps = 25
|
| 361 |
+
|
| 362 |
+
# Add a new allowed root
|
| 363 |
+
engine.config.allowed_roots.append(Path("/new/path"))
|
| 364 |
+
engine.path_jail = PathJail(roots=engine.config.allowed_roots)
|
| 365 |
+
```
|
| 366 |
+
|
| 367 |
+
## Configuration Validation
|
| 368 |
+
|
| 369 |
+
Validate config before starting engine:
|
| 370 |
+
|
| 371 |
+
```python
|
| 372 |
+
from src.sovereign import EngineConfig
|
| 373 |
+
from pathlib import Path
|
| 374 |
+
|
| 375 |
+
config = EngineConfig(
|
| 376 |
+
allowed_roots=[Path("/tmp/test")],
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
# Check roots exist
|
| 380 |
+
for root in config.allowed_roots:
|
| 381 |
+
if not root.exists():
|
| 382 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 383 |
+
|
| 384 |
+
# Check continuity dir writable
|
| 385 |
+
try:
|
| 386 |
+
config.continuity_dir.mkdir(parents=True, exist_ok=True)
|
| 387 |
+
(config.continuity_dir / ".test").touch()
|
| 388 |
+
(config.continuity_dir / ".test").unlink()
|
| 389 |
+
except PermissionError:
|
| 390 |
+
raise RuntimeError(f"Cannot write to {config.continuity_dir}")
|
| 391 |
+
```
|
docs/GETTING_STARTED.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Getting Started with Sovereign Engine
|
| 2 |
+
|
| 3 |
+
Welcome! This guide will get you running your first agent task in under 10 minutes.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
- **Python 3.11 or higher** — Check with: `python --version`
|
| 8 |
+
- **No external dependencies** — Everything uses Python stdlib (struct, hashlib, asyncio, etc.)
|
| 9 |
+
- Optional: NASM (for native assembly compilation)
|
| 10 |
+
- Optional: C compiler (for IPC core dispatcher)
|
| 11 |
+
|
| 12 |
+
## Installation
|
| 13 |
+
|
| 14 |
+
### Option 1: From Source
|
| 15 |
+
|
| 16 |
+
```bash
|
| 17 |
+
git clone https://github.com/SNAPKITTYWEST/sovereign-reverse.git
|
| 18 |
+
cd sovereign-reverse/engine
|
| 19 |
+
python -m pip install -e .
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
### Option 2: Install Locally
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
cd /path/to/sovereign-reverse/engine
|
| 26 |
+
python -m pip install -e .
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
### Verify Installation
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
python -c "from src.sovereign import SovereignEngine; print('OK')"
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
## First Run: Hello World
|
| 36 |
+
|
| 37 |
+
Create a file `hello.py`:
|
| 38 |
+
|
| 39 |
+
```python
|
| 40 |
+
import asyncio
|
| 41 |
+
from pathlib import Path
|
| 42 |
+
from src.sovereign import SovereignEngine, EngineConfig
|
| 43 |
+
|
| 44 |
+
async def main():
|
| 45 |
+
# Create engine with default config
|
| 46 |
+
config = EngineConfig(
|
| 47 |
+
allowed_roots=[Path.cwd()],
|
| 48 |
+
continuity_dir=Path.home() / ".sovereign" / "continuity"
|
| 49 |
+
)
|
| 50 |
+
engine = SovereignEngine(config)
|
| 51 |
+
|
| 52 |
+
# Run a simple task
|
| 53 |
+
result = await engine.run("write a Python function to calculate fibonacci(5)")
|
| 54 |
+
print("\n" + "="*60)
|
| 55 |
+
print("RESULT:")
|
| 56 |
+
print("="*60)
|
| 57 |
+
print(result)
|
| 58 |
+
|
| 59 |
+
# Clean up
|
| 60 |
+
engine.shutdown()
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
asyncio.run(main())
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
Run it:
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
python hello.py
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
You should see output like:
|
| 73 |
+
|
| 74 |
+
```
|
| 75 |
+
2024-08-06 10:15:23,456 - sovereign.engine - INFO - SovereignEngine initialized
|
| 76 |
+
2024-08-06 10:15:23,478 - sovereign.engine - INFO - Starting task: write a Python function to calculate fibonacci(5)
|
| 77 |
+
...
|
| 78 |
+
============================================================
|
| 79 |
+
RESULT:
|
| 80 |
+
============================================================
|
| 81 |
+
def fibonacci(n):
|
| 82 |
+
if n <= 1:
|
| 83 |
+
return n
|
| 84 |
+
return fibonacci(n-1) + fibonacci(n-2)
|
| 85 |
+
|
| 86 |
+
print(fibonacci(5)) # Output: 5
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## Understanding the Output
|
| 90 |
+
|
| 91 |
+
### WORM Seal
|
| 92 |
+
|
| 93 |
+
Every task execution is sealed in a **Write-Once Read-Many (WORM)** cryptographic ledger:
|
| 94 |
+
|
| 95 |
+
```
|
| 96 |
+
sovereign.worm # Binary append-only file (no text, no injection surface)
|
| 97 |
+
└─ 152-byte header # Magic + version + timestamps + Blake2b hash + Ed25519 sig
|
| 98 |
+
└─ payload # Your task result (binary encoded)
|
| 99 |
+
└─ metadata # Execution context (binary encoded)
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
Read the ledger:
|
| 103 |
+
|
| 104 |
+
```python
|
| 105 |
+
from src.core.storage import WORMFile
|
| 106 |
+
from pathlib import Path
|
| 107 |
+
|
| 108 |
+
wf = WORMFile(Path("sovereign.worm"))
|
| 109 |
+
for record in wf.scan():
|
| 110 |
+
print(f"Event: {record.event_type}")
|
| 111 |
+
print(f"Timestamp: {record.timestamp_s}")
|
| 112 |
+
print(f"Hash: {record.content_hash.hex()}")
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
### Routing Weights
|
| 116 |
+
|
| 117 |
+
The engine routes tasks to multiple **experts** (specialized agents). Output shows which experts fired and their contribution:
|
| 118 |
+
|
| 119 |
+
```
|
| 120 |
+
Routing Decision:
|
| 121 |
+
code_generation: 0.65 # 65% contribution
|
| 122 |
+
error_checking: 0.28 # 28% contribution
|
| 123 |
+
documentation: 0.07 # 7% contribution
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
This is driven by **Jordan algebra**, not softmax:
|
| 127 |
+
- Non-associative grouping — expert topology matters
|
| 128 |
+
- Fixed-point convergence — stable routing attractors
|
| 129 |
+
- Spectral decomposition — provably unique expert selection
|
| 130 |
+
|
| 131 |
+
### Continuity Directory
|
| 132 |
+
|
| 133 |
+
Agent state survives restarts across 4 paradigms:
|
| 134 |
+
|
| 135 |
+
```
|
| 136 |
+
~/.sovereign/continuity/
|
| 137 |
+
└─ env_state.bin # Paradigm 1: 64-bit env bitmask (hot-restart)
|
| 138 |
+
└─ seed.bin # Paradigm 2: Blake2b seed for deterministic replay
|
| 139 |
+
└─ inode/
|
| 140 |
+
│ └─ agent_id.state # Paradigm 3: Zero-byte files as boolean gates
|
| 141 |
+
│ └─ agent_id.lock
|
| 142 |
+
└─ shm_block.bin # Paradigm 4: Shared memory (cross-process realtime)
|
| 143 |
+
└─ checkpoints/
|
| 144 |
+
└─ checkpoint_1.ckpt # Full binary snapshot with signature
|
| 145 |
+
└─ checkpoint_2.ckpt
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
## Common First-Time Issues
|
| 149 |
+
|
| 150 |
+
### Issue: "ModuleNotFoundError: No module named 'src.sovereign'"
|
| 151 |
+
|
| 152 |
+
**Fix:** Make sure you're in the engine directory and have run `pip install -e .`:
|
| 153 |
+
|
| 154 |
+
```bash
|
| 155 |
+
cd /path/to/sovereign-reverse/engine
|
| 156 |
+
python -m pip install -e .
|
| 157 |
+
python hello.py
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
### Issue: "PermissionError: Cannot create continuity directory"
|
| 161 |
+
|
| 162 |
+
**Fix:** The engine needs to write to `~/.sovereign/continuity`. Make sure it exists and is writable:
|
| 163 |
+
|
| 164 |
+
```bash
|
| 165 |
+
mkdir -p ~/.sovereign/continuity
|
| 166 |
+
chmod 700 ~/.sovereign/continuity
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
Or use a custom path:
|
| 170 |
+
|
| 171 |
+
```python
|
| 172 |
+
config = EngineConfig(
|
| 173 |
+
continuity_dir=Path("/tmp/my_continuity")
|
| 174 |
+
)
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
### Issue: "ValueError: no signature in WORM record"
|
| 178 |
+
|
| 179 |
+
**Fix:** This is expected if no signing key is configured. The engine defaults to zero-signature (all bytes 0). To use real Ed25519 signing:
|
| 180 |
+
|
| 181 |
+
```python
|
| 182 |
+
from src.core.crypto import generate_signing_key
|
| 183 |
+
|
| 184 |
+
key = generate_signing_key()
|
| 185 |
+
engine = SovereignEngine(config)
|
| 186 |
+
# engine will use key internally
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
### Issue: "Timeout: task exceeded 30 seconds"
|
| 190 |
+
|
| 191 |
+
**Fix:** Increase the max step count or timeout in config:
|
| 192 |
+
|
| 193 |
+
```python
|
| 194 |
+
from src.agents.react import ReActConfig
|
| 195 |
+
|
| 196 |
+
config = EngineConfig(
|
| 197 |
+
max_steps=25 # More thinking steps
|
| 198 |
+
)
|
| 199 |
+
engine = SovereignEngine(config)
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
Or in individual agent config:
|
| 203 |
+
|
| 204 |
+
```python
|
| 205 |
+
from src.agents.react import ReActAgent, ReActConfig
|
| 206 |
+
|
| 207 |
+
react_config = ReActConfig(max_steps=25, timeout_ms=60000)
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
### Issue: "PathJail: denied /path/to/file"
|
| 211 |
+
|
| 212 |
+
**Fix:** The engine restricts filesystem access to `allowed_roots`. Add your path:
|
| 213 |
+
|
| 214 |
+
```python
|
| 215 |
+
config = EngineConfig(
|
| 216 |
+
allowed_roots=[
|
| 217 |
+
Path("/home/user/projects"),
|
| 218 |
+
Path("/tmp/scratch")
|
| 219 |
+
]
|
| 220 |
+
)
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
## Next Steps
|
| 224 |
+
|
| 225 |
+
- **[CONFIGURATION.md](CONFIGURATION.md)** — All EngineConfig options with defaults
|
| 226 |
+
- **[ROUTING.md](ROUTING.md)** — How the 11-stage pipeline works
|
| 227 |
+
- **[TOOLS.md](TOOLS.md)** — Complete tool inventory (34 tools)
|
| 228 |
+
- **[CONTINUITY.md](CONTINUITY.md)** — State recovery and replay
|
| 229 |
+
- **[SECURITY.md](SECURITY.md)** — PathJail, SSRFGuard, WORM verification
|
| 230 |
+
|
| 231 |
+
## Production Checklist
|
| 232 |
+
|
| 233 |
+
Before shipping to production:
|
| 234 |
+
|
| 235 |
+
- [ ] Set up Ed25519 signing key (see [SECURITY.md](SECURITY.md))
|
| 236 |
+
- [ ] Configure `allowed_roots` to restrict filesystem access
|
| 237 |
+
- [ ] Enable WORM ledger integrity checks (script in [SECURITY.md](SECURITY.md))
|
| 238 |
+
- [ ] Set up continuity directory with proper permissions
|
| 239 |
+
- [ ] Test task recovery (kill agent mid-task, restart)
|
| 240 |
+
- [ ] Configure rate limits on destructive tools (see [TOOLS.md](TOOLS.md))
|
| 241 |
+
- [ ] Enable shadow agent for async observation: `enable_shadow=True`
|
| 242 |
+
- [ ] Set appropriate model provider in config (Bedrock/OpenRouter/Ollama)
|
ide/CMakeLists.txt
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cmake_minimum_required(VERSION 3.24)
|
| 2 |
+
project(sovereign-ide C CXX)
|
| 3 |
+
|
| 4 |
+
set(CMAKE_C_STANDARD 17)
|
| 5 |
+
set(CMAKE_C_STANDARD_REQUIRED ON)
|
| 6 |
+
set(CMAKE_CXX_STANDARD 17)
|
| 7 |
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
| 8 |
+
|
| 9 |
+
if(NOT WIN32)
|
| 10 |
+
message(FATAL_ERROR "Sovereign IDE targets Windows only (Win32 + Direct2D)")
|
| 11 |
+
endif()
|
| 12 |
+
|
| 13 |
+
add_compile_definitions(_WIN32_WINNT=0x0A00 NTDDI_VERSION=0x0A000000)
|
| 14 |
+
|
| 15 |
+
if(MSVC)
|
| 16 |
+
add_compile_options(/W4 /WX /utf-8)
|
| 17 |
+
else()
|
| 18 |
+
add_compile_options(-Wall -Wextra -Werror -Wno-missing-field-initializers)
|
| 19 |
+
endif()
|
| 20 |
+
|
| 21 |
+
# --- Core library (pure C, no GUI deps) ---
|
| 22 |
+
add_library(sov_core STATIC
|
| 23 |
+
native/core/arena.c
|
| 24 |
+
native/core/strings.c
|
| 25 |
+
native/core/events.c
|
| 26 |
+
native/editor/buffer.c
|
| 27 |
+
native/editor/document.c
|
| 28 |
+
native/chat/protocol.c
|
| 29 |
+
native/fcl/evaluator.c
|
| 30 |
+
native/git/repository.c
|
| 31 |
+
native/build/cmake_runner.c
|
| 32 |
+
native/lsp/client.c
|
| 33 |
+
)
|
| 34 |
+
target_include_directories(sov_core PUBLIC native)
|
| 35 |
+
|
| 36 |
+
# --- Editor graphics (C++, Direct2D + DirectWrite) ---
|
| 37 |
+
add_library(sov_editor STATIC
|
| 38 |
+
native/graphics/d2d_renderer.cpp
|
| 39 |
+
native/editor/editor_view.cpp
|
| 40 |
+
)
|
| 41 |
+
target_include_directories(sov_editor PUBLIC native)
|
| 42 |
+
target_link_libraries(sov_editor PUBLIC sov_core d2d1 dwrite dxgi ole32)
|
| 43 |
+
|
| 44 |
+
# --- UI widgets (C++, Direct2D + DirectWrite) ---
|
| 45 |
+
add_library(sov_ui STATIC
|
| 46 |
+
native/ui/layout.c
|
| 47 |
+
native/ui/project_tree.cpp
|
| 48 |
+
native/ui/status_bar.cpp
|
| 49 |
+
native/ui/output_panel.cpp
|
| 50 |
+
native/terminal/conpty.c
|
| 51 |
+
native/terminal/terminal_view.cpp
|
| 52 |
+
)
|
| 53 |
+
target_include_directories(sov_ui PUBLIC native)
|
| 54 |
+
target_link_libraries(sov_ui PUBLIC sov_core sov_editor d2d1 dwrite ole32)
|
| 55 |
+
|
| 56 |
+
# --- Platform layer (C, Win32) ---
|
| 57 |
+
add_library(sov_platform STATIC
|
| 58 |
+
native/platform/windows/application.c
|
| 59 |
+
native/platform/windows/shell.c
|
| 60 |
+
native/chat/pipe_client.c
|
| 61 |
+
)
|
| 62 |
+
target_include_directories(sov_platform PUBLIC native)
|
| 63 |
+
target_link_libraries(sov_platform PUBLIC sov_core sov_editor sov_ui user32 gdi32 shcore comdlg32 shell32)
|
| 64 |
+
|
| 65 |
+
# --- Main executable ---
|
| 66 |
+
add_executable(sovereign-ide WIN32 native/main.c)
|
| 67 |
+
target_link_libraries(sovereign-ide PRIVATE sov_core sov_platform sov_editor sov_ui)
|
| 68 |
+
if(MINGW)
|
| 69 |
+
target_link_options(sovereign-ide PRIVATE -municode -static-libgcc -static-libstdc++ -static)
|
| 70 |
+
endif()
|
| 71 |
+
|
| 72 |
+
set_target_properties(sovereign-ide PROPERTIES
|
| 73 |
+
OUTPUT_NAME "sovereign-ide"
|
| 74 |
+
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# --- Tests ---
|
| 78 |
+
enable_testing()
|
| 79 |
+
|
| 80 |
+
add_executable(test_core
|
| 81 |
+
tests/test_arena.c
|
| 82 |
+
tests/test_strings.c
|
| 83 |
+
tests/test_events.c
|
| 84 |
+
tests/test_buffer.c
|
| 85 |
+
tests/test_protocol.c
|
| 86 |
+
tests/test_fcl.c
|
| 87 |
+
tests/test_main.c
|
| 88 |
+
)
|
| 89 |
+
target_link_libraries(test_core PRIVATE sov_core)
|
| 90 |
+
target_include_directories(test_core PRIVATE native)
|
| 91 |
+
|
| 92 |
+
add_test(NAME core_tests COMMAND test_core)
|
ide/native/bridge/bridge.h
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Bridge Client — C → Python Engine
|
| 3 |
+
* Sovereign IDE Bridge Layer
|
| 4 |
+
*
|
| 5 |
+
* Communicates with Python sovereign-engine via stdio or HTTP.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
#ifndef BRIDGE_H
|
| 9 |
+
#define BRIDGE_H
|
| 10 |
+
|
| 11 |
+
#include <stddef.h>
|
| 12 |
+
#include <stdbool.h>
|
| 13 |
+
|
| 14 |
+
#ifdef __cplusplus
|
| 15 |
+
extern "C" {
|
| 16 |
+
#endif
|
| 17 |
+
|
| 18 |
+
/* Bridge transport types */
|
| 19 |
+
typedef enum {
|
| 20 |
+
BRIDGE_TRANSPORT_STDIO,
|
| 21 |
+
BRIDGE_TRANSPORT_HTTP
|
| 22 |
+
} BridgeTransport;
|
| 23 |
+
|
| 24 |
+
/* Bridge result codes */
|
| 25 |
+
typedef enum {
|
| 26 |
+
BRIDGE_OK = 0,
|
| 27 |
+
BRIDGE_ERROR_INIT = -1,
|
| 28 |
+
BRIDGE_ERROR_SEND = -2,
|
| 29 |
+
BRIDGE_ERROR_RECV = -3,
|
| 30 |
+
BRIDGE_ERROR_PARSE = -4,
|
| 31 |
+
BRIDGE_ERROR_TIMEOUT = -5
|
| 32 |
+
} BridgeResult;
|
| 33 |
+
|
| 34 |
+
/* Bridge handle (opaque) */
|
| 35 |
+
typedef struct Bridge Bridge;
|
| 36 |
+
|
| 37 |
+
/* Bridge configuration */
|
| 38 |
+
typedef struct {
|
| 39 |
+
BridgeTransport transport;
|
| 40 |
+
|
| 41 |
+
/* HTTP config */
|
| 42 |
+
const char* http_host;
|
| 43 |
+
unsigned short http_port;
|
| 44 |
+
|
| 45 |
+
/* Stdio config */
|
| 46 |
+
const char* python_executable;
|
| 47 |
+
const char* bridge_script;
|
| 48 |
+
|
| 49 |
+
/* Timeouts (milliseconds) */
|
| 50 |
+
unsigned int request_timeout;
|
| 51 |
+
} BridgeConfig;
|
| 52 |
+
|
| 53 |
+
/* Response callback */
|
| 54 |
+
typedef void (*BridgeCallback)(const char* response_json, void* user_data);
|
| 55 |
+
|
| 56 |
+
/*
|
| 57 |
+
* Initialize bridge
|
| 58 |
+
* Returns bridge handle on success, NULL on failure
|
| 59 |
+
*/
|
| 60 |
+
Bridge* bridge_init(const BridgeConfig* config);
|
| 61 |
+
|
| 62 |
+
/*
|
| 63 |
+
* Shutdown bridge
|
| 64 |
+
*/
|
| 65 |
+
void bridge_shutdown(Bridge* bridge);
|
| 66 |
+
|
| 67 |
+
/*
|
| 68 |
+
* Check if bridge is ready
|
| 69 |
+
*/
|
| 70 |
+
bool bridge_is_ready(Bridge* bridge);
|
| 71 |
+
|
| 72 |
+
/*
|
| 73 |
+
* Run agent task (blocking)
|
| 74 |
+
* Returns JSON response string (caller must free)
|
| 75 |
+
*/
|
| 76 |
+
char* bridge_agent_run(Bridge* bridge, const char* task);
|
| 77 |
+
|
| 78 |
+
/*
|
| 79 |
+
* Run agent task (async)
|
| 80 |
+
*/
|
| 81 |
+
BridgeResult bridge_agent_run_async(
|
| 82 |
+
Bridge* bridge,
|
| 83 |
+
const char* task,
|
| 84 |
+
BridgeCallback callback,
|
| 85 |
+
void* user_data
|
| 86 |
+
);
|
| 87 |
+
|
| 88 |
+
/*
|
| 89 |
+
* Execute tool (blocking)
|
| 90 |
+
*/
|
| 91 |
+
char* bridge_tool_execute(
|
| 92 |
+
Bridge* bridge,
|
| 93 |
+
const char* tool_name,
|
| 94 |
+
const char* args_json
|
| 95 |
+
);
|
| 96 |
+
|
| 97 |
+
/*
|
| 98 |
+
* List available tools (blocking)
|
| 99 |
+
*/
|
| 100 |
+
char* bridge_tools_list(Bridge* bridge);
|
| 101 |
+
|
| 102 |
+
/*
|
| 103 |
+
* Send chat message (blocking)
|
| 104 |
+
*/
|
| 105 |
+
char* bridge_chat_message(Bridge* bridge, const char* message);
|
| 106 |
+
|
| 107 |
+
/*
|
| 108 |
+
* Health check (blocking)
|
| 109 |
+
*/
|
| 110 |
+
bool bridge_health_check(Bridge* bridge);
|
| 111 |
+
|
| 112 |
+
/*
|
| 113 |
+
* Get last error message
|
| 114 |
+
*/
|
| 115 |
+
const char* bridge_get_error(Bridge* bridge);
|
| 116 |
+
|
| 117 |
+
/*
|
| 118 |
+
* Set API key for provider
|
| 119 |
+
* provider: "openrouter" or "ollama"
|
| 120 |
+
* Returns JSON response (caller must free)
|
| 121 |
+
*/
|
| 122 |
+
char* bridge_set_key(Bridge* bridge, const char* provider, const char* api_key);
|
| 123 |
+
|
| 124 |
+
/*
|
| 125 |
+
* Get API key status
|
| 126 |
+
* Returns JSON with all provider statuses (caller must free)
|
| 127 |
+
*/
|
| 128 |
+
char* bridge_get_key_status(Bridge* bridge);
|
| 129 |
+
|
| 130 |
+
/*
|
| 131 |
+
* Delete API key for provider
|
| 132 |
+
* Returns JSON response (caller must free)
|
| 133 |
+
*/
|
| 134 |
+
char* bridge_delete_key(Bridge* bridge, const char* provider);
|
| 135 |
+
|
| 136 |
+
#ifdef __cplusplus
|
| 137 |
+
}
|
| 138 |
+
#endif
|
| 139 |
+
|
| 140 |
+
#endif /* BRIDGE_H */
|
ide/native/bridge/bridge_http.c
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Bridge HTTP Client Implementation
|
| 3 |
+
* Uses WinHTTP for HTTP communication with Python engine
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
#include "bridge.h"
|
| 7 |
+
#include <windows.h>
|
| 8 |
+
#include <winhttp.h>
|
| 9 |
+
#include <stdio.h>
|
| 10 |
+
#include <stdlib.h>
|
| 11 |
+
#include <string.h>
|
| 12 |
+
|
| 13 |
+
#pragma comment(lib, "winhttp.lib")
|
| 14 |
+
|
| 15 |
+
/* Bridge internal structure */
|
| 16 |
+
struct Bridge {
|
| 17 |
+
BridgeConfig config;
|
| 18 |
+
HINTERNET hSession;
|
| 19 |
+
HINTERNET hConnect;
|
| 20 |
+
char error_msg[512];
|
| 21 |
+
bool ready;
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
/* Helper: validate input length for JSON */
|
| 25 |
+
static bool validate_input_length(Bridge* bridge, const char* input, const char* field_name) {
|
| 26 |
+
size_t len = strlen(input);
|
| 27 |
+
if (len > 4000) { /* Max input: 4KB minus JSON overhead */
|
| 28 |
+
snprintf(bridge->error_msg, sizeof(bridge->error_msg),
|
| 29 |
+
"%s too long: %lu chars (max 4000)", field_name, (unsigned long)len);
|
| 30 |
+
return false;
|
| 31 |
+
}
|
| 32 |
+
return true;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* Helper: escape JSON special characters */
|
| 36 |
+
static char* json_escape(const char* str) {
|
| 37 |
+
if (!str) return NULL;
|
| 38 |
+
|
| 39 |
+
size_t len = strlen(str);
|
| 40 |
+
/* Worst case: every char needs escaping (doubled length) */
|
| 41 |
+
char* escaped = (char*)malloc(len * 2 + 1);
|
| 42 |
+
if (!escaped) return NULL;
|
| 43 |
+
|
| 44 |
+
size_t j = 0;
|
| 45 |
+
for (size_t i = 0; i < len; i++) {
|
| 46 |
+
switch (str[i]) {
|
| 47 |
+
case '"':
|
| 48 |
+
case '\\':
|
| 49 |
+
escaped[j++] = '\\';
|
| 50 |
+
escaped[j++] = str[i];
|
| 51 |
+
break;
|
| 52 |
+
case '\n':
|
| 53 |
+
escaped[j++] = '\\';
|
| 54 |
+
escaped[j++] = 'n';
|
| 55 |
+
break;
|
| 56 |
+
case '\r':
|
| 57 |
+
escaped[j++] = '\\';
|
| 58 |
+
escaped[j++] = 'r';
|
| 59 |
+
break;
|
| 60 |
+
case '\t':
|
| 61 |
+
escaped[j++] = '\\';
|
| 62 |
+
escaped[j++] = 't';
|
| 63 |
+
break;
|
| 64 |
+
default:
|
| 65 |
+
escaped[j++] = str[i];
|
| 66 |
+
break;
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
escaped[j] = '\0';
|
| 70 |
+
return escaped;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* Helper: HTTP POST request */
|
| 74 |
+
static char* http_post(Bridge* bridge, const char* path, const char* body) {
|
| 75 |
+
if (!bridge || !bridge->hConnect) {
|
| 76 |
+
return NULL;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/* Convert path to wide string */
|
| 80 |
+
wchar_t wide_path[256];
|
| 81 |
+
MultiByteToWideChar(CP_UTF8, 0, path ? path : "/", -1, wide_path, 256);
|
| 82 |
+
|
| 83 |
+
/* Open request */
|
| 84 |
+
HINTERNET hRequest = WinHttpOpenRequest(
|
| 85 |
+
bridge->hConnect,
|
| 86 |
+
L"POST",
|
| 87 |
+
wide_path,
|
| 88 |
+
NULL,
|
| 89 |
+
WINHTTP_NO_REFERER,
|
| 90 |
+
WINHTTP_DEFAULT_ACCEPT_TYPES,
|
| 91 |
+
0
|
| 92 |
+
);
|
| 93 |
+
|
| 94 |
+
if (!hRequest) {
|
| 95 |
+
snprintf(bridge->error_msg, sizeof(bridge->error_msg),
|
| 96 |
+
"Failed to open HTTP request");
|
| 97 |
+
return NULL;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/* Set headers */
|
| 101 |
+
LPCWSTR headers = L"Content-Type: application/json\r\n";
|
| 102 |
+
WinHttpAddRequestHeaders(hRequest, headers, -1, WINHTTP_ADDREQ_FLAG_ADD);
|
| 103 |
+
|
| 104 |
+
/* Send request */
|
| 105 |
+
DWORD body_len = (DWORD)strlen(body);
|
| 106 |
+
BOOL result = WinHttpSendRequest(
|
| 107 |
+
hRequest,
|
| 108 |
+
WINHTTP_NO_ADDITIONAL_HEADERS,
|
| 109 |
+
0,
|
| 110 |
+
(LPVOID)body,
|
| 111 |
+
body_len,
|
| 112 |
+
body_len,
|
| 113 |
+
0
|
| 114 |
+
);
|
| 115 |
+
|
| 116 |
+
if (!result) {
|
| 117 |
+
WinHttpCloseHandle(hRequest);
|
| 118 |
+
snprintf(bridge->error_msg, sizeof(bridge->error_msg),
|
| 119 |
+
"Failed to send HTTP request");
|
| 120 |
+
return NULL;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/* Receive response */
|
| 124 |
+
result = WinHttpReceiveResponse(hRequest, NULL);
|
| 125 |
+
if (!result) {
|
| 126 |
+
WinHttpCloseHandle(hRequest);
|
| 127 |
+
snprintf(bridge->error_msg, sizeof(bridge->error_msg),
|
| 128 |
+
"Failed to receive HTTP response");
|
| 129 |
+
return NULL;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/* Read response body */
|
| 133 |
+
DWORD bytes_available = 0;
|
| 134 |
+
DWORD bytes_read = 0;
|
| 135 |
+
char* response = NULL;
|
| 136 |
+
size_t response_len = 0;
|
| 137 |
+
|
| 138 |
+
do {
|
| 139 |
+
bytes_available = 0;
|
| 140 |
+
if (!WinHttpQueryDataAvailable(hRequest, &bytes_available)) {
|
| 141 |
+
break;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
if (bytes_available == 0) {
|
| 145 |
+
break;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/* Allocate buffer */
|
| 149 |
+
char* buffer = (char*)malloc(bytes_available + 1);
|
| 150 |
+
if (!buffer) break;
|
| 151 |
+
|
| 152 |
+
/* Read data */
|
| 153 |
+
if (WinHttpReadData(hRequest, buffer, bytes_available, &bytes_read)) {
|
| 154 |
+
/* Append to response */
|
| 155 |
+
char* new_response = (char*)realloc(response, response_len + bytes_read + 1);
|
| 156 |
+
if (new_response) {
|
| 157 |
+
response = new_response;
|
| 158 |
+
memcpy(response + response_len, buffer, bytes_read);
|
| 159 |
+
response_len += bytes_read;
|
| 160 |
+
response[response_len] = '\0';
|
| 161 |
+
} else {
|
| 162 |
+
/* realloc failed - clean up */
|
| 163 |
+
free(response);
|
| 164 |
+
free(buffer);
|
| 165 |
+
WinHttpCloseHandle(hRequest);
|
| 166 |
+
snprintf(bridge->error_msg, sizeof(bridge->error_msg),
|
| 167 |
+
"Out of memory during HTTP response");
|
| 168 |
+
return NULL;
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
free(buffer);
|
| 173 |
+
} while (bytes_available > 0);
|
| 174 |
+
|
| 175 |
+
WinHttpCloseHandle(hRequest);
|
| 176 |
+
return response;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/* Helper: HTTP GET request */
|
| 180 |
+
static char* http_get(Bridge* bridge, const char* path) {
|
| 181 |
+
if (!bridge || !bridge->hConnect) {
|
| 182 |
+
return NULL;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
/* Convert path to wide string */
|
| 186 |
+
wchar_t wide_path[256];
|
| 187 |
+
MultiByteToWideChar(CP_UTF8, 0, path ? path : "/", -1, wide_path, 256);
|
| 188 |
+
|
| 189 |
+
/* Open request */
|
| 190 |
+
HINTERNET hRequest = WinHttpOpenRequest(
|
| 191 |
+
bridge->hConnect,
|
| 192 |
+
L"GET",
|
| 193 |
+
wide_path,
|
| 194 |
+
NULL,
|
| 195 |
+
WINHTTP_NO_REFERER,
|
| 196 |
+
WINHTTP_DEFAULT_ACCEPT_TYPES,
|
| 197 |
+
0
|
| 198 |
+
);
|
| 199 |
+
|
| 200 |
+
if (!hRequest) {
|
| 201 |
+
return NULL;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/* Send request */
|
| 205 |
+
BOOL result = WinHttpSendRequest(
|
| 206 |
+
hRequest,
|
| 207 |
+
WINHTTP_NO_ADDITIONAL_HEADERS,
|
| 208 |
+
0,
|
| 209 |
+
WINHTTP_NO_REQUEST_DATA,
|
| 210 |
+
0,
|
| 211 |
+
0,
|
| 212 |
+
0
|
| 213 |
+
);
|
| 214 |
+
|
| 215 |
+
if (!result) {
|
| 216 |
+
WinHttpCloseHandle(hRequest);
|
| 217 |
+
return NULL;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
/* Receive response */
|
| 221 |
+
result = WinHttpReceiveResponse(hRequest, NULL);
|
| 222 |
+
if (!result) {
|
| 223 |
+
WinHttpCloseHandle(hRequest);
|
| 224 |
+
return NULL;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
/* Read response (same as POST) */
|
| 228 |
+
DWORD bytes_available = 0;
|
| 229 |
+
DWORD bytes_read = 0;
|
| 230 |
+
char* response = NULL;
|
| 231 |
+
size_t response_len = 0;
|
| 232 |
+
|
| 233 |
+
do {
|
| 234 |
+
bytes_available = 0;
|
| 235 |
+
if (!WinHttpQueryDataAvailable(hRequest, &bytes_available)) {
|
| 236 |
+
break;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
if (bytes_available == 0) {
|
| 240 |
+
break;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
char* buffer = (char*)malloc(bytes_available + 1);
|
| 244 |
+
if (!buffer) {
|
| 245 |
+
free(response);
|
| 246 |
+
break;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
if (WinHttpReadData(hRequest, buffer, bytes_available, &bytes_read)) {
|
| 250 |
+
char* new_response = (char*)realloc(response, response_len + bytes_read + 1);
|
| 251 |
+
if (new_response) {
|
| 252 |
+
response = new_response;
|
| 253 |
+
memcpy(response + response_len, buffer, bytes_read);
|
| 254 |
+
response_len += bytes_read;
|
| 255 |
+
response[response_len] = '\0';
|
| 256 |
+
} else {
|
| 257 |
+
/* realloc failed - clean up */
|
| 258 |
+
free(response);
|
| 259 |
+
free(buffer);
|
| 260 |
+
WinHttpCloseHandle(hRequest);
|
| 261 |
+
return NULL;
|
| 262 |
+
}
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
free(buffer);
|
| 266 |
+
} while (bytes_available > 0);
|
| 267 |
+
|
| 268 |
+
WinHttpCloseHandle(hRequest);
|
| 269 |
+
return response;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/* Public API */
|
| 273 |
+
|
| 274 |
+
Bridge* bridge_init(const BridgeConfig* config) {
|
| 275 |
+
if (!config) return NULL;
|
| 276 |
+
|
| 277 |
+
Bridge* bridge = (Bridge*)calloc(1, sizeof(Bridge));
|
| 278 |
+
if (!bridge) return NULL;
|
| 279 |
+
|
| 280 |
+
/* Copy config */
|
| 281 |
+
memcpy(&bridge->config, config, sizeof(BridgeConfig));
|
| 282 |
+
|
| 283 |
+
if (config->transport == BRIDGE_TRANSPORT_HTTP) {
|
| 284 |
+
/* Initialize WinHTTP session */
|
| 285 |
+
bridge->hSession = WinHttpOpen(
|
| 286 |
+
L"SovereignIDE/1.0",
|
| 287 |
+
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
| 288 |
+
WINHTTP_NO_PROXY_NAME,
|
| 289 |
+
WINHTTP_NO_PROXY_BYPASS,
|
| 290 |
+
0
|
| 291 |
+
);
|
| 292 |
+
|
| 293 |
+
if (!bridge->hSession) {
|
| 294 |
+
snprintf(bridge->error_msg, sizeof(bridge->error_msg),
|
| 295 |
+
"Failed to initialize WinHTTP");
|
| 296 |
+
free(bridge);
|
| 297 |
+
return NULL;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
/* Set timeout */
|
| 301 |
+
DWORD timeout = config->request_timeout ? config->request_timeout : 30000;
|
| 302 |
+
WinHttpSetTimeouts(bridge->hSession, timeout, timeout, timeout, timeout);
|
| 303 |
+
|
| 304 |
+
/* Connect to host */
|
| 305 |
+
wchar_t host[256];
|
| 306 |
+
MultiByteToWideChar(CP_UTF8, 0, config->http_host, -1, host, 256);
|
| 307 |
+
|
| 308 |
+
bridge->hConnect = WinHttpConnect(
|
| 309 |
+
bridge->hSession,
|
| 310 |
+
host,
|
| 311 |
+
config->http_port,
|
| 312 |
+
0
|
| 313 |
+
);
|
| 314 |
+
|
| 315 |
+
if (!bridge->hConnect) {
|
| 316 |
+
snprintf(bridge->error_msg, sizeof(bridge->error_msg),
|
| 317 |
+
"Failed to connect to %s:%d", config->http_host, config->http_port);
|
| 318 |
+
WinHttpCloseHandle(bridge->hSession);
|
| 319 |
+
free(bridge);
|
| 320 |
+
return NULL;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
bridge->ready = true;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
return bridge;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
void bridge_shutdown(Bridge* bridge) {
|
| 330 |
+
if (!bridge) return;
|
| 331 |
+
|
| 332 |
+
if (bridge->hConnect) {
|
| 333 |
+
WinHttpCloseHandle(bridge->hConnect);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
if (bridge->hSession) {
|
| 337 |
+
WinHttpCloseHandle(bridge->hSession);
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
free(bridge);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
bool bridge_is_ready(Bridge* bridge) {
|
| 344 |
+
return bridge && bridge->ready;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
char* bridge_agent_run(Bridge* bridge, const char* task) {
|
| 348 |
+
if (!bridge || !task) return NULL;
|
| 349 |
+
|
| 350 |
+
/* Validate input length */
|
| 351 |
+
if (!validate_input_length(bridge, task, "Task")) {
|
| 352 |
+
return NULL;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
/* Escape JSON special characters */
|
| 356 |
+
char* escaped_task = json_escape(task);
|
| 357 |
+
if (!escaped_task) {
|
| 358 |
+
snprintf(bridge->error_msg, sizeof(bridge->error_msg),
|
| 359 |
+
"Out of memory during JSON escaping");
|
| 360 |
+
return NULL;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
/* Build request JSON */
|
| 364 |
+
char request_body[4096];
|
| 365 |
+
snprintf(request_body, sizeof(request_body),
|
| 366 |
+
"{\"task\":\"%s\"}", escaped_task);
|
| 367 |
+
|
| 368 |
+
free(escaped_task);
|
| 369 |
+
|
| 370 |
+
/* POST to /agent/run */
|
| 371 |
+
return http_post(bridge, "/agent/run", request_body);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
char* bridge_tool_execute(Bridge* bridge, const char* tool_name, const char* args_json) {
|
| 375 |
+
if (!bridge || !tool_name) return NULL;
|
| 376 |
+
|
| 377 |
+
/* Build request */
|
| 378 |
+
char request_body[4096];
|
| 379 |
+
snprintf(request_body, sizeof(request_body),
|
| 380 |
+
"{\"tool\":\"%s\",\"args\":%s}",
|
| 381 |
+
tool_name, args_json ? args_json : "{}");
|
| 382 |
+
|
| 383 |
+
return http_post(bridge, "/tool/execute", request_body);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
char* bridge_tools_list(Bridge* bridge) {
|
| 387 |
+
if (!bridge) return NULL;
|
| 388 |
+
return http_get(bridge, "/tools");
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
char* bridge_chat_message(Bridge* bridge, const char* message) {
|
| 392 |
+
if (!bridge || !message) return NULL;
|
| 393 |
+
|
| 394 |
+
/* Validate input length */
|
| 395 |
+
if (!validate_input_length(bridge, message, "Message")) {
|
| 396 |
+
return NULL;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
/* Escape JSON special characters */
|
| 400 |
+
char* escaped_message = json_escape(message);
|
| 401 |
+
if (!escaped_message) {
|
| 402 |
+
snprintf(bridge->error_msg, sizeof(bridge->error_msg),
|
| 403 |
+
"Out of memory during JSON escaping");
|
| 404 |
+
return NULL;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
char request_body[4096];
|
| 408 |
+
snprintf(request_body, sizeof(request_body),
|
| 409 |
+
"{\"message\":\"%s\"}", escaped_message);
|
| 410 |
+
|
| 411 |
+
free(escaped_message);
|
| 412 |
+
|
| 413 |
+
return http_post(bridge, "/chat", request_body);
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
bool bridge_health_check(Bridge* bridge) {
|
| 417 |
+
if (!bridge) return false;
|
| 418 |
+
|
| 419 |
+
char* response = http_get(bridge, "/health");
|
| 420 |
+
if (!response) return false;
|
| 421 |
+
|
| 422 |
+
/* Check for "ok" in response */
|
| 423 |
+
bool ok = strstr(response, "ok") != NULL;
|
| 424 |
+
free(response);
|
| 425 |
+
|
| 426 |
+
return ok;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
const char* bridge_get_error(Bridge* bridge) {
|
| 430 |
+
return bridge ? bridge->error_msg : "Bridge is NULL";
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
char* bridge_set_key(Bridge* bridge, const char* provider, const char* api_key) {
|
| 434 |
+
if (!bridge || !provider || !api_key) return NULL;
|
| 435 |
+
|
| 436 |
+
/* Validate input lengths */
|
| 437 |
+
if (!validate_input_length(bridge, provider, "Provider")) {
|
| 438 |
+
return NULL;
|
| 439 |
+
}
|
| 440 |
+
if (!validate_input_length(bridge, api_key, "API key")) {
|
| 441 |
+
return NULL;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
/* Escape JSON special characters */
|
| 445 |
+
char* escaped_provider = json_escape(provider);
|
| 446 |
+
char* escaped_key = json_escape(api_key);
|
| 447 |
+
|
| 448 |
+
if (!escaped_provider || !escaped_key) {
|
| 449 |
+
free(escaped_provider);
|
| 450 |
+
free(escaped_key);
|
| 451 |
+
snprintf(bridge->error_msg, sizeof(bridge->error_msg),
|
| 452 |
+
"Out of memory during JSON escaping");
|
| 453 |
+
return NULL;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
/* Build request JSON */
|
| 457 |
+
char request_body[4096];
|
| 458 |
+
snprintf(request_body, sizeof(request_body),
|
| 459 |
+
"{\"provider\":\"%s\",\"key\":\"%s\"}",
|
| 460 |
+
escaped_provider, escaped_key);
|
| 461 |
+
|
| 462 |
+
free(escaped_provider);
|
| 463 |
+
free(escaped_key);
|
| 464 |
+
|
| 465 |
+
return http_post(bridge, "/keys/set", request_body);
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
char* bridge_get_key_status(Bridge* bridge) {
|
| 469 |
+
if (!bridge) return NULL;
|
| 470 |
+
return http_get(bridge, "/keys/status");
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
char* bridge_delete_key(Bridge* bridge, const char* provider) {
|
| 474 |
+
if (!bridge || !provider) return NULL;
|
| 475 |
+
|
| 476 |
+
/* Build path */
|
| 477 |
+
char path[256];
|
| 478 |
+
snprintf(path, sizeof(path), "/keys/%s", provider);
|
| 479 |
+
|
| 480 |
+
/* Need to implement DELETE - for now use POST with _method */
|
| 481 |
+
char request_body[256];
|
| 482 |
+
snprintf(request_body, sizeof(request_body), "{\"_method\":\"DELETE\"}");
|
| 483 |
+
|
| 484 |
+
return http_post(bridge, path, request_body);
|
| 485 |
+
}
|
ide/native/bridge/example.c
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Bridge Usage Example
|
| 3 |
+
* Demonstrates how to use the C → Python bridge
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
#include "bridge.h"
|
| 7 |
+
#include <stdio.h>
|
| 8 |
+
#include <stdlib.h>
|
| 9 |
+
|
| 10 |
+
int main(int argc, char** argv) {
|
| 11 |
+
/* Configure HTTP bridge */
|
| 12 |
+
BridgeConfig config = {
|
| 13 |
+
.transport = BRIDGE_TRANSPORT_HTTP,
|
| 14 |
+
.http_host = "127.0.0.1",
|
| 15 |
+
.http_port = 19000,
|
| 16 |
+
.request_timeout = 30000 /* 30 seconds */
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
/* Initialize bridge */
|
| 20 |
+
printf("Initializing bridge...\n");
|
| 21 |
+
Bridge* bridge = bridge_init(&config);
|
| 22 |
+
if (!bridge) {
|
| 23 |
+
fprintf(stderr, "Failed to initialize bridge\n");
|
| 24 |
+
return 1;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* Health check */
|
| 28 |
+
printf("Health check...\n");
|
| 29 |
+
if (bridge_health_check(bridge)) {
|
| 30 |
+
printf("✓ Bridge is healthy\n\n");
|
| 31 |
+
} else {
|
| 32 |
+
fprintf(stderr, "✗ Bridge health check failed: %s\n",
|
| 33 |
+
bridge_get_error(bridge));
|
| 34 |
+
bridge_shutdown(bridge);
|
| 35 |
+
return 1;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/* List tools */
|
| 39 |
+
printf("Listing available tools...\n");
|
| 40 |
+
char* tools = bridge_tools_list(bridge);
|
| 41 |
+
if (tools) {
|
| 42 |
+
printf("Tools: %s\n\n", tools);
|
| 43 |
+
free(tools);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/* Send chat message */
|
| 47 |
+
printf("Sending chat message...\n");
|
| 48 |
+
char* chat_response = bridge_chat_message(bridge, "Hello from C!");
|
| 49 |
+
if (chat_response) {
|
| 50 |
+
printf("Response: %s\n\n", chat_response);
|
| 51 |
+
free(chat_response);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* Run agent task */
|
| 55 |
+
printf("Running agent task...\n");
|
| 56 |
+
char* agent_result = bridge_agent_run(
|
| 57 |
+
bridge,
|
| 58 |
+
"List all files in the current directory"
|
| 59 |
+
);
|
| 60 |
+
|
| 61 |
+
if (agent_result) {
|
| 62 |
+
printf("Agent result: %s\n\n", agent_result);
|
| 63 |
+
free(agent_result);
|
| 64 |
+
} else {
|
| 65 |
+
fprintf(stderr, "Agent failed: %s\n", bridge_get_error(bridge));
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/* Execute tool */
|
| 69 |
+
printf("Executing tool...\n");
|
| 70 |
+
char* tool_result = bridge_tool_execute(
|
| 71 |
+
bridge,
|
| 72 |
+
"filesystem.read",
|
| 73 |
+
"{\"path\":\"README.md\"}"
|
| 74 |
+
);
|
| 75 |
+
|
| 76 |
+
if (tool_result) {
|
| 77 |
+
printf("Tool result: %s\n\n", tool_result);
|
| 78 |
+
free(tool_result);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* Cleanup */
|
| 82 |
+
bridge_shutdown(bridge);
|
| 83 |
+
printf("Bridge shutdown complete\n");
|
| 84 |
+
|
| 85 |
+
return 0;
|
| 86 |
+
}
|
ide/native/bridge/test_keys.c
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* API Key Management Test
|
| 3 |
+
* Demonstrates setting keys via bridge
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
#include "bridge.h"
|
| 7 |
+
#include <stdio.h>
|
| 8 |
+
#include <stdlib.h>
|
| 9 |
+
#include <string.h>
|
| 10 |
+
|
| 11 |
+
void print_status(Bridge* bridge) {
|
| 12 |
+
printf("\n=== API Key Status ===\n");
|
| 13 |
+
char* status = bridge_get_key_status(bridge);
|
| 14 |
+
if (status) {
|
| 15 |
+
printf("%s\n", status);
|
| 16 |
+
free(status);
|
| 17 |
+
} else {
|
| 18 |
+
printf("Failed to get status\n");
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
int main(int argc, char** argv) {
|
| 23 |
+
/* Configure bridge */
|
| 24 |
+
BridgeConfig config = {
|
| 25 |
+
.transport = BRIDGE_TRANSPORT_HTTP,
|
| 26 |
+
.http_host = "127.0.0.1",
|
| 27 |
+
.http_port = 19000,
|
| 28 |
+
.request_timeout = 30000
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
/* Initialize */
|
| 32 |
+
printf("Initializing bridge...\n");
|
| 33 |
+
Bridge* bridge = bridge_init(&config);
|
| 34 |
+
if (!bridge) {
|
| 35 |
+
fprintf(stderr, "Failed to initialize bridge\n");
|
| 36 |
+
return 1;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/* Health check */
|
| 40 |
+
if (!bridge_health_check(bridge)) {
|
| 41 |
+
fprintf(stderr, "Bridge not healthy\n");
|
| 42 |
+
bridge_shutdown(bridge);
|
| 43 |
+
return 1;
|
| 44 |
+
}
|
| 45 |
+
printf("OK: Bridge healthy\n");
|
| 46 |
+
|
| 47 |
+
/* Get initial status */
|
| 48 |
+
print_status(bridge);
|
| 49 |
+
|
| 50 |
+
/* Simulate UI - set OpenRouter key */
|
| 51 |
+
printf("\n--- Setting OpenRouter Key ---\n");
|
| 52 |
+
printf("(In real UI, user would paste their key here)\n");
|
| 53 |
+
|
| 54 |
+
const char* demo_key = "sk-or-v1-demo-key-12345"; /* Demo key */
|
| 55 |
+
char* set_result = bridge_set_key(bridge, "openrouter", demo_key);
|
| 56 |
+
if (set_result) {
|
| 57 |
+
printf("Result: %s\n", set_result);
|
| 58 |
+
free(set_result);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* Get updated status */
|
| 62 |
+
print_status(bridge);
|
| 63 |
+
|
| 64 |
+
/* Test chat with key set */
|
| 65 |
+
printf("\n--- Testing Chat (should use OpenRouter) ---\n");
|
| 66 |
+
char* chat_reply = bridge_chat_message(bridge, "Hello! Test message.");
|
| 67 |
+
if (chat_reply) {
|
| 68 |
+
printf("Reply: %s\n", chat_reply);
|
| 69 |
+
free(chat_reply);
|
| 70 |
+
} else {
|
| 71 |
+
printf("Chat failed (expected - demo key not real)\n");
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* Delete key */
|
| 75 |
+
printf("\n--- Deleting OpenRouter Key ---\n");
|
| 76 |
+
char* delete_result = bridge_delete_key(bridge, "openrouter");
|
| 77 |
+
if (delete_result) {
|
| 78 |
+
printf("Result: %s\n", delete_result);
|
| 79 |
+
free(delete_result);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* Final status */
|
| 83 |
+
print_status(bridge);
|
| 84 |
+
|
| 85 |
+
/* Cleanup */
|
| 86 |
+
bridge_shutdown(bridge);
|
| 87 |
+
printf("\nDone\n");
|
| 88 |
+
|
| 89 |
+
return 0;
|
| 90 |
+
}
|
ide/native/bridge/test_tools.c
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Tool Execution Test
|
| 3 |
+
* Tests filesystem and code tools
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
#include "bridge.h"
|
| 7 |
+
#include <stdio.h>
|
| 8 |
+
#include <stdlib.h>
|
| 9 |
+
|
| 10 |
+
int main(int argc, char** argv) {
|
| 11 |
+
/* Configure bridge */
|
| 12 |
+
BridgeConfig config = {
|
| 13 |
+
.transport = BRIDGE_TRANSPORT_HTTP,
|
| 14 |
+
.http_host = "127.0.0.1",
|
| 15 |
+
.http_port = 19000,
|
| 16 |
+
.request_timeout = 30000
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
/* Initialize */
|
| 20 |
+
printf("Initializing bridge...\n");
|
| 21 |
+
Bridge* bridge = bridge_init(&config);
|
| 22 |
+
if (!bridge) {
|
| 23 |
+
fprintf(stderr, "Failed to initialize bridge\n");
|
| 24 |
+
return 1;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* Health check */
|
| 28 |
+
if (!bridge_health_check(bridge)) {
|
| 29 |
+
fprintf(stderr, "Bridge not healthy\n");
|
| 30 |
+
bridge_shutdown(bridge);
|
| 31 |
+
return 1;
|
| 32 |
+
}
|
| 33 |
+
printf("OK: Bridge healthy\n\n");
|
| 34 |
+
|
| 35 |
+
/* List available tools */
|
| 36 |
+
printf("=== Available Tools ===\n");
|
| 37 |
+
char* tools = bridge_tools_list(bridge);
|
| 38 |
+
if (tools) {
|
| 39 |
+
printf("%s\n\n", tools);
|
| 40 |
+
free(tools);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* Test filesystem.read */
|
| 44 |
+
printf("=== Test: filesystem.read ===\n");
|
| 45 |
+
char* read_result = bridge_tool_execute(
|
| 46 |
+
bridge,
|
| 47 |
+
"filesystem.read",
|
| 48 |
+
"{\"path\":\"README.md\"}"
|
| 49 |
+
);
|
| 50 |
+
if (read_result) {
|
| 51 |
+
printf("Result: %s\n\n", read_result);
|
| 52 |
+
free(read_result);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/* Test filesystem.write */
|
| 56 |
+
printf("=== Test: filesystem.write ===\n");
|
| 57 |
+
char* write_result = bridge_tool_execute(
|
| 58 |
+
bridge,
|
| 59 |
+
"filesystem.write",
|
| 60 |
+
"{\"path\":\"test_output.txt\",\"content\":\"Hello from C!\\nBridge is working.\"}"
|
| 61 |
+
);
|
| 62 |
+
if (write_result) {
|
| 63 |
+
printf("Result: %s\n\n", write_result);
|
| 64 |
+
free(write_result);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* Test filesystem.list */
|
| 68 |
+
printf("=== Test: filesystem.list ===\n");
|
| 69 |
+
char* list_result = bridge_tool_execute(
|
| 70 |
+
bridge,
|
| 71 |
+
"filesystem.list",
|
| 72 |
+
"{\"path\":\".\"}"
|
| 73 |
+
);
|
| 74 |
+
if (list_result) {
|
| 75 |
+
printf("Result: %s\n\n", list_result);
|
| 76 |
+
free(list_result);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/* Cleanup */
|
| 80 |
+
bridge_shutdown(bridge);
|
| 81 |
+
printf("Done\n");
|
| 82 |
+
|
| 83 |
+
return 0;
|
| 84 |
+
}
|
ide/native/chat/pipe_client.c
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Named Pipe Client
|
| 3 |
+
* Connects to the Elixir SovereignChat process via \\.\pipe\sovereign-ide-chat.
|
| 4 |
+
* Framed binary protocol per contract section 8.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
#include "pipe_client.h"
|
| 8 |
+
#include "protocol.h"
|
| 9 |
+
#include "../core/errors.h"
|
| 10 |
+
#include "../core/arena.h"
|
| 11 |
+
|
| 12 |
+
#include <string.h>
|
| 13 |
+
|
| 14 |
+
#define PIPE_NAME L"\\\\.\\pipe\\sovereign-ide-chat"
|
| 15 |
+
#define PIPE_BUFFER_SIZE (64 * 1024)
|
| 16 |
+
#define RECONNECT_DELAY_MS 1000
|
| 17 |
+
#define MAX_RECONNECT_ATTEMPTS 10
|
| 18 |
+
|
| 19 |
+
static struct {
|
| 20 |
+
HANDLE pipe;
|
| 21 |
+
bool connected;
|
| 22 |
+
Arena recv_arena;
|
| 23 |
+
uint8_t recv_buf[PIPE_BUFFER_SIZE];
|
| 24 |
+
} g_pipe;
|
| 25 |
+
|
| 26 |
+
SovResult pipe_client_init(void) {
|
| 27 |
+
g_pipe.pipe = INVALID_HANDLE_VALUE;
|
| 28 |
+
g_pipe.connected = false;
|
| 29 |
+
arena_init(&g_pipe.recv_arena, 256 * 1024);
|
| 30 |
+
return SOV_OK;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
SovResult pipe_client_connect(void) {
|
| 34 |
+
for (int attempt = 0; attempt < MAX_RECONNECT_ATTEMPTS; attempt++) {
|
| 35 |
+
g_pipe.pipe = CreateFileW(
|
| 36 |
+
PIPE_NAME,
|
| 37 |
+
GENERIC_READ | GENERIC_WRITE,
|
| 38 |
+
0, NULL,
|
| 39 |
+
OPEN_EXISTING,
|
| 40 |
+
FILE_FLAG_OVERLAPPED,
|
| 41 |
+
NULL
|
| 42 |
+
);
|
| 43 |
+
|
| 44 |
+
if (g_pipe.pipe != INVALID_HANDLE_VALUE) {
|
| 45 |
+
DWORD mode = PIPE_READMODE_MESSAGE;
|
| 46 |
+
SetNamedPipeHandleState(g_pipe.pipe, &mode, NULL, NULL);
|
| 47 |
+
g_pipe.connected = true;
|
| 48 |
+
return SOV_OK;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
if (GetLastError() != ERROR_PIPE_BUSY) {
|
| 52 |
+
Sleep(RECONNECT_DELAY_MS);
|
| 53 |
+
} else {
|
| 54 |
+
WaitNamedPipeW(PIPE_NAME, RECONNECT_DELAY_MS);
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
return SOV_ERR_PIPE;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
SovResult pipe_client_send(const uint8_t *data, size_t len) {
|
| 62 |
+
if (!g_pipe.connected) return SOV_ERR_PIPE;
|
| 63 |
+
|
| 64 |
+
DWORD written;
|
| 65 |
+
BOOL ok = WriteFile(g_pipe.pipe, data, (DWORD)len, &written, NULL);
|
| 66 |
+
if (!ok || written != (DWORD)len) return SOV_ERR_PIPE;
|
| 67 |
+
|
| 68 |
+
return SOV_OK;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
SovResult pipe_client_recv(uint8_t *out, size_t max_len, size_t *out_len) {
|
| 72 |
+
if (!g_pipe.connected) return SOV_ERR_PIPE;
|
| 73 |
+
|
| 74 |
+
DWORD read;
|
| 75 |
+
BOOL ok = ReadFile(g_pipe.pipe, out, (DWORD)max_len, &read, NULL);
|
| 76 |
+
if (!ok) {
|
| 77 |
+
DWORD err = GetLastError();
|
| 78 |
+
if (err == ERROR_MORE_DATA) {
|
| 79 |
+
*out_len = read;
|
| 80 |
+
return SOV_OK;
|
| 81 |
+
}
|
| 82 |
+
g_pipe.connected = false;
|
| 83 |
+
return SOV_ERR_PIPE;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
*out_len = read;
|
| 87 |
+
return SOV_OK;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
bool pipe_client_is_connected(void) {
|
| 91 |
+
return g_pipe.connected;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
void pipe_client_disconnect(void) {
|
| 95 |
+
if (g_pipe.pipe != INVALID_HANDLE_VALUE) {
|
| 96 |
+
CloseHandle(g_pipe.pipe);
|
| 97 |
+
g_pipe.pipe = INVALID_HANDLE_VALUE;
|
| 98 |
+
}
|
| 99 |
+
g_pipe.connected = false;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
void pipe_client_shutdown(void) {
|
| 103 |
+
pipe_client_disconnect();
|
| 104 |
+
arena_destroy(&g_pipe.recv_arena);
|
| 105 |
+
}
|
ide/native/chat/pipe_client.h
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_PIPE_CLIENT_H
|
| 2 |
+
#define SOVEREIGN_PIPE_CLIENT_H
|
| 3 |
+
|
| 4 |
+
#include <windows.h>
|
| 5 |
+
#include <stdint.h>
|
| 6 |
+
#include <stddef.h>
|
| 7 |
+
#include <stdbool.h>
|
| 8 |
+
#include "../core/errors.h"
|
| 9 |
+
|
| 10 |
+
SovResult pipe_client_init(void);
|
| 11 |
+
SovResult pipe_client_connect(void);
|
| 12 |
+
SovResult pipe_client_send(const uint8_t *data, size_t len);
|
| 13 |
+
SovResult pipe_client_recv(uint8_t *out, size_t max_len, size_t *out_len);
|
| 14 |
+
bool pipe_client_is_connected(void);
|
| 15 |
+
void pipe_client_disconnect(void);
|
| 16 |
+
void pipe_client_shutdown(void);
|
| 17 |
+
|
| 18 |
+
#endif /* SOVEREIGN_PIPE_CLIENT_H */
|
ide/native/chat/protocol.c
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — IPC Protocol
|
| 3 |
+
* CRC32 checksum for frame validation.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
#include "protocol.h"
|
| 7 |
+
|
| 8 |
+
uint32_t protocol_checksum(const uint8_t *data, size_t len) {
|
| 9 |
+
uint32_t crc = 0xFFFFFFFF;
|
| 10 |
+
for (size_t i = 0; i < len; i++) {
|
| 11 |
+
crc ^= data[i];
|
| 12 |
+
for (int j = 0; j < 8; j++) {
|
| 13 |
+
crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
return ~crc;
|
| 17 |
+
}
|
ide/native/chat/protocol.h
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_PROTOCOL_H
|
| 2 |
+
#define SOVEREIGN_PROTOCOL_H
|
| 3 |
+
|
| 4 |
+
#include <stdint.h>
|
| 5 |
+
#include <stddef.h>
|
| 6 |
+
|
| 7 |
+
#define PROTOCOL_VERSION 1
|
| 8 |
+
#define MAX_MESSAGE_SIZE (4 * 1024 * 1024)
|
| 9 |
+
|
| 10 |
+
typedef enum MsgType {
|
| 11 |
+
MSG_HANDSHAKE = 0x01,
|
| 12 |
+
MSG_CONVERSATION_CREATE = 0x10,
|
| 13 |
+
MSG_CONVERSATION_MSG = 0x11,
|
| 14 |
+
MSG_CONVERSATION_CANCEL = 0x12,
|
| 15 |
+
MSG_STREAM_STARTED = 0x20,
|
| 16 |
+
MSG_STREAM_DELTA = 0x21,
|
| 17 |
+
MSG_STREAM_COMPLETED = 0x22,
|
| 18 |
+
MSG_TOOL_STARTED = 0x30,
|
| 19 |
+
MSG_TOOL_COMPLETED = 0x31,
|
| 20 |
+
MSG_APPROVAL_REQUESTED = 0x40,
|
| 21 |
+
MSG_APPROVAL_RESOLVED = 0x41,
|
| 22 |
+
MSG_AGENT_STATE = 0x50,
|
| 23 |
+
MSG_ERROR = 0xFF,
|
| 24 |
+
} MsgType;
|
| 25 |
+
|
| 26 |
+
#pragma pack(push, 1)
|
| 27 |
+
typedef struct FrameHeader {
|
| 28 |
+
uint8_t protocol_version;
|
| 29 |
+
uint8_t message_type;
|
| 30 |
+
uint32_t request_id;
|
| 31 |
+
uint32_t workspace_id;
|
| 32 |
+
uint32_t conversation_id;
|
| 33 |
+
uint32_t payload_length;
|
| 34 |
+
uint32_t checksum;
|
| 35 |
+
} FrameHeader;
|
| 36 |
+
#pragma pack(pop)
|
| 37 |
+
|
| 38 |
+
#define FRAME_HEADER_SIZE sizeof(FrameHeader)
|
| 39 |
+
|
| 40 |
+
uint32_t protocol_checksum(const uint8_t *data, size_t len);
|
| 41 |
+
|
| 42 |
+
#endif /* SOVEREIGN_PROTOCOL_H */
|
ide/native/core/arena.c
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Arena Allocator
|
| 3 |
+
* Bump-pointer allocator for frame-scoped allocations.
|
| 4 |
+
* No individual frees. Reset the arena to reclaim all memory at once.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
#include "arena.h"
|
| 8 |
+
#include <string.h>
|
| 9 |
+
#include <assert.h>
|
| 10 |
+
|
| 11 |
+
#ifndef ARENA_DEFAULT_CAPACITY
|
| 12 |
+
#define ARENA_DEFAULT_CAPACITY (1024 * 1024) /* 1 MiB */
|
| 13 |
+
#endif
|
| 14 |
+
|
| 15 |
+
#define ARENA_ALIGN 16
|
| 16 |
+
|
| 17 |
+
static size_t align_up(size_t n, size_t align) {
|
| 18 |
+
return (n + align - 1) & ~(align - 1);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
void arena_init(Arena *a, size_t capacity) {
|
| 22 |
+
a->base = (uint8_t *)VirtualAlloc(NULL, capacity,
|
| 23 |
+
MEM_RESERVE | MEM_COMMIT,
|
| 24 |
+
PAGE_READWRITE);
|
| 25 |
+
assert(a->base != NULL);
|
| 26 |
+
a->capacity = capacity;
|
| 27 |
+
a->offset = 0;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
void arena_init_default(Arena *a) {
|
| 31 |
+
arena_init(a, ARENA_DEFAULT_CAPACITY);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
void *arena_alloc(Arena *a, size_t size) {
|
| 35 |
+
size_t aligned = align_up(size, ARENA_ALIGN);
|
| 36 |
+
assert(a->offset + aligned <= a->capacity);
|
| 37 |
+
void *ptr = a->base + a->offset;
|
| 38 |
+
a->offset += aligned;
|
| 39 |
+
return ptr;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
void *arena_alloc_zero(Arena *a, size_t size) {
|
| 43 |
+
void *ptr = arena_alloc(a, size);
|
| 44 |
+
memset(ptr, 0, size);
|
| 45 |
+
return ptr;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
void arena_reset(Arena *a) {
|
| 49 |
+
a->offset = 0;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
void arena_destroy(Arena *a) {
|
| 53 |
+
if (a->base) {
|
| 54 |
+
VirtualFree(a->base, 0, MEM_RELEASE);
|
| 55 |
+
a->base = NULL;
|
| 56 |
+
}
|
| 57 |
+
a->capacity = 0;
|
| 58 |
+
a->offset = 0;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
ArenaMark arena_mark(Arena *a) {
|
| 62 |
+
return (ArenaMark){ .offset = a->offset };
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
void arena_restore(Arena *a, ArenaMark mark) {
|
| 66 |
+
assert(mark.offset <= a->offset);
|
| 67 |
+
a->offset = mark.offset;
|
| 68 |
+
}
|
ide/native/core/arena.h
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_ARENA_H
|
| 2 |
+
#define SOVEREIGN_ARENA_H
|
| 3 |
+
|
| 4 |
+
#include <stddef.h>
|
| 5 |
+
#include <stdint.h>
|
| 6 |
+
#include <windows.h>
|
| 7 |
+
|
| 8 |
+
typedef struct Arena {
|
| 9 |
+
uint8_t *base;
|
| 10 |
+
size_t capacity;
|
| 11 |
+
size_t offset;
|
| 12 |
+
} Arena;
|
| 13 |
+
|
| 14 |
+
typedef struct ArenaMark {
|
| 15 |
+
size_t offset;
|
| 16 |
+
} ArenaMark;
|
| 17 |
+
|
| 18 |
+
void arena_init(Arena *a, size_t capacity);
|
| 19 |
+
void arena_init_default(Arena *a);
|
| 20 |
+
void *arena_alloc(Arena *a, size_t size);
|
| 21 |
+
void *arena_alloc_zero(Arena *a, size_t size);
|
| 22 |
+
void arena_reset(Arena *a);
|
| 23 |
+
void arena_destroy(Arena *a);
|
| 24 |
+
|
| 25 |
+
ArenaMark arena_mark(Arena *a);
|
| 26 |
+
void arena_restore(Arena *a, ArenaMark mark);
|
| 27 |
+
|
| 28 |
+
#endif /* SOVEREIGN_ARENA_H */
|
ide/native/core/errors.h
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_ERRORS_H
|
| 2 |
+
#define SOVEREIGN_ERRORS_H
|
| 3 |
+
|
| 4 |
+
#include <stdio.h>
|
| 5 |
+
#include <stdlib.h>
|
| 6 |
+
|
| 7 |
+
typedef enum SovResult {
|
| 8 |
+
SOV_OK = 0,
|
| 9 |
+
SOV_ERR_ALLOC,
|
| 10 |
+
SOV_ERR_IO,
|
| 11 |
+
SOV_ERR_PARSE,
|
| 12 |
+
SOV_ERR_PROTOCOL,
|
| 13 |
+
SOV_ERR_PIPE,
|
| 14 |
+
SOV_ERR_FCL_DENIED,
|
| 15 |
+
SOV_ERR_FCL_INVALID,
|
| 16 |
+
SOV_ERR_LSP,
|
| 17 |
+
SOV_ERR_GIT,
|
| 18 |
+
SOV_ERR_BUILD,
|
| 19 |
+
} SovResult;
|
| 20 |
+
|
| 21 |
+
#define SOV_ASSERT(cond, msg) \
|
| 22 |
+
do { (void)(cond); (void)(msg); } while(0)
|
| 23 |
+
|
| 24 |
+
#define SOV_TRY(expr) \
|
| 25 |
+
do { SovResult _r = (expr); if (_r != SOV_OK) return _r; } while(0)
|
| 26 |
+
|
| 27 |
+
#endif /* SOVEREIGN_ERRORS_H */
|
ide/native/core/events.c
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Event System
|
| 3 |
+
* Lock-free SPSC ring buffer for inter-module communication.
|
| 4 |
+
* C native modules post events; consumers drain per frame.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
#include "events.h"
|
| 8 |
+
#include <string.h>
|
| 9 |
+
#include <assert.h>
|
| 10 |
+
|
| 11 |
+
#define EVENT_QUEUE_CAPACITY 4096
|
| 12 |
+
|
| 13 |
+
static struct {
|
| 14 |
+
Event ring[EVENT_QUEUE_CAPACITY];
|
| 15 |
+
volatile LONG write_pos;
|
| 16 |
+
volatile LONG read_pos;
|
| 17 |
+
} g_events;
|
| 18 |
+
|
| 19 |
+
void events_init(void) {
|
| 20 |
+
memset(&g_events, 0, sizeof(g_events));
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
bool event_post(Event ev) {
|
| 24 |
+
LONG w = g_events.write_pos;
|
| 25 |
+
LONG next = (w + 1) % EVENT_QUEUE_CAPACITY;
|
| 26 |
+
if (next == g_events.read_pos) {
|
| 27 |
+
return false; /* queue full */
|
| 28 |
+
}
|
| 29 |
+
g_events.ring[w] = ev;
|
| 30 |
+
MemoryBarrier();
|
| 31 |
+
InterlockedExchange(&g_events.write_pos, next);
|
| 32 |
+
return true;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
bool event_poll(Event *out) {
|
| 36 |
+
LONG r = g_events.read_pos;
|
| 37 |
+
if (r == g_events.write_pos) {
|
| 38 |
+
return false; /* empty */
|
| 39 |
+
}
|
| 40 |
+
*out = g_events.ring[r];
|
| 41 |
+
MemoryBarrier();
|
| 42 |
+
InterlockedExchange(&g_events.read_pos, (r + 1) % EVENT_QUEUE_CAPACITY);
|
| 43 |
+
return true;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
void events_drain(EventHandler handler, void *ctx) {
|
| 47 |
+
Event ev;
|
| 48 |
+
while (event_poll(&ev)) {
|
| 49 |
+
handler(&ev, ctx);
|
| 50 |
+
}
|
| 51 |
+
}
|
ide/native/core/events.h
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_EVENTS_H
|
| 2 |
+
#define SOVEREIGN_EVENTS_H
|
| 3 |
+
|
| 4 |
+
#include <stdbool.h>
|
| 5 |
+
#include <stdint.h>
|
| 6 |
+
#include <windows.h>
|
| 7 |
+
|
| 8 |
+
typedef enum EventKind {
|
| 9 |
+
EVENT_NONE = 0,
|
| 10 |
+
EVENT_KEY_DOWN,
|
| 11 |
+
EVENT_KEY_UP,
|
| 12 |
+
EVENT_CHAR_INPUT,
|
| 13 |
+
EVENT_MOUSE_MOVE,
|
| 14 |
+
EVENT_MOUSE_DOWN,
|
| 15 |
+
EVENT_MOUSE_UP,
|
| 16 |
+
EVENT_MOUSE_WHEEL,
|
| 17 |
+
EVENT_RESIZE,
|
| 18 |
+
EVENT_FOCUS_GAINED,
|
| 19 |
+
EVENT_FOCUS_LOST,
|
| 20 |
+
EVENT_FILE_CHANGED,
|
| 21 |
+
EVENT_CHAT_MESSAGE,
|
| 22 |
+
EVENT_TOOL_RESULT,
|
| 23 |
+
EVENT_APPROVAL_REQUEST,
|
| 24 |
+
EVENT_BUILD_OUTPUT,
|
| 25 |
+
EVENT_DIAGNOSTIC,
|
| 26 |
+
EVENT_QUIT,
|
| 27 |
+
} EventKind;
|
| 28 |
+
|
| 29 |
+
typedef struct Event {
|
| 30 |
+
EventKind kind;
|
| 31 |
+
uint64_t timestamp;
|
| 32 |
+
union {
|
| 33 |
+
struct { uint32_t vk; uint32_t scancode; uint32_t mods; } key;
|
| 34 |
+
struct { uint32_t codepoint; } ch;
|
| 35 |
+
struct { int32_t x; int32_t y; uint32_t buttons; } mouse;
|
| 36 |
+
struct { int32_t delta; } wheel;
|
| 37 |
+
struct { uint32_t width; uint32_t height; } resize;
|
| 38 |
+
struct { uint64_t id; } message;
|
| 39 |
+
};
|
| 40 |
+
} Event;
|
| 41 |
+
|
| 42 |
+
typedef void (*EventHandler)(const Event *ev, void *ctx);
|
| 43 |
+
|
| 44 |
+
void events_init(void);
|
| 45 |
+
bool event_post(Event ev);
|
| 46 |
+
bool event_poll(Event *out);
|
| 47 |
+
void events_drain(EventHandler handler, void *ctx);
|
| 48 |
+
|
| 49 |
+
#endif /* SOVEREIGN_EVENTS_H */
|
ide/native/core/strings.c
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Interned Strings
|
| 3 |
+
* All strings in the IDE are length-prefixed, immutable, arena-allocated.
|
| 4 |
+
* Interning deduplicates identical strings via FNV-1a hash table.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
#include "strings.h"
|
| 8 |
+
#include "arena.h"
|
| 9 |
+
#include <string.h>
|
| 10 |
+
|
| 11 |
+
#define INTERN_BUCKET_COUNT 4096
|
| 12 |
+
|
| 13 |
+
static uint64_t fnv1a(const char *data, size_t len) {
|
| 14 |
+
uint64_t h = 0xcbf29ce484222325ULL;
|
| 15 |
+
for (size_t i = 0; i < len; i++) {
|
| 16 |
+
h ^= (uint8_t)data[i];
|
| 17 |
+
h *= 0x100000001b3ULL;
|
| 18 |
+
}
|
| 19 |
+
return h;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
typedef struct InternEntry {
|
| 23 |
+
Str str;
|
| 24 |
+
struct InternEntry *next;
|
| 25 |
+
} InternEntry;
|
| 26 |
+
|
| 27 |
+
static struct {
|
| 28 |
+
Arena arena;
|
| 29 |
+
InternEntry *buckets[INTERN_BUCKET_COUNT];
|
| 30 |
+
size_t count;
|
| 31 |
+
} g_intern;
|
| 32 |
+
|
| 33 |
+
void strings_init(void) {
|
| 34 |
+
arena_init_default(&g_intern.arena);
|
| 35 |
+
memset(g_intern.buckets, 0, sizeof(g_intern.buckets));
|
| 36 |
+
g_intern.count = 0;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
void strings_shutdown(void) {
|
| 40 |
+
arena_destroy(&g_intern.arena);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
Str str_intern_len(const char *data, size_t len) {
|
| 44 |
+
uint64_t hash = fnv1a(data, len);
|
| 45 |
+
size_t bucket = hash % INTERN_BUCKET_COUNT;
|
| 46 |
+
|
| 47 |
+
for (InternEntry *e = g_intern.buckets[bucket]; e; e = e->next) {
|
| 48 |
+
if (e->str.len == len && memcmp(e->str.ptr, data, len) == 0) {
|
| 49 |
+
return e->str;
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
char *copy = (char *)arena_alloc(&g_intern.arena, len + 1);
|
| 54 |
+
memcpy(copy, data, len);
|
| 55 |
+
copy[len] = '\0';
|
| 56 |
+
|
| 57 |
+
InternEntry *entry = (InternEntry *)arena_alloc(&g_intern.arena, sizeof(InternEntry));
|
| 58 |
+
entry->str.ptr = copy;
|
| 59 |
+
entry->str.len = len;
|
| 60 |
+
entry->next = g_intern.buckets[bucket];
|
| 61 |
+
g_intern.buckets[bucket] = entry;
|
| 62 |
+
g_intern.count++;
|
| 63 |
+
|
| 64 |
+
return entry->str;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
Str str_intern(const char *cstr) {
|
| 68 |
+
return str_intern_len(cstr, strlen(cstr));
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
bool str_eq(Str a, Str b) {
|
| 72 |
+
return a.ptr == b.ptr;
|
| 73 |
+
}
|
ide/native/core/strings.h
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_STRINGS_H
|
| 2 |
+
#define SOVEREIGN_STRINGS_H
|
| 3 |
+
|
| 4 |
+
#include <stddef.h>
|
| 5 |
+
#include <stdbool.h>
|
| 6 |
+
|
| 7 |
+
typedef struct Str {
|
| 8 |
+
const char *ptr;
|
| 9 |
+
size_t len;
|
| 10 |
+
} Str;
|
| 11 |
+
|
| 12 |
+
void strings_init(void);
|
| 13 |
+
void strings_shutdown(void);
|
| 14 |
+
|
| 15 |
+
Str str_intern(const char *cstr);
|
| 16 |
+
Str str_intern_len(const char *data, size_t len);
|
| 17 |
+
bool str_eq(Str a, Str b);
|
| 18 |
+
|
| 19 |
+
#define STR_FMT "%.*s"
|
| 20 |
+
#define STR_ARG(s) (int)(s).len, (s).ptr
|
| 21 |
+
|
| 22 |
+
#endif /* SOVEREIGN_STRINGS_H */
|
ide/native/core/threading.h
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_THREADING_H
|
| 2 |
+
#define SOVEREIGN_THREADING_H
|
| 3 |
+
|
| 4 |
+
#include <windows.h>
|
| 5 |
+
#include <stdint.h>
|
| 6 |
+
|
| 7 |
+
typedef struct SovThread {
|
| 8 |
+
HANDLE handle;
|
| 9 |
+
DWORD id;
|
| 10 |
+
void (*entry)(void *);
|
| 11 |
+
void *arg;
|
| 12 |
+
} SovThread;
|
| 13 |
+
|
| 14 |
+
typedef struct SovMutex {
|
| 15 |
+
SRWLOCK lock;
|
| 16 |
+
} SovMutex;
|
| 17 |
+
|
| 18 |
+
typedef struct SovCondVar {
|
| 19 |
+
CONDITION_VARIABLE cv;
|
| 20 |
+
} SovCondVar;
|
| 21 |
+
|
| 22 |
+
static inline void sov_mutex_init(SovMutex *m) { InitializeSRWLock(&m->lock); }
|
| 23 |
+
static inline void sov_mutex_lock(SovMutex *m) { AcquireSRWLockExclusive(&m->lock); }
|
| 24 |
+
static inline void sov_mutex_unlock(SovMutex *m) { ReleaseSRWLockExclusive(&m->lock); }
|
| 25 |
+
|
| 26 |
+
static inline void sov_condvar_init(SovCondVar *cv) { InitializeConditionVariable(&cv->cv); }
|
| 27 |
+
static inline void sov_condvar_wait(SovCondVar *cv, SovMutex *m) { SleepConditionVariableSRW(&cv->cv, &m->lock, INFINITE, 0); }
|
| 28 |
+
static inline void sov_condvar_signal(SovCondVar *cv) { WakeConditionVariable(&cv->cv); }
|
| 29 |
+
static inline void sov_condvar_broadcast(SovCondVar *cv) { WakeAllConditionVariable(&cv->cv); }
|
| 30 |
+
|
| 31 |
+
#endif /* SOVEREIGN_THREADING_H */
|
ide/native/editor/buffer.c
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Piece Table Buffer
|
| 3 |
+
* Supports insert, delete, undo/redo, line indexing.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
#include "buffer.h"
|
| 7 |
+
#include "../core/arena.h"
|
| 8 |
+
#include <string.h>
|
| 9 |
+
#include <assert.h>
|
| 10 |
+
#include <stdlib.h>
|
| 11 |
+
|
| 12 |
+
#define ADD_BUFFER_INITIAL_CAP (256 * 1024)
|
| 13 |
+
#define UNDO_STACK_CAP 1024
|
| 14 |
+
|
| 15 |
+
typedef enum PieceSource { PIECE_ORIGINAL, PIECE_ADD } PieceSource;
|
| 16 |
+
|
| 17 |
+
typedef struct Piece {
|
| 18 |
+
PieceSource source;
|
| 19 |
+
size_t start;
|
| 20 |
+
size_t length;
|
| 21 |
+
struct Piece *prev;
|
| 22 |
+
struct Piece *next;
|
| 23 |
+
} Piece;
|
| 24 |
+
|
| 25 |
+
typedef enum UndoKind { UNDO_INSERT, UNDO_DELETE } UndoKind;
|
| 26 |
+
|
| 27 |
+
typedef struct UndoEntry {
|
| 28 |
+
UndoKind kind;
|
| 29 |
+
size_t offset;
|
| 30 |
+
size_t len;
|
| 31 |
+
char *deleted_text;
|
| 32 |
+
} UndoEntry;
|
| 33 |
+
|
| 34 |
+
struct Buffer {
|
| 35 |
+
Arena arena;
|
| 36 |
+
|
| 37 |
+
char *original;
|
| 38 |
+
size_t original_len;
|
| 39 |
+
|
| 40 |
+
char *add_buf;
|
| 41 |
+
size_t add_len;
|
| 42 |
+
size_t add_cap;
|
| 43 |
+
|
| 44 |
+
Piece sentinel;
|
| 45 |
+
size_t total_len;
|
| 46 |
+
size_t line_count;
|
| 47 |
+
|
| 48 |
+
UndoEntry undo_stack[UNDO_STACK_CAP];
|
| 49 |
+
int undo_top;
|
| 50 |
+
int redo_top;
|
| 51 |
+
bool dirty;
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
static const char *piece_data(const Buffer *b, const Piece *p) {
|
| 55 |
+
return (p->source == PIECE_ORIGINAL)
|
| 56 |
+
? b->original + p->start
|
| 57 |
+
: b->add_buf + p->start;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
static size_t count_newlines(const char *text, size_t len) {
|
| 61 |
+
size_t n = 0;
|
| 62 |
+
for (size_t i = 0; i < len; i++) {
|
| 63 |
+
if (text[i] == '\n') n++;
|
| 64 |
+
}
|
| 65 |
+
return n;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
static void append_to_add(Buffer *b, const char *text, size_t len) {
|
| 69 |
+
if (b->add_len + len > b->add_cap) {
|
| 70 |
+
size_t new_cap = b->add_cap * 2;
|
| 71 |
+
while (new_cap < b->add_len + len) new_cap *= 2;
|
| 72 |
+
char *new_buf = (char *)arena_alloc(&b->arena, new_cap);
|
| 73 |
+
memcpy(new_buf, b->add_buf, b->add_len);
|
| 74 |
+
b->add_buf = new_buf;
|
| 75 |
+
b->add_cap = new_cap;
|
| 76 |
+
}
|
| 77 |
+
memcpy(b->add_buf + b->add_len, text, len);
|
| 78 |
+
b->add_len += len;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
static Piece *alloc_piece(Buffer *b) {
|
| 82 |
+
return (Piece *)arena_alloc(&b->arena, sizeof(Piece));
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
static void find_piece_at(Buffer *b, size_t offset, Piece **out_piece, size_t *out_local) {
|
| 86 |
+
Piece *p = b->sentinel.next;
|
| 87 |
+
size_t pos = 0;
|
| 88 |
+
while (p != &b->sentinel && pos + p->length <= offset) {
|
| 89 |
+
pos += p->length;
|
| 90 |
+
p = p->next;
|
| 91 |
+
}
|
| 92 |
+
*out_piece = p;
|
| 93 |
+
*out_local = offset - pos;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
Buffer *buffer_create(const char *text, size_t len) {
|
| 97 |
+
Arena a;
|
| 98 |
+
arena_init(&a, 8 * 1024 * 1024);
|
| 99 |
+
|
| 100 |
+
Buffer *b = (Buffer *)arena_alloc_zero(&a, sizeof(Buffer));
|
| 101 |
+
b->arena = a;
|
| 102 |
+
|
| 103 |
+
b->original = (char *)arena_alloc(&b->arena, len + 1);
|
| 104 |
+
memcpy(b->original, text, len);
|
| 105 |
+
b->original[len] = '\0';
|
| 106 |
+
b->original_len = len;
|
| 107 |
+
|
| 108 |
+
b->add_cap = ADD_BUFFER_INITIAL_CAP;
|
| 109 |
+
b->add_buf = (char *)arena_alloc(&b->arena, b->add_cap);
|
| 110 |
+
b->add_len = 0;
|
| 111 |
+
|
| 112 |
+
if (len > 0) {
|
| 113 |
+
Piece *first = alloc_piece(b);
|
| 114 |
+
first->source = PIECE_ORIGINAL;
|
| 115 |
+
first->start = 0;
|
| 116 |
+
first->length = len;
|
| 117 |
+
b->sentinel.next = first;
|
| 118 |
+
b->sentinel.prev = first;
|
| 119 |
+
first->prev = &b->sentinel;
|
| 120 |
+
first->next = &b->sentinel;
|
| 121 |
+
} else {
|
| 122 |
+
b->sentinel.next = &b->sentinel;
|
| 123 |
+
b->sentinel.prev = &b->sentinel;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
b->total_len = len;
|
| 127 |
+
b->line_count = 1 + count_newlines(text, len);
|
| 128 |
+
b->undo_top = 0;
|
| 129 |
+
b->redo_top = 0;
|
| 130 |
+
b->dirty = false;
|
| 131 |
+
|
| 132 |
+
return b;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
void buffer_destroy(Buffer *b) {
|
| 136 |
+
for (int i = 0; i < b->undo_top; i++) {
|
| 137 |
+
free(b->undo_stack[i].deleted_text);
|
| 138 |
+
}
|
| 139 |
+
Arena a = b->arena;
|
| 140 |
+
arena_destroy(&a);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
size_t buffer_length(const Buffer *b) {
|
| 144 |
+
return b->total_len;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
size_t buffer_line_count(const Buffer *b) {
|
| 148 |
+
return b->line_count;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
bool buffer_is_dirty(const Buffer *b) {
|
| 152 |
+
return b->dirty;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
void buffer_mark_clean(Buffer *b) {
|
| 156 |
+
b->dirty = false;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
bool buffer_can_undo(const Buffer *b) {
|
| 160 |
+
return b->undo_top > 0;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
bool buffer_can_redo(const Buffer *b) {
|
| 164 |
+
return b->redo_top > 0;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
static void push_undo(Buffer *b, UndoKind kind, size_t offset, size_t len, const char *text) {
|
| 168 |
+
if (b->undo_top >= UNDO_STACK_CAP) {
|
| 169 |
+
free(b->undo_stack[0].deleted_text);
|
| 170 |
+
memmove(&b->undo_stack[0], &b->undo_stack[1], (UNDO_STACK_CAP - 1) * sizeof(UndoEntry));
|
| 171 |
+
b->undo_top--;
|
| 172 |
+
}
|
| 173 |
+
/* discard redo stack on new edit */
|
| 174 |
+
for (int i = b->undo_top; i < b->undo_top + b->redo_top; i++) {
|
| 175 |
+
if (i < UNDO_STACK_CAP) free(b->undo_stack[i].deleted_text);
|
| 176 |
+
}
|
| 177 |
+
b->redo_top = 0;
|
| 178 |
+
|
| 179 |
+
UndoEntry *e = &b->undo_stack[b->undo_top++];
|
| 180 |
+
e->kind = kind;
|
| 181 |
+
e->offset = offset;
|
| 182 |
+
e->len = len;
|
| 183 |
+
/* always copy the text — inserts need it for redo, deletes need it for undo */
|
| 184 |
+
if (text && len > 0) {
|
| 185 |
+
e->deleted_text = (char *)malloc(len);
|
| 186 |
+
memcpy(e->deleted_text, text, len);
|
| 187 |
+
} else {
|
| 188 |
+
e->deleted_text = NULL;
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
void buffer_insert(Buffer *b, size_t offset, const char *text, size_t len) {
|
| 193 |
+
if (len == 0) return;
|
| 194 |
+
if (offset > b->total_len) offset = b->total_len;
|
| 195 |
+
|
| 196 |
+
size_t add_start = b->add_len;
|
| 197 |
+
append_to_add(b, text, len);
|
| 198 |
+
|
| 199 |
+
Piece *p;
|
| 200 |
+
size_t local;
|
| 201 |
+
find_piece_at(b, offset, &p, &local);
|
| 202 |
+
|
| 203 |
+
Piece *new_piece = alloc_piece(b);
|
| 204 |
+
new_piece->source = PIECE_ADD;
|
| 205 |
+
new_piece->start = add_start;
|
| 206 |
+
new_piece->length = len;
|
| 207 |
+
|
| 208 |
+
if (p == &b->sentinel || local == 0) {
|
| 209 |
+
new_piece->prev = p->prev;
|
| 210 |
+
new_piece->next = p;
|
| 211 |
+
p->prev->next = new_piece;
|
| 212 |
+
p->prev = new_piece;
|
| 213 |
+
} else if (local == p->length) {
|
| 214 |
+
new_piece->prev = p;
|
| 215 |
+
new_piece->next = p->next;
|
| 216 |
+
p->next->prev = new_piece;
|
| 217 |
+
p->next = new_piece;
|
| 218 |
+
} else {
|
| 219 |
+
Piece *right = alloc_piece(b);
|
| 220 |
+
right->source = p->source;
|
| 221 |
+
right->start = p->start + local;
|
| 222 |
+
right->length = p->length - local;
|
| 223 |
+
p->length = local;
|
| 224 |
+
|
| 225 |
+
new_piece->prev = p;
|
| 226 |
+
new_piece->next = right;
|
| 227 |
+
right->prev = new_piece;
|
| 228 |
+
right->next = p->next;
|
| 229 |
+
p->next->prev = right;
|
| 230 |
+
p->next = new_piece;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
b->total_len += len;
|
| 234 |
+
b->line_count += count_newlines(text, len);
|
| 235 |
+
b->dirty = true;
|
| 236 |
+
|
| 237 |
+
push_undo(b, UNDO_INSERT, offset, len, text);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
void buffer_delete(Buffer *b, size_t offset, size_t len) {
|
| 241 |
+
if (len == 0 || offset >= b->total_len) return;
|
| 242 |
+
if (offset + len > b->total_len) len = b->total_len - offset;
|
| 243 |
+
|
| 244 |
+
/* save deleted text for undo */
|
| 245 |
+
char *deleted = (char *)malloc(len);
|
| 246 |
+
buffer_read(b, offset, deleted, len);
|
| 247 |
+
b->line_count -= count_newlines(deleted, len);
|
| 248 |
+
|
| 249 |
+
size_t remaining = len;
|
| 250 |
+
Piece *p;
|
| 251 |
+
size_t local;
|
| 252 |
+
find_piece_at(b, offset, &p, &local);
|
| 253 |
+
|
| 254 |
+
while (remaining > 0 && p != &b->sentinel) {
|
| 255 |
+
if (local == 0 && remaining >= p->length) {
|
| 256 |
+
/* remove entire piece */
|
| 257 |
+
Piece *next = p->next;
|
| 258 |
+
p->prev->next = p->next;
|
| 259 |
+
p->next->prev = p->prev;
|
| 260 |
+
remaining -= p->length;
|
| 261 |
+
p = next;
|
| 262 |
+
local = 0;
|
| 263 |
+
} else if (local == 0) {
|
| 264 |
+
/* trim from start */
|
| 265 |
+
p->start += remaining;
|
| 266 |
+
p->length -= remaining;
|
| 267 |
+
remaining = 0;
|
| 268 |
+
} else if (local + remaining >= p->length) {
|
| 269 |
+
/* trim from end */
|
| 270 |
+
size_t removed = p->length - local;
|
| 271 |
+
p->length = local;
|
| 272 |
+
remaining -= removed;
|
| 273 |
+
p = p->next;
|
| 274 |
+
local = 0;
|
| 275 |
+
} else {
|
| 276 |
+
/* split: remove middle */
|
| 277 |
+
Piece *right = alloc_piece(b);
|
| 278 |
+
right->source = p->source;
|
| 279 |
+
right->start = p->start + local + remaining;
|
| 280 |
+
right->length = p->length - local - remaining;
|
| 281 |
+
p->length = local;
|
| 282 |
+
|
| 283 |
+
right->next = p->next;
|
| 284 |
+
right->prev = p;
|
| 285 |
+
p->next->prev = right;
|
| 286 |
+
p->next = right;
|
| 287 |
+
remaining = 0;
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
b->total_len -= len;
|
| 292 |
+
b->dirty = true;
|
| 293 |
+
|
| 294 |
+
push_undo(b, UNDO_DELETE, offset, len, deleted);
|
| 295 |
+
free(deleted);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
static void raw_insert(Buffer *b, size_t offset, const char *text, size_t len) {
|
| 299 |
+
size_t add_start = b->add_len;
|
| 300 |
+
append_to_add(b, text, len);
|
| 301 |
+
|
| 302 |
+
Piece *p;
|
| 303 |
+
size_t local;
|
| 304 |
+
find_piece_at(b, offset, &p, &local);
|
| 305 |
+
|
| 306 |
+
Piece *new_piece = alloc_piece(b);
|
| 307 |
+
new_piece->source = PIECE_ADD;
|
| 308 |
+
new_piece->start = add_start;
|
| 309 |
+
new_piece->length = len;
|
| 310 |
+
|
| 311 |
+
if (p == &b->sentinel || local == 0) {
|
| 312 |
+
new_piece->prev = p->prev;
|
| 313 |
+
new_piece->next = p;
|
| 314 |
+
p->prev->next = new_piece;
|
| 315 |
+
p->prev = new_piece;
|
| 316 |
+
} else if (local == p->length) {
|
| 317 |
+
new_piece->prev = p;
|
| 318 |
+
new_piece->next = p->next;
|
| 319 |
+
p->next->prev = new_piece;
|
| 320 |
+
p->next = new_piece;
|
| 321 |
+
} else {
|
| 322 |
+
Piece *right = alloc_piece(b);
|
| 323 |
+
right->source = p->source;
|
| 324 |
+
right->start = p->start + local;
|
| 325 |
+
right->length = p->length - local;
|
| 326 |
+
p->length = local;
|
| 327 |
+
new_piece->prev = p;
|
| 328 |
+
new_piece->next = right;
|
| 329 |
+
right->prev = new_piece;
|
| 330 |
+
right->next = p->next;
|
| 331 |
+
p->next->prev = right;
|
| 332 |
+
p->next = new_piece;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
b->total_len += len;
|
| 336 |
+
b->line_count += count_newlines(text, len);
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
static void raw_delete(Buffer *b, size_t offset, size_t len) {
|
| 340 |
+
if (len == 0) return;
|
| 341 |
+
char *tmp = (char *)malloc(len);
|
| 342 |
+
buffer_read(b, offset, tmp, len);
|
| 343 |
+
b->line_count -= count_newlines(tmp, len);
|
| 344 |
+
free(tmp);
|
| 345 |
+
|
| 346 |
+
size_t remaining = len;
|
| 347 |
+
Piece *p;
|
| 348 |
+
size_t local;
|
| 349 |
+
find_piece_at(b, offset, &p, &local);
|
| 350 |
+
|
| 351 |
+
while (remaining > 0 && p != &b->sentinel) {
|
| 352 |
+
if (local == 0 && remaining >= p->length) {
|
| 353 |
+
Piece *next = p->next;
|
| 354 |
+
p->prev->next = p->next;
|
| 355 |
+
p->next->prev = p->prev;
|
| 356 |
+
remaining -= p->length;
|
| 357 |
+
p = next;
|
| 358 |
+
local = 0;
|
| 359 |
+
} else if (local == 0) {
|
| 360 |
+
p->start += remaining;
|
| 361 |
+
p->length -= remaining;
|
| 362 |
+
remaining = 0;
|
| 363 |
+
} else if (local + remaining >= p->length) {
|
| 364 |
+
size_t removed = p->length - local;
|
| 365 |
+
p->length = local;
|
| 366 |
+
remaining -= removed;
|
| 367 |
+
p = p->next;
|
| 368 |
+
local = 0;
|
| 369 |
+
} else {
|
| 370 |
+
Piece *right = alloc_piece(b);
|
| 371 |
+
right->source = p->source;
|
| 372 |
+
right->start = p->start + local + remaining;
|
| 373 |
+
right->length = p->length - local - remaining;
|
| 374 |
+
p->length = local;
|
| 375 |
+
right->next = p->next;
|
| 376 |
+
right->prev = p;
|
| 377 |
+
p->next->prev = right;
|
| 378 |
+
p->next = right;
|
| 379 |
+
remaining = 0;
|
| 380 |
+
}
|
| 381 |
+
}
|
| 382 |
+
b->total_len -= len;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
void buffer_undo(Buffer *b) {
|
| 386 |
+
if (b->undo_top == 0) return;
|
| 387 |
+
b->undo_top--;
|
| 388 |
+
UndoEntry *e = &b->undo_stack[b->undo_top];
|
| 389 |
+
b->redo_top++;
|
| 390 |
+
|
| 391 |
+
if (e->kind == UNDO_INSERT) {
|
| 392 |
+
raw_delete(b, e->offset, e->len);
|
| 393 |
+
} else {
|
| 394 |
+
raw_insert(b, e->offset, e->deleted_text, e->len);
|
| 395 |
+
}
|
| 396 |
+
b->dirty = true;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
void buffer_redo(Buffer *b) {
|
| 400 |
+
if (b->redo_top == 0) return;
|
| 401 |
+
UndoEntry *e = &b->undo_stack[b->undo_top];
|
| 402 |
+
b->undo_top++;
|
| 403 |
+
b->redo_top--;
|
| 404 |
+
|
| 405 |
+
if (e->kind == UNDO_INSERT) {
|
| 406 |
+
raw_insert(b, e->offset, e->deleted_text, e->len);
|
| 407 |
+
} else {
|
| 408 |
+
raw_delete(b, e->offset, e->len);
|
| 409 |
+
}
|
| 410 |
+
b->dirty = true;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
size_t buffer_read(const Buffer *b, size_t offset, char *out, size_t max_len) {
|
| 414 |
+
size_t copied = 0;
|
| 415 |
+
size_t pos = 0;
|
| 416 |
+
const Piece *p = b->sentinel.next;
|
| 417 |
+
|
| 418 |
+
while (p != &b->sentinel && pos + p->length <= offset) {
|
| 419 |
+
pos += p->length;
|
| 420 |
+
p = p->next;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
size_t skip = offset - pos;
|
| 424 |
+
while (p != &b->sentinel && copied < max_len) {
|
| 425 |
+
const char *data = piece_data(b, p) + skip;
|
| 426 |
+
size_t avail = p->length - skip;
|
| 427 |
+
size_t to_copy = (avail < max_len - copied) ? avail : (max_len - copied);
|
| 428 |
+
memcpy(out + copied, data, to_copy);
|
| 429 |
+
copied += to_copy;
|
| 430 |
+
skip = 0;
|
| 431 |
+
p = p->next;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
return copied;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
size_t buffer_line_start(const Buffer *b, size_t line) {
|
| 438 |
+
if (line == 0) return 0;
|
| 439 |
+
size_t current_line = 0;
|
| 440 |
+
size_t pos = 0;
|
| 441 |
+
const Piece *p = b->sentinel.next;
|
| 442 |
+
|
| 443 |
+
while (p != &b->sentinel) {
|
| 444 |
+
const char *data = piece_data(b, p);
|
| 445 |
+
for (size_t i = 0; i < p->length; i++) {
|
| 446 |
+
if (data[i] == '\n') {
|
| 447 |
+
current_line++;
|
| 448 |
+
if (current_line == line) return pos + i + 1;
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
pos += p->length;
|
| 452 |
+
p = p->next;
|
| 453 |
+
}
|
| 454 |
+
return b->total_len;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
size_t buffer_line_length(const Buffer *b, size_t line) {
|
| 458 |
+
size_t start = buffer_line_start(b, line);
|
| 459 |
+
if (start >= b->total_len) return 0;
|
| 460 |
+
|
| 461 |
+
size_t pos = start;
|
| 462 |
+
const Piece *p = b->sentinel.next;
|
| 463 |
+
size_t piece_pos = 0;
|
| 464 |
+
|
| 465 |
+
while (p != &b->sentinel && piece_pos + p->length <= start) {
|
| 466 |
+
piece_pos += p->length;
|
| 467 |
+
p = p->next;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
size_t skip = start - piece_pos;
|
| 471 |
+
while (p != &b->sentinel) {
|
| 472 |
+
const char *data = piece_data(b, p) + skip;
|
| 473 |
+
size_t avail = p->length - skip;
|
| 474 |
+
for (size_t i = 0; i < avail; i++) {
|
| 475 |
+
if (data[i] == '\n') return pos - start;
|
| 476 |
+
pos++;
|
| 477 |
+
}
|
| 478 |
+
skip = 0;
|
| 479 |
+
p = p->next;
|
| 480 |
+
}
|
| 481 |
+
return pos - start;
|
| 482 |
+
}
|
ide/native/editor/buffer.h
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_BUFFER_H
|
| 2 |
+
#define SOVEREIGN_BUFFER_H
|
| 3 |
+
|
| 4 |
+
#include <stddef.h>
|
| 5 |
+
#include <stdbool.h>
|
| 6 |
+
|
| 7 |
+
typedef struct Buffer Buffer;
|
| 8 |
+
|
| 9 |
+
Buffer *buffer_create(const char *text, size_t len);
|
| 10 |
+
void buffer_destroy(Buffer *b);
|
| 11 |
+
size_t buffer_length(const Buffer *b);
|
| 12 |
+
size_t buffer_line_count(const Buffer *b);
|
| 13 |
+
void buffer_insert(Buffer *b, size_t offset, const char *text, size_t len);
|
| 14 |
+
void buffer_delete(Buffer *b, size_t offset, size_t len);
|
| 15 |
+
size_t buffer_read(const Buffer *b, size_t offset, char *out, size_t max_len);
|
| 16 |
+
|
| 17 |
+
void buffer_undo(Buffer *b);
|
| 18 |
+
void buffer_redo(Buffer *b);
|
| 19 |
+
bool buffer_can_undo(const Buffer *b);
|
| 20 |
+
bool buffer_can_redo(const Buffer *b);
|
| 21 |
+
bool buffer_is_dirty(const Buffer *b);
|
| 22 |
+
void buffer_mark_clean(Buffer *b);
|
| 23 |
+
|
| 24 |
+
size_t buffer_line_start(const Buffer *b, size_t line);
|
| 25 |
+
size_t buffer_line_length(const Buffer *b, size_t line);
|
| 26 |
+
|
| 27 |
+
#endif /* SOVEREIGN_BUFFER_H */
|
ide/native/editor/document.c
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Document
|
| 3 |
+
* File open/save with UTF-8 + CRLF normalization.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
#include "document.h"
|
| 7 |
+
#include <windows.h>
|
| 8 |
+
#include <stdlib.h>
|
| 9 |
+
#include <string.h>
|
| 10 |
+
|
| 11 |
+
void document_new(Document *doc) {
|
| 12 |
+
doc->buffer = buffer_create("", 0);
|
| 13 |
+
doc->path[0] = L'\0';
|
| 14 |
+
doc->has_path = false;
|
| 15 |
+
doc->line_ending = LE_LF;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
static LineEnding detect_line_ending(const char *text, size_t len) {
|
| 19 |
+
for (size_t i = 0; i + 1 < len; i++) {
|
| 20 |
+
if (text[i] == '\r' && text[i+1] == '\n') return LE_CRLF;
|
| 21 |
+
}
|
| 22 |
+
for (size_t i = 0; i < len; i++) {
|
| 23 |
+
if (text[i] == '\r') return LE_CR;
|
| 24 |
+
}
|
| 25 |
+
return LE_LF;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/* Normalize CRLF/CR to LF for internal storage */
|
| 29 |
+
static char *normalize_to_lf(const char *src, size_t src_len, size_t *out_len) {
|
| 30 |
+
char *dst = (char *)malloc(src_len + 1);
|
| 31 |
+
size_t j = 0;
|
| 32 |
+
for (size_t i = 0; i < src_len; i++) {
|
| 33 |
+
if (src[i] == '\r') {
|
| 34 |
+
dst[j++] = '\n';
|
| 35 |
+
if (i + 1 < src_len && src[i+1] == '\n') i++;
|
| 36 |
+
} else {
|
| 37 |
+
dst[j++] = src[i];
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
dst[j] = '\0';
|
| 41 |
+
*out_len = j;
|
| 42 |
+
return dst;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
SovResult document_open(Document *doc, const wchar_t *path) {
|
| 46 |
+
HANDLE h = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL,
|
| 47 |
+
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
| 48 |
+
if (h == INVALID_HANDLE_VALUE) return SOV_ERR_IO;
|
| 49 |
+
|
| 50 |
+
LARGE_INTEGER file_size;
|
| 51 |
+
GetFileSizeEx(h, &file_size);
|
| 52 |
+
if (file_size.QuadPart > 256 * 1024 * 1024) {
|
| 53 |
+
CloseHandle(h);
|
| 54 |
+
return SOV_ERR_IO;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
size_t sz = (size_t)file_size.QuadPart;
|
| 58 |
+
char *raw = (char *)malloc(sz + 1);
|
| 59 |
+
DWORD nread = 0;
|
| 60 |
+
ReadFile(h, raw, (DWORD)sz, &nread, NULL);
|
| 61 |
+
CloseHandle(h);
|
| 62 |
+
raw[nread] = '\0';
|
| 63 |
+
|
| 64 |
+
/* Skip UTF-8 BOM if present */
|
| 65 |
+
const char *text = raw;
|
| 66 |
+
size_t text_len = nread;
|
| 67 |
+
if (text_len >= 3 &&
|
| 68 |
+
(unsigned char)text[0] == 0xEF &&
|
| 69 |
+
(unsigned char)text[1] == 0xBB &&
|
| 70 |
+
(unsigned char)text[2] == 0xBF) {
|
| 71 |
+
text += 3;
|
| 72 |
+
text_len -= 3;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
doc->line_ending = detect_line_ending(text, text_len);
|
| 76 |
+
|
| 77 |
+
size_t norm_len;
|
| 78 |
+
char *norm = normalize_to_lf(text, text_len, &norm_len);
|
| 79 |
+
free(raw);
|
| 80 |
+
|
| 81 |
+
doc->buffer = buffer_create(norm, norm_len);
|
| 82 |
+
free(norm);
|
| 83 |
+
|
| 84 |
+
wcscpy_s(doc->path, MAX_PATH, path);
|
| 85 |
+
doc->has_path = true;
|
| 86 |
+
return SOV_OK;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
SovResult document_save(Document *doc) {
|
| 90 |
+
if (!doc->has_path) return SOV_ERR_IO;
|
| 91 |
+
SovResult r = document_save_as(doc, doc->path);
|
| 92 |
+
return r;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
SovResult document_save_as(Document *doc, const wchar_t *path) {
|
| 96 |
+
size_t len = buffer_length(doc->buffer);
|
| 97 |
+
char *content = (char *)malloc(len + 1);
|
| 98 |
+
buffer_read(doc->buffer, 0, content, len);
|
| 99 |
+
content[len] = '\0';
|
| 100 |
+
|
| 101 |
+
/* Re-apply original line endings */
|
| 102 |
+
char *output = content;
|
| 103 |
+
size_t out_len = len;
|
| 104 |
+
char *crlf_buf = NULL;
|
| 105 |
+
|
| 106 |
+
if (doc->line_ending == LE_CRLF) {
|
| 107 |
+
/* count newlines to allocate */
|
| 108 |
+
size_t nl = 0;
|
| 109 |
+
for (size_t i = 0; i < len; i++) if (content[i] == '\n') nl++;
|
| 110 |
+
crlf_buf = (char *)malloc(len + nl + 1);
|
| 111 |
+
size_t j = 0;
|
| 112 |
+
for (size_t i = 0; i < len; i++) {
|
| 113 |
+
if (content[i] == '\n') crlf_buf[j++] = '\r';
|
| 114 |
+
crlf_buf[j++] = content[i];
|
| 115 |
+
}
|
| 116 |
+
crlf_buf[j] = '\0';
|
| 117 |
+
output = crlf_buf;
|
| 118 |
+
out_len = j;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
HANDLE h = CreateFileW(path, GENERIC_WRITE, 0, NULL,
|
| 122 |
+
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
|
| 123 |
+
if (h == INVALID_HANDLE_VALUE) {
|
| 124 |
+
free(content);
|
| 125 |
+
free(crlf_buf);
|
| 126 |
+
return SOV_ERR_IO;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
DWORD written;
|
| 130 |
+
WriteFile(h, output, (DWORD)out_len, &written, NULL);
|
| 131 |
+
CloseHandle(h);
|
| 132 |
+
|
| 133 |
+
free(content);
|
| 134 |
+
free(crlf_buf);
|
| 135 |
+
|
| 136 |
+
wcscpy_s(doc->path, MAX_PATH, path);
|
| 137 |
+
doc->has_path = true;
|
| 138 |
+
buffer_mark_clean(doc->buffer);
|
| 139 |
+
return SOV_OK;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
bool document_is_dirty(const Document *doc) {
|
| 143 |
+
return buffer_is_dirty(doc->buffer);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
void document_destroy(Document *doc) {
|
| 147 |
+
if (doc->buffer) {
|
| 148 |
+
buffer_destroy(doc->buffer);
|
| 149 |
+
doc->buffer = NULL;
|
| 150 |
+
}
|
| 151 |
+
}
|
ide/native/editor/document.h
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_DOCUMENT_H
|
| 2 |
+
#define SOVEREIGN_DOCUMENT_H
|
| 3 |
+
|
| 4 |
+
#include <windows.h>
|
| 5 |
+
#include "buffer.h"
|
| 6 |
+
#include "../core/errors.h"
|
| 7 |
+
#include <stdbool.h>
|
| 8 |
+
#include <stddef.h>
|
| 9 |
+
|
| 10 |
+
typedef enum LineEnding { LE_LF, LE_CRLF, LE_CR } LineEnding;
|
| 11 |
+
|
| 12 |
+
typedef struct Document {
|
| 13 |
+
Buffer *buffer;
|
| 14 |
+
wchar_t path[MAX_PATH];
|
| 15 |
+
LineEnding line_ending;
|
| 16 |
+
bool has_path;
|
| 17 |
+
} Document;
|
| 18 |
+
|
| 19 |
+
SovResult document_open(Document *doc, const wchar_t *path);
|
| 20 |
+
SovResult document_save(Document *doc);
|
| 21 |
+
SovResult document_save_as(Document *doc, const wchar_t *path);
|
| 22 |
+
void document_new(Document *doc);
|
| 23 |
+
void document_destroy(Document *doc);
|
| 24 |
+
bool document_is_dirty(const Document *doc);
|
| 25 |
+
|
| 26 |
+
#endif
|
ide/native/editor/editor_view.cpp
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Editor View
|
| 3 |
+
* DirectWrite text rendering, visible caret, selection, keyboard navigation.
|
| 4 |
+
* C++ for COM vtable calls; all public functions are extern "C".
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
#include <windows.h>
|
| 8 |
+
#include <d2d1.h>
|
| 9 |
+
#include <dwrite.h>
|
| 10 |
+
#include <stdint.h>
|
| 11 |
+
#include <stdlib.h>
|
| 12 |
+
#include <string.h>
|
| 13 |
+
#include <wchar.h>
|
| 14 |
+
|
| 15 |
+
extern "C" {
|
| 16 |
+
#include "editor_view.h"
|
| 17 |
+
#include "document.h"
|
| 18 |
+
#include "buffer.h"
|
| 19 |
+
#include "../lsp/client.h"
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
#define FONT_SIZE 14.0f
|
| 23 |
+
#define LINE_HEIGHT 20.0f
|
| 24 |
+
#define GUTTER_WIDTH 48.0f
|
| 25 |
+
#define PADDING_LEFT 4.0f
|
| 26 |
+
#define CARET_WIDTH 2.0f
|
| 27 |
+
#define SCROLL_LINES 3
|
| 28 |
+
|
| 29 |
+
/* Colors (dark theme) */
|
| 30 |
+
static const D2D1_COLOR_F C_BG = { 0.122f, 0.122f, 0.141f, 1.0f };
|
| 31 |
+
static const D2D1_COLOR_F C_GUTTER = { 0.098f, 0.098f, 0.118f, 1.0f };
|
| 32 |
+
static const D2D1_COLOR_F C_GUTTER_FG = { 0.420f, 0.420f, 0.520f, 1.0f };
|
| 33 |
+
static const D2D1_COLOR_F C_TEXT = { 0.859f, 0.859f, 0.859f, 1.0f };
|
| 34 |
+
static const D2D1_COLOR_F C_CARET = { 1.000f, 1.000f, 1.000f, 1.0f };
|
| 35 |
+
static const D2D1_COLOR_F C_SEL = { 0.173f, 0.369f, 0.529f, 0.6f };
|
| 36 |
+
static const D2D1_COLOR_F C_ACTIVE_LINE= { 1.000f, 1.000f, 1.000f, 0.04f };
|
| 37 |
+
static const D2D1_COLOR_F C_SQUIGGLE_E = { 1.000f, 0.260f, 0.260f, 1.0f }; /* error */
|
| 38 |
+
static const D2D1_COLOR_F C_SQUIGGLE_W = { 1.000f, 0.780f, 0.200f, 1.0f }; /* warning */
|
| 39 |
+
|
| 40 |
+
struct EditorView {
|
| 41 |
+
HWND hwnd;
|
| 42 |
+
Document *doc;
|
| 43 |
+
|
| 44 |
+
ID2D1Factory *d2d_factory;
|
| 45 |
+
ID2D1HwndRenderTarget *rt;
|
| 46 |
+
IDWriteFactory *dw_factory;
|
| 47 |
+
IDWriteTextFormat *text_format;
|
| 48 |
+
IDWriteTextFormat *gutter_format;
|
| 49 |
+
|
| 50 |
+
ID2D1SolidColorBrush *br_text;
|
| 51 |
+
ID2D1SolidColorBrush *br_gutter_fg;
|
| 52 |
+
ID2D1SolidColorBrush *br_caret;
|
| 53 |
+
ID2D1SolidColorBrush *br_sel;
|
| 54 |
+
ID2D1SolidColorBrush *br_active_line;
|
| 55 |
+
ID2D1SolidColorBrush *br_gutter_bg;
|
| 56 |
+
ID2D1SolidColorBrush *br_squiggle_e;
|
| 57 |
+
ID2D1SolidColorBrush *br_squiggle_w;
|
| 58 |
+
|
| 59 |
+
uint32_t width;
|
| 60 |
+
uint32_t height;
|
| 61 |
+
float scroll_y; /* in lines */
|
| 62 |
+
|
| 63 |
+
size_t caret; /* byte offset in buffer */
|
| 64 |
+
size_t sel_anchor; /* selection start; equals caret = no selection */
|
| 65 |
+
bool has_sel;
|
| 66 |
+
|
| 67 |
+
bool caret_visible; /* blink state */
|
| 68 |
+
DWORD caret_tick;
|
| 69 |
+
|
| 70 |
+
/* LSP diagnostics for current file */
|
| 71 |
+
LspDiagList diags;
|
| 72 |
+
bool has_diags;
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
static HRESULT create_brushes(EditorView *ev) {
|
| 76 |
+
HRESULT hr;
|
| 77 |
+
hr = ev->rt->CreateSolidColorBrush(C_TEXT, &ev->br_text); if (FAILED(hr)) return hr;
|
| 78 |
+
hr = ev->rt->CreateSolidColorBrush(C_GUTTER_FG, &ev->br_gutter_fg); if (FAILED(hr)) return hr;
|
| 79 |
+
hr = ev->rt->CreateSolidColorBrush(C_CARET, &ev->br_caret); if (FAILED(hr)) return hr;
|
| 80 |
+
hr = ev->rt->CreateSolidColorBrush(C_SEL, &ev->br_sel); if (FAILED(hr)) return hr;
|
| 81 |
+
hr = ev->rt->CreateSolidColorBrush(C_ACTIVE_LINE, &ev->br_active_line); if (FAILED(hr)) return hr;
|
| 82 |
+
hr = ev->rt->CreateSolidColorBrush(C_GUTTER, &ev->br_gutter_bg); if (FAILED(hr)) return hr;
|
| 83 |
+
hr = ev->rt->CreateSolidColorBrush(C_SQUIGGLE_E, &ev->br_squiggle_e); if (FAILED(hr)) return hr;
|
| 84 |
+
hr = ev->rt->CreateSolidColorBrush(C_SQUIGGLE_W, &ev->br_squiggle_w); if (FAILED(hr)) return hr;
|
| 85 |
+
return S_OK;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
static void release_brushes(EditorView *ev) {
|
| 89 |
+
auto safe = [](IUnknown *p) { if (p) p->Release(); };
|
| 90 |
+
safe(ev->br_text); ev->br_text = nullptr;
|
| 91 |
+
safe(ev->br_gutter_fg); ev->br_gutter_fg = nullptr;
|
| 92 |
+
safe(ev->br_caret); ev->br_caret = nullptr;
|
| 93 |
+
safe(ev->br_sel); ev->br_sel = nullptr;
|
| 94 |
+
safe(ev->br_active_line); ev->br_active_line = nullptr;
|
| 95 |
+
safe(ev->br_gutter_bg); ev->br_gutter_bg = nullptr;
|
| 96 |
+
safe(ev->br_squiggle_e); ev->br_squiggle_e = nullptr;
|
| 97 |
+
safe(ev->br_squiggle_w); ev->br_squiggle_w = nullptr;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
extern "C" SovResult editor_view_create(EditorView **out, HWND hwnd) {
|
| 101 |
+
EditorView *ev = (EditorView *)calloc(1, sizeof(EditorView));
|
| 102 |
+
if (!ev) return SOV_ERR_ALLOC;
|
| 103 |
+
|
| 104 |
+
ev->hwnd = hwnd;
|
| 105 |
+
ev->caret_visible = true;
|
| 106 |
+
ev->caret_tick = GetTickCount();
|
| 107 |
+
|
| 108 |
+
HRESULT hr;
|
| 109 |
+
hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &ev->d2d_factory);
|
| 110 |
+
if (FAILED(hr)) { free(ev); return SOV_ERR_ALLOC; }
|
| 111 |
+
|
| 112 |
+
hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
|
| 113 |
+
__uuidof(IDWriteFactory),
|
| 114 |
+
reinterpret_cast<IUnknown **>(&ev->dw_factory));
|
| 115 |
+
if (FAILED(hr)) { free(ev); return SOV_ERR_ALLOC; }
|
| 116 |
+
|
| 117 |
+
hr = ev->dw_factory->CreateTextFormat(
|
| 118 |
+
L"Consolas", nullptr,
|
| 119 |
+
DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL,
|
| 120 |
+
FONT_SIZE, L"en-us", &ev->text_format);
|
| 121 |
+
if (FAILED(hr)) { free(ev); return SOV_ERR_ALLOC; }
|
| 122 |
+
|
| 123 |
+
hr = ev->dw_factory->CreateTextFormat(
|
| 124 |
+
L"Consolas", nullptr,
|
| 125 |
+
DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL,
|
| 126 |
+
11.0f, L"en-us", &ev->gutter_format);
|
| 127 |
+
if (FAILED(hr)) { free(ev); return SOV_ERR_ALLOC; }
|
| 128 |
+
ev->gutter_format->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_TRAILING);
|
| 129 |
+
|
| 130 |
+
RECT rc;
|
| 131 |
+
GetClientRect(hwnd, &rc);
|
| 132 |
+
ev->width = (uint32_t)(rc.right - rc.left);
|
| 133 |
+
ev->height = (uint32_t)(rc.bottom - rc.top);
|
| 134 |
+
|
| 135 |
+
D2D1_RENDER_TARGET_PROPERTIES rtp = D2D1::RenderTargetProperties();
|
| 136 |
+
D2D1_HWND_RENDER_TARGET_PROPERTIES hwp = D2D1::HwndRenderTargetProperties(
|
| 137 |
+
hwnd, D2D1::SizeU(ev->width, ev->height));
|
| 138 |
+
hr = ev->d2d_factory->CreateHwndRenderTarget(rtp, hwp, &ev->rt);
|
| 139 |
+
if (FAILED(hr)) { free(ev); return SOV_ERR_ALLOC; }
|
| 140 |
+
|
| 141 |
+
if (FAILED(create_brushes(ev))) { free(ev); return SOV_ERR_ALLOC; }
|
| 142 |
+
|
| 143 |
+
*out = ev;
|
| 144 |
+
return SOV_OK;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
extern "C" void editor_view_destroy(EditorView *ev) {
|
| 148 |
+
if (!ev) return;
|
| 149 |
+
release_brushes(ev);
|
| 150 |
+
if (ev->text_format) { ev->text_format->Release(); }
|
| 151 |
+
if (ev->gutter_format) { ev->gutter_format->Release(); }
|
| 152 |
+
if (ev->rt) { ev->rt->Release(); }
|
| 153 |
+
if (ev->d2d_factory) { ev->d2d_factory->Release(); }
|
| 154 |
+
if (ev->dw_factory) { ev->dw_factory->Release(); }
|
| 155 |
+
free(ev);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
extern "C" void editor_view_set_document(EditorView *ev, Document *doc) {
|
| 159 |
+
ev->doc = doc;
|
| 160 |
+
ev->caret = 0;
|
| 161 |
+
ev->sel_anchor = 0;
|
| 162 |
+
ev->has_sel = false;
|
| 163 |
+
ev->scroll_y = 0.0f;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
extern "C" void editor_view_set_diagnostics(EditorView *ev, const LspDiagList *list) {
|
| 167 |
+
if (!ev) return;
|
| 168 |
+
if (list) {
|
| 169 |
+
ev->diags = *list;
|
| 170 |
+
ev->has_diags = (list->count > 0);
|
| 171 |
+
} else {
|
| 172 |
+
ev->has_diags = false;
|
| 173 |
+
}
|
| 174 |
+
InvalidateRect(ev->hwnd, nullptr, FALSE);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
extern "C" void editor_view_resize(EditorView *ev, uint32_t w, uint32_t h) {
|
| 178 |
+
ev->width = w;
|
| 179 |
+
ev->height = h;
|
| 180 |
+
if (ev->rt) ev->rt->Resize(D2D1::SizeU(w, h));
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
/* Convert byte offset → (line, col) */
|
| 184 |
+
static void offset_to_line_col(Document *doc, size_t offset, size_t *line, size_t *col) {
|
| 185 |
+
char tmp[4096];
|
| 186 |
+
size_t pos = 0;
|
| 187 |
+
*line = 0;
|
| 188 |
+
*col = 0;
|
| 189 |
+
size_t buf_len = buffer_length(doc->buffer);
|
| 190 |
+
while (pos < offset && pos < buf_len) {
|
| 191 |
+
size_t chunk = offset - pos;
|
| 192 |
+
if (chunk > sizeof(tmp)) chunk = sizeof(tmp);
|
| 193 |
+
size_t n = buffer_read(doc->buffer, pos, tmp, chunk);
|
| 194 |
+
if (n == 0) break;
|
| 195 |
+
for (size_t i = 0; i < n && pos < offset; i++, pos++) {
|
| 196 |
+
if (tmp[i] == '\n') { (*line)++; *col = 0; }
|
| 197 |
+
else { (*col)++; }
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/* Render one line of text using DirectWrite */
|
| 203 |
+
static void draw_line(EditorView *ev, size_t line_idx, float y,
|
| 204 |
+
size_t caret_line, size_t sel_start, size_t sel_end, bool has_sel) {
|
| 205 |
+
if (!ev->doc) return;
|
| 206 |
+
float x0 = GUTTER_WIDTH + PADDING_LEFT;
|
| 207 |
+
float view_w = (float)ev->width - x0;
|
| 208 |
+
if (view_w <= 0) return;
|
| 209 |
+
|
| 210 |
+
/* Active line highlight */
|
| 211 |
+
if (line_idx == caret_line) {
|
| 212 |
+
D2D1_RECT_F lr = D2D1::RectF(GUTTER_WIDTH, y, (float)ev->width, y + LINE_HEIGHT);
|
| 213 |
+
ev->rt->FillRectangle(lr, ev->br_active_line);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/* Gutter line number */
|
| 217 |
+
wchar_t num[16];
|
| 218 |
+
swprintf_s(num, 16, L"%zu", line_idx + 1);
|
| 219 |
+
D2D1_RECT_F gr = D2D1::RectF(0, y + 2.0f, GUTTER_WIDTH - 6.0f, y + LINE_HEIGHT);
|
| 220 |
+
ev->rt->DrawText(num, (UINT32)wcslen(num), ev->gutter_format, gr, ev->br_gutter_fg);
|
| 221 |
+
|
| 222 |
+
/* Read line text */
|
| 223 |
+
size_t line_start = buffer_line_start(ev->doc->buffer, line_idx);
|
| 224 |
+
size_t line_len = buffer_line_length(ev->doc->buffer, line_idx);
|
| 225 |
+
if (line_len == 0) return;
|
| 226 |
+
|
| 227 |
+
char utf8[4096];
|
| 228 |
+
size_t n = buffer_read(ev->doc->buffer, line_start, utf8, line_len < 4095 ? line_len : 4095);
|
| 229 |
+
|
| 230 |
+
/* UTF-8 → UTF-16 */
|
| 231 |
+
wchar_t wide[4096];
|
| 232 |
+
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8, (int)n, wide, 4095);
|
| 233 |
+
if (wlen <= 0) return;
|
| 234 |
+
|
| 235 |
+
/* Selection highlight */
|
| 236 |
+
if (has_sel && sel_start < line_start + line_len && sel_end > line_start) {
|
| 237 |
+
size_t s = sel_start > line_start ? sel_start - line_start : 0;
|
| 238 |
+
size_t e = sel_end < line_start + line_len ? sel_end - line_start : line_len;
|
| 239 |
+
IDWriteTextLayout *layout = nullptr;
|
| 240 |
+
ev->dw_factory->CreateTextLayout(wide, (UINT32)wlen,
|
| 241 |
+
ev->text_format, view_w, LINE_HEIGHT, &layout);
|
| 242 |
+
if (layout) {
|
| 243 |
+
DWRITE_HIT_TEST_METRICS m;
|
| 244 |
+
float xs, dummy;
|
| 245 |
+
layout->HitTestTextPosition((UINT32)s, FALSE, &xs, &dummy, &m);
|
| 246 |
+
float xe;
|
| 247 |
+
layout->HitTestTextPosition((UINT32)(e < (size_t)wlen ? e : wlen), FALSE, &xe, &dummy, &m);
|
| 248 |
+
D2D1_RECT_F sr = D2D1::RectF(x0 + xs, y, x0 + xe, y + LINE_HEIGHT);
|
| 249 |
+
ev->rt->FillRectangle(sr, ev->br_sel);
|
| 250 |
+
layout->Release();
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
/* Draw text */
|
| 255 |
+
D2D1_RECT_F tr = D2D1::RectF(x0, y, x0 + view_w, y + LINE_HEIGHT);
|
| 256 |
+
ev->rt->DrawText(wide, (UINT32)wlen, ev->text_format, tr, ev->br_text);
|
| 257 |
+
|
| 258 |
+
/* LSP squiggles */
|
| 259 |
+
if (ev->has_diags) {
|
| 260 |
+
for (int di = 0; di < ev->diags.count; di++) {
|
| 261 |
+
const LspDiagnostic *d = &ev->diags.entries[di];
|
| 262 |
+
if (d->line != (uint32_t)line_idx) continue;
|
| 263 |
+
|
| 264 |
+
ID2D1SolidColorBrush *br = (d->severity == 1) ? ev->br_squiggle_e : ev->br_squiggle_w;
|
| 265 |
+
|
| 266 |
+
/* Compute x start from col via layout hit-test */
|
| 267 |
+
float sq_x = x0;
|
| 268 |
+
if (d->col > 0) {
|
| 269 |
+
IDWriteTextLayout *lay = nullptr;
|
| 270 |
+
ev->dw_factory->CreateTextLayout(wide, (UINT32)wlen,
|
| 271 |
+
ev->text_format, view_w, LINE_HEIGHT, &lay);
|
| 272 |
+
if (lay) {
|
| 273 |
+
DWRITE_HIT_TEST_METRICS m;
|
| 274 |
+
float dummy;
|
| 275 |
+
UINT32 col_clamped = (d->col < (UINT32)wlen) ? d->col : (UINT32)wlen;
|
| 276 |
+
lay->HitTestTextPosition(col_clamped, FALSE, &sq_x, &dummy, &m);
|
| 277 |
+
sq_x += x0;
|
| 278 |
+
lay->Release();
|
| 279 |
+
}
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
float sq_y = y + LINE_HEIGHT - 3.0f;
|
| 283 |
+
float sq_end = x0 + view_w;
|
| 284 |
+
float step = 4.0f;
|
| 285 |
+
bool up = true;
|
| 286 |
+
for (float sx = sq_x; sx < sq_end - step; sx += step, up = !up) {
|
| 287 |
+
float sy0 = up ? sq_y : sq_y + 2.0f;
|
| 288 |
+
float sy1 = up ? sq_y + 2.0f : sq_y;
|
| 289 |
+
ev->rt->DrawLine(D2D1::Point2F(sx, sy0),
|
| 290 |
+
D2D1::Point2F(sx + step, sy1), br, 1.2f);
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
extern "C" void editor_view_paint(EditorView *ev, RECT client) {
|
| 297 |
+
if (!ev || !ev->rt) return;
|
| 298 |
+
|
| 299 |
+
/* Caret blink */
|
| 300 |
+
DWORD now = GetTickCount();
|
| 301 |
+
if (now - ev->caret_tick > 530) {
|
| 302 |
+
ev->caret_visible = !ev->caret_visible;
|
| 303 |
+
ev->caret_tick = now;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
ev->rt->BeginDraw();
|
| 307 |
+
ev->rt->Clear(&C_BG);
|
| 308 |
+
|
| 309 |
+
/* Gutter background */
|
| 310 |
+
D2D1_RECT_F gutter_rect = D2D1::RectF(0, 0, GUTTER_WIDTH, (float)client.bottom);
|
| 311 |
+
ev->rt->FillRectangle(gutter_rect, ev->br_gutter_bg);
|
| 312 |
+
|
| 313 |
+
if (!ev->doc) {
|
| 314 |
+
ev->rt->EndDraw(nullptr, nullptr);
|
| 315 |
+
return;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
size_t caret_line, caret_col;
|
| 319 |
+
offset_to_line_col(ev->doc, ev->caret, &caret_line, &caret_col);
|
| 320 |
+
|
| 321 |
+
size_t sel_start = ev->has_sel ? (ev->sel_anchor < ev->caret ? ev->sel_anchor : ev->caret) : 0;
|
| 322 |
+
size_t sel_end = ev->has_sel ? (ev->sel_anchor > ev->caret ? ev->sel_anchor : ev->caret) : 0;
|
| 323 |
+
|
| 324 |
+
size_t total_lines = buffer_line_count(ev->doc->buffer);
|
| 325 |
+
size_t first_line = (size_t)ev->scroll_y;
|
| 326 |
+
size_t visible = (size_t)((float)ev->height / LINE_HEIGHT) + 2;
|
| 327 |
+
|
| 328 |
+
for (size_t i = first_line; i < total_lines && i < first_line + visible; i++) {
|
| 329 |
+
float y = (float)(i - first_line) * LINE_HEIGHT;
|
| 330 |
+
draw_line(ev, i, y, caret_line, sel_start, sel_end, ev->has_sel);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
/* Draw caret */
|
| 334 |
+
if (ev->caret_visible) {
|
| 335 |
+
size_t line_start = buffer_line_start(ev->doc->buffer, caret_line);
|
| 336 |
+
size_t char_in_line = ev->caret - line_start;
|
| 337 |
+
|
| 338 |
+
float caret_x = GUTTER_WIDTH + PADDING_LEFT;
|
| 339 |
+
if (char_in_line > 0) {
|
| 340 |
+
char utf8[1024];
|
| 341 |
+
size_t n = buffer_read(ev->doc->buffer, line_start, utf8, char_in_line < 1023 ? char_in_line : 1023);
|
| 342 |
+
wchar_t wide[1024];
|
| 343 |
+
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8, (int)n, wide, 1023);
|
| 344 |
+
if (wlen > 0) {
|
| 345 |
+
IDWriteTextLayout *layout = nullptr;
|
| 346 |
+
ev->dw_factory->CreateTextLayout(wide, (UINT32)wlen,
|
| 347 |
+
ev->text_format, 9999.0f, LINE_HEIGHT, &layout);
|
| 348 |
+
if (layout) {
|
| 349 |
+
DWRITE_TEXT_METRICS tm;
|
| 350 |
+
layout->GetMetrics(&tm);
|
| 351 |
+
caret_x += tm.width;
|
| 352 |
+
layout->Release();
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
float caret_y = (float)(caret_line - first_line) * LINE_HEIGHT;
|
| 357 |
+
D2D1_RECT_F cr = D2D1::RectF(caret_x, caret_y + 2.0f, caret_x + CARET_WIDTH, caret_y + LINE_HEIGHT - 2.0f);
|
| 358 |
+
ev->rt->FillRectangle(cr, ev->br_caret);
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
HRESULT hr = ev->rt->EndDraw(nullptr, nullptr);
|
| 362 |
+
if (hr == (HRESULT)D2DERR_RECREATE_TARGET) {
|
| 363 |
+
/* Device lost — recreate */
|
| 364 |
+
release_brushes(ev);
|
| 365 |
+
ev->rt->Release(); ev->rt = nullptr;
|
| 366 |
+
RECT rc; GetClientRect(ev->hwnd, &rc);
|
| 367 |
+
D2D1_RENDER_TARGET_PROPERTIES rtp = D2D1::RenderTargetProperties();
|
| 368 |
+
D2D1_HWND_RENDER_TARGET_PROPERTIES hwp = D2D1::HwndRenderTargetProperties(
|
| 369 |
+
ev->hwnd, D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top));
|
| 370 |
+
ev->d2d_factory->CreateHwndRenderTarget(rtp, hwp, &ev->rt);
|
| 371 |
+
create_brushes(ev);
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
/* Clamp caret into buffer */
|
| 376 |
+
static void clamp_caret(EditorView *ev) {
|
| 377 |
+
if (!ev->doc) return;
|
| 378 |
+
size_t len = buffer_length(ev->doc->buffer);
|
| 379 |
+
if (ev->caret > len) ev->caret = len;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
/* Move caret left/right by one byte (UTF-8 aware would need more work) */
|
| 383 |
+
static void caret_move_left(EditorView *ev) {
|
| 384 |
+
if (ev->caret > 0) ev->caret--;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
static void caret_move_right(EditorView *ev) {
|
| 388 |
+
if (!ev->doc) return;
|
| 389 |
+
if (ev->caret < buffer_length(ev->doc->buffer)) ev->caret++;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
static void caret_move_up(EditorView *ev) {
|
| 393 |
+
if (!ev->doc) return;
|
| 394 |
+
size_t line, col;
|
| 395 |
+
offset_to_line_col(ev->doc, ev->caret, &line, &col);
|
| 396 |
+
if (line == 0) { ev->caret = 0; return; }
|
| 397 |
+
size_t prev_start = buffer_line_start(ev->doc->buffer, line - 1);
|
| 398 |
+
size_t prev_len = buffer_line_length(ev->doc->buffer, line - 1);
|
| 399 |
+
ev->caret = prev_start + (col < prev_len ? col : prev_len);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
static void caret_move_down(EditorView *ev) {
|
| 403 |
+
if (!ev->doc) return;
|
| 404 |
+
size_t line, col;
|
| 405 |
+
offset_to_line_col(ev->doc, ev->caret, &line, &col);
|
| 406 |
+
size_t total = buffer_line_count(ev->doc->buffer);
|
| 407 |
+
if (line + 1 >= total) return;
|
| 408 |
+
size_t next_start = buffer_line_start(ev->doc->buffer, line + 1);
|
| 409 |
+
size_t next_len = buffer_line_length(ev->doc->buffer, line + 1);
|
| 410 |
+
ev->caret = next_start + (col < next_len ? col : next_len);
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
static void caret_home(EditorView *ev) {
|
| 414 |
+
if (!ev->doc) return;
|
| 415 |
+
size_t line, col; (void)col;
|
| 416 |
+
offset_to_line_col(ev->doc, ev->caret, &line, &col);
|
| 417 |
+
ev->caret = buffer_line_start(ev->doc->buffer, line);
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
static void caret_end(EditorView *ev) {
|
| 421 |
+
if (!ev->doc) return;
|
| 422 |
+
size_t line, col; (void)col;
|
| 423 |
+
offset_to_line_col(ev->doc, ev->caret, &line, &col);
|
| 424 |
+
ev->caret = buffer_line_start(ev->doc->buffer, line)
|
| 425 |
+
+ buffer_line_length(ev->doc->buffer, line);
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
static void ensure_caret_visible(EditorView *ev) {
|
| 429 |
+
if (!ev->doc) return;
|
| 430 |
+
size_t line, col; (void)col;
|
| 431 |
+
offset_to_line_col(ev->doc, ev->caret, &line, &col);
|
| 432 |
+
float visible_lines = (float)ev->height / LINE_HEIGHT;
|
| 433 |
+
if ((float)line < ev->scroll_y) {
|
| 434 |
+
ev->scroll_y = (float)line;
|
| 435 |
+
} else if ((float)line >= ev->scroll_y + visible_lines - 1) {
|
| 436 |
+
ev->scroll_y = (float)line - visible_lines + 2.0f;
|
| 437 |
+
if (ev->scroll_y < 0) ev->scroll_y = 0;
|
| 438 |
+
}
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
extern "C" void editor_view_key_down(EditorView *ev, UINT vk, UINT mods) {
|
| 442 |
+
if (!ev->doc) return;
|
| 443 |
+
|
| 444 |
+
bool shift = (mods & 1) != 0;
|
| 445 |
+
bool ctrl = (mods & 2) != 0;
|
| 446 |
+
|
| 447 |
+
if (shift && !ev->has_sel) {
|
| 448 |
+
ev->sel_anchor = ev->caret;
|
| 449 |
+
ev->has_sel = true;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
switch (vk) {
|
| 453 |
+
case VK_LEFT:
|
| 454 |
+
if (!shift && ev->has_sel) { ev->caret = ev->sel_anchor < ev->caret ? ev->sel_anchor : ev->caret; ev->has_sel = false; }
|
| 455 |
+
else caret_move_left(ev);
|
| 456 |
+
break;
|
| 457 |
+
case VK_RIGHT:
|
| 458 |
+
if (!shift && ev->has_sel) { ev->caret = ev->sel_anchor > ev->caret ? ev->sel_anchor : ev->caret; ev->has_sel = false; }
|
| 459 |
+
else caret_move_right(ev);
|
| 460 |
+
break;
|
| 461 |
+
case VK_UP: caret_move_up(ev); break;
|
| 462 |
+
case VK_DOWN: caret_move_down(ev); break;
|
| 463 |
+
case VK_HOME: caret_home(ev); break;
|
| 464 |
+
case VK_END: caret_end(ev); break;
|
| 465 |
+
case VK_PRIOR: /* Page Up */
|
| 466 |
+
for (int i = 0; i < 20; i++) caret_move_up(ev);
|
| 467 |
+
break;
|
| 468 |
+
case VK_NEXT: /* Page Down */
|
| 469 |
+
for (int i = 0; i < 20; i++) caret_move_down(ev);
|
| 470 |
+
break;
|
| 471 |
+
case VK_DELETE: {
|
| 472 |
+
if (ev->has_sel) {
|
| 473 |
+
size_t s = ev->sel_anchor < ev->caret ? ev->sel_anchor : ev->caret;
|
| 474 |
+
size_t e = ev->sel_anchor > ev->caret ? ev->sel_anchor : ev->caret;
|
| 475 |
+
buffer_delete(ev->doc->buffer, s, e - s);
|
| 476 |
+
ev->caret = s; ev->has_sel = false;
|
| 477 |
+
} else {
|
| 478 |
+
buffer_delete(ev->doc->buffer, ev->caret, 1);
|
| 479 |
+
}
|
| 480 |
+
break;
|
| 481 |
+
}
|
| 482 |
+
case VK_BACK: {
|
| 483 |
+
if (ev->has_sel) {
|
| 484 |
+
size_t s = ev->sel_anchor < ev->caret ? ev->sel_anchor : ev->caret;
|
| 485 |
+
size_t e = ev->sel_anchor > ev->caret ? ev->sel_anchor : ev->caret;
|
| 486 |
+
buffer_delete(ev->doc->buffer, s, e - s);
|
| 487 |
+
ev->caret = s; ev->has_sel = false;
|
| 488 |
+
} else if (ev->caret > 0) {
|
| 489 |
+
ev->caret--;
|
| 490 |
+
buffer_delete(ev->doc->buffer, ev->caret, 1);
|
| 491 |
+
}
|
| 492 |
+
break;
|
| 493 |
+
}
|
| 494 |
+
case 'Z':
|
| 495 |
+
if (ctrl) {
|
| 496 |
+
if (buffer_can_undo(ev->doc->buffer)) {
|
| 497 |
+
buffer_undo(ev->doc->buffer);
|
| 498 |
+
clamp_caret(ev);
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
+
break;
|
| 502 |
+
case 'Y':
|
| 503 |
+
if (ctrl) {
|
| 504 |
+
if (buffer_can_redo(ev->doc->buffer)) {
|
| 505 |
+
buffer_redo(ev->doc->buffer);
|
| 506 |
+
clamp_caret(ev);
|
| 507 |
+
}
|
| 508 |
+
}
|
| 509 |
+
break;
|
| 510 |
+
case 'A':
|
| 511 |
+
if (ctrl) {
|
| 512 |
+
ev->sel_anchor = 0;
|
| 513 |
+
ev->caret = buffer_length(ev->doc->buffer);
|
| 514 |
+
ev->has_sel = true;
|
| 515 |
+
}
|
| 516 |
+
break;
|
| 517 |
+
case 'S':
|
| 518 |
+
if (ctrl) document_save(ev->doc);
|
| 519 |
+
break;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
if (!shift) ev->has_sel = false;
|
| 523 |
+
clamp_caret(ev);
|
| 524 |
+
ensure_caret_visible(ev);
|
| 525 |
+
ev->caret_visible = true;
|
| 526 |
+
ev->caret_tick = GetTickCount();
|
| 527 |
+
|
| 528 |
+
InvalidateRect(ev->hwnd, NULL, FALSE);
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
extern "C" void editor_view_char(EditorView *ev, wchar_t ch) {
|
| 532 |
+
if (!ev->doc) return;
|
| 533 |
+
if (ch < 0x20 && ch != '\t' && ch != '\n' && ch != '\r') return;
|
| 534 |
+
if (ch == '\r') ch = '\n';
|
| 535 |
+
|
| 536 |
+
if (ev->has_sel) {
|
| 537 |
+
size_t s = ev->sel_anchor < ev->caret ? ev->sel_anchor : ev->caret;
|
| 538 |
+
size_t e = ev->sel_anchor > ev->caret ? ev->sel_anchor : ev->caret;
|
| 539 |
+
buffer_delete(ev->doc->buffer, s, e - s);
|
| 540 |
+
ev->caret = s; ev->has_sel = false;
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
/* UTF-16 → UTF-8 */
|
| 544 |
+
char utf8[4];
|
| 545 |
+
int utf8_len = WideCharToMultiByte(CP_UTF8, 0, &ch, 1, utf8, 4, NULL, NULL);
|
| 546 |
+
if (utf8_len > 0) {
|
| 547 |
+
buffer_insert(ev->doc->buffer, ev->caret, utf8, (size_t)utf8_len);
|
| 548 |
+
ev->caret += (size_t)utf8_len;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
ensure_caret_visible(ev);
|
| 552 |
+
ev->caret_visible = true;
|
| 553 |
+
ev->caret_tick = GetTickCount();
|
| 554 |
+
InvalidateRect(ev->hwnd, NULL, FALSE);
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
/* Hit test: pixel (x, y) → buffer offset */
|
| 558 |
+
static size_t hit_test(EditorView *ev, int px, int py) {
|
| 559 |
+
if (!ev->doc) return 0;
|
| 560 |
+
|
| 561 |
+
size_t line = (size_t)(ev->scroll_y + (float)py / LINE_HEIGHT);
|
| 562 |
+
size_t total = buffer_line_count(ev->doc->buffer);
|
| 563 |
+
if (line >= total) line = total - 1;
|
| 564 |
+
|
| 565 |
+
size_t line_start = buffer_line_start(ev->doc->buffer, line);
|
| 566 |
+
size_t line_len = buffer_line_length(ev->doc->buffer, line);
|
| 567 |
+
if (line_len == 0) return line_start;
|
| 568 |
+
|
| 569 |
+
float text_x = (float)px - (GUTTER_WIDTH + PADDING_LEFT);
|
| 570 |
+
if (text_x <= 0) return line_start;
|
| 571 |
+
|
| 572 |
+
char utf8[4096];
|
| 573 |
+
size_t n = buffer_read(ev->doc->buffer, line_start, utf8, line_len < 4095 ? line_len : 4095);
|
| 574 |
+
wchar_t wide[4096];
|
| 575 |
+
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8, (int)n, wide, 4095);
|
| 576 |
+
if (wlen <= 0) return line_start;
|
| 577 |
+
|
| 578 |
+
IDWriteTextLayout *layout = nullptr;
|
| 579 |
+
ev->dw_factory->CreateTextLayout(wide, (UINT32)wlen,
|
| 580 |
+
ev->text_format, 9999.0f, LINE_HEIGHT, &layout);
|
| 581 |
+
if (!layout) return line_start;
|
| 582 |
+
|
| 583 |
+
BOOL trailing, inside;
|
| 584 |
+
DWRITE_HIT_TEST_METRICS m;
|
| 585 |
+
layout->HitTestPoint(text_x, LINE_HEIGHT / 2.0f, &trailing, &inside, &m);
|
| 586 |
+
layout->Release();
|
| 587 |
+
|
| 588 |
+
size_t char_pos = m.textPosition + (trailing ? 1 : 0);
|
| 589 |
+
return line_start + (char_pos < line_len ? char_pos : line_len);
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
extern "C" void editor_view_mouse_down(EditorView *ev, int x, int y) {
|
| 593 |
+
ev->caret = hit_test(ev, x, y);
|
| 594 |
+
ev->sel_anchor = ev->caret;
|
| 595 |
+
ev->has_sel = false;
|
| 596 |
+
ev->caret_visible = true;
|
| 597 |
+
ev->caret_tick = GetTickCount();
|
| 598 |
+
InvalidateRect(ev->hwnd, NULL, FALSE);
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
extern "C" void editor_view_mouse_move(EditorView *ev, int x, int y, bool btn) {
|
| 602 |
+
if (!btn) return;
|
| 603 |
+
size_t new_pos = hit_test(ev, x, y);
|
| 604 |
+
if (new_pos != ev->caret) {
|
| 605 |
+
ev->caret = new_pos;
|
| 606 |
+
ev->has_sel = (ev->caret != ev->sel_anchor);
|
| 607 |
+
InvalidateRect(ev->hwnd, NULL, FALSE);
|
| 608 |
+
}
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
extern "C" size_t editor_view_caret_offset(const EditorView *ev) {
|
| 612 |
+
return ev->caret;
|
| 613 |
+
}
|
ide/native/editor/editor_view.h
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_EDITOR_VIEW_H
|
| 2 |
+
#define SOVEREIGN_EDITOR_VIEW_H
|
| 3 |
+
|
| 4 |
+
#include "document.h"
|
| 5 |
+
#include "../core/errors.h"
|
| 6 |
+
#include "../lsp/client.h"
|
| 7 |
+
#include <windows.h>
|
| 8 |
+
#include <stdbool.h>
|
| 9 |
+
#include <stdint.h>
|
| 10 |
+
|
| 11 |
+
typedef struct EditorView EditorView;
|
| 12 |
+
|
| 13 |
+
SovResult editor_view_create(EditorView **out, HWND hwnd);
|
| 14 |
+
void editor_view_destroy(EditorView *ev);
|
| 15 |
+
void editor_view_set_document(EditorView *ev, Document *doc);
|
| 16 |
+
void editor_view_set_diagnostics(EditorView *ev, const LspDiagList *list);
|
| 17 |
+
void editor_view_paint(EditorView *ev, RECT client);
|
| 18 |
+
void editor_view_resize(EditorView *ev, uint32_t w, uint32_t h);
|
| 19 |
+
|
| 20 |
+
void editor_view_key_down(EditorView *ev, UINT vk, UINT mods);
|
| 21 |
+
void editor_view_char(EditorView *ev, wchar_t ch);
|
| 22 |
+
void editor_view_mouse_down(EditorView *ev, int x, int y);
|
| 23 |
+
void editor_view_mouse_move(EditorView *ev, int x, int y, bool btn);
|
| 24 |
+
|
| 25 |
+
size_t editor_view_caret_offset(const EditorView *ev);
|
| 26 |
+
|
| 27 |
+
#endif
|
ide/native/fcl/evaluator.c
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — FCL Evaluator
|
| 3 |
+
* Default-deny. Rules evaluated in order; first match wins.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
#include "evaluator.h"
|
| 7 |
+
#include <string.h>
|
| 8 |
+
|
| 9 |
+
static bool path_matches(Str pattern, Str target) {
|
| 10 |
+
if (pattern.len == 1 && pattern.ptr[0] == '*') return true;
|
| 11 |
+
if (pattern.len == target.len && memcmp(pattern.ptr, target.ptr, pattern.len) == 0) return true;
|
| 12 |
+
|
| 13 |
+
if (pattern.len >= 2 && pattern.ptr[pattern.len - 1] == '*') {
|
| 14 |
+
/* prefix glob: "src/ STAR" matches "src/foo.c" */
|
| 15 |
+
size_t prefix_len = pattern.len - 1;
|
| 16 |
+
if (target.len >= prefix_len && memcmp(pattern.ptr, target.ptr, prefix_len) == 0) {
|
| 17 |
+
return true;
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
if (pattern.len >= 2 && pattern.ptr[0] == '*') {
|
| 22 |
+
/* suffix glob: "*.exe" matches "virus.exe" */
|
| 23 |
+
size_t suffix_len = pattern.len - 1;
|
| 24 |
+
const char *suffix = pattern.ptr + 1;
|
| 25 |
+
if (target.len >= suffix_len &&
|
| 26 |
+
memcmp(target.ptr + target.len - suffix_len, suffix, suffix_len) == 0) {
|
| 27 |
+
return true;
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
return false;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
FclVerdict fcl_evaluate(const FclRuleSet *rules, const FclRequest *req) {
|
| 35 |
+
for (size_t i = 0; i < rules->count; i++) {
|
| 36 |
+
const FclRule *r = &rules->rules[i];
|
| 37 |
+
|
| 38 |
+
if (r->action != req->action) continue;
|
| 39 |
+
if (!path_matches(r->subject, req->target)) continue;
|
| 40 |
+
if (r->agent.ptr && !str_eq(r->agent, req->agent)) continue;
|
| 41 |
+
|
| 42 |
+
switch (r->kind) {
|
| 43 |
+
case FCL_PERMIT:
|
| 44 |
+
if (r->requires_approval) return FCL_AWAIT_APPROVAL;
|
| 45 |
+
return FCL_ALLOW;
|
| 46 |
+
case FCL_DENY:
|
| 47 |
+
return FCL_DENY_RULE;
|
| 48 |
+
default:
|
| 49 |
+
continue;
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
return FCL_DENY_NO_RULE; /* default deny */
|
| 54 |
+
}
|
ide/native/fcl/evaluator.h
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_FCL_EVALUATOR_H
|
| 2 |
+
#define SOVEREIGN_FCL_EVALUATOR_H
|
| 3 |
+
|
| 4 |
+
#include "parser.h"
|
| 5 |
+
#include <stdbool.h>
|
| 6 |
+
|
| 7 |
+
typedef enum FclVerdict {
|
| 8 |
+
FCL_ALLOW,
|
| 9 |
+
FCL_DENY_RULE,
|
| 10 |
+
FCL_DENY_NO_RULE,
|
| 11 |
+
FCL_AWAIT_APPROVAL,
|
| 12 |
+
} FclVerdict;
|
| 13 |
+
|
| 14 |
+
typedef struct FclRequest {
|
| 15 |
+
FclAction action;
|
| 16 |
+
Str target;
|
| 17 |
+
Str agent;
|
| 18 |
+
} FclRequest;
|
| 19 |
+
|
| 20 |
+
FclVerdict fcl_evaluate(const FclRuleSet *rules, const FclRequest *req);
|
| 21 |
+
|
| 22 |
+
#endif /* SOVEREIGN_FCL_EVALUATOR_H */
|
ide/native/fcl/parser.h
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_FCL_PARSER_H
|
| 2 |
+
#define SOVEREIGN_FCL_PARSER_H
|
| 3 |
+
|
| 4 |
+
#include "../core/errors.h"
|
| 5 |
+
#include "../core/strings.h"
|
| 6 |
+
#include <stdbool.h>
|
| 7 |
+
|
| 8 |
+
typedef enum FclNodeKind {
|
| 9 |
+
FCL_PERMIT,
|
| 10 |
+
FCL_DENY,
|
| 11 |
+
FCL_REQUIRE,
|
| 12 |
+
FCL_CONSTRAIN,
|
| 13 |
+
FCL_BOUNDARY,
|
| 14 |
+
} FclNodeKind;
|
| 15 |
+
|
| 16 |
+
typedef enum FclAction {
|
| 17 |
+
FCL_ACT_READ,
|
| 18 |
+
FCL_ACT_WRITE,
|
| 19 |
+
FCL_ACT_EXECUTE,
|
| 20 |
+
FCL_ACT_GIT_READ,
|
| 21 |
+
FCL_ACT_GIT_MUTATE,
|
| 22 |
+
FCL_ACT_BUILD,
|
| 23 |
+
FCL_ACT_DEPLOY,
|
| 24 |
+
FCL_ACT_MODEL,
|
| 25 |
+
} FclAction;
|
| 26 |
+
|
| 27 |
+
typedef struct FclRule {
|
| 28 |
+
FclNodeKind kind;
|
| 29 |
+
FclAction action;
|
| 30 |
+
Str subject; /* file path or tool name */
|
| 31 |
+
Str agent; /* which agent requested */
|
| 32 |
+
bool requires_approval;
|
| 33 |
+
} FclRule;
|
| 34 |
+
|
| 35 |
+
typedef struct FclRuleSet {
|
| 36 |
+
FclRule *rules;
|
| 37 |
+
size_t count;
|
| 38 |
+
size_t capacity;
|
| 39 |
+
} FclRuleSet;
|
| 40 |
+
|
| 41 |
+
SovResult fcl_parse(const char *source, size_t len, FclRuleSet *out);
|
| 42 |
+
void fcl_ruleset_free(FclRuleSet *rs);
|
| 43 |
+
|
| 44 |
+
#endif /* SOVEREIGN_FCL_PARSER_H */
|
ide/native/git/repository.c
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Git repository helpers
|
| 3 |
+
* Reads .git/HEAD and .git/index mtime for branch + dirty detection.
|
| 4 |
+
* No libgit2 — pure Win32 file I/O.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
#include "repository.h"
|
| 8 |
+
#include <stdlib.h>
|
| 9 |
+
#include <string.h>
|
| 10 |
+
#include <stdio.h>
|
| 11 |
+
|
| 12 |
+
/* Walk up from path looking for a .git directory/file */
|
| 13 |
+
static bool find_git_root(const wchar_t *start, wchar_t *root_out, size_t root_cap) {
|
| 14 |
+
wchar_t cur[MAX_PATH];
|
| 15 |
+
wcscpy_s(cur, MAX_PATH, start);
|
| 16 |
+
|
| 17 |
+
for (int depth = 0; depth < 32; depth++) {
|
| 18 |
+
wchar_t candidate[MAX_PATH];
|
| 19 |
+
swprintf_s(candidate, MAX_PATH, L"%s\\.git", cur);
|
| 20 |
+
|
| 21 |
+
DWORD attr = GetFileAttributesW(candidate);
|
| 22 |
+
if (attr != INVALID_FILE_ATTRIBUTES) {
|
| 23 |
+
wcscpy_s(root_out, root_cap, cur);
|
| 24 |
+
return true;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* Go up one level */
|
| 28 |
+
wchar_t *last = wcsrchr(cur, L'\\');
|
| 29 |
+
if (!last || last == cur) break;
|
| 30 |
+
*last = L'\0';
|
| 31 |
+
}
|
| 32 |
+
return false;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
SovResult git_repo_open(GitRepo *repo, const wchar_t *path) {
|
| 36 |
+
memset(repo, 0, sizeof(*repo));
|
| 37 |
+
|
| 38 |
+
if (!find_git_root(path, repo->root, MAX_PATH))
|
| 39 |
+
return SOV_ERR_IO;
|
| 40 |
+
|
| 41 |
+
swprintf_s(repo->git_dir, MAX_PATH, L"%s\\.git", repo->root);
|
| 42 |
+
repo->valid = true;
|
| 43 |
+
return git_repo_refresh(repo);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
SovResult git_repo_refresh(GitRepo *repo) {
|
| 47 |
+
if (!repo->valid) return SOV_ERR_IO;
|
| 48 |
+
|
| 49 |
+
/* Read HEAD → branch name */
|
| 50 |
+
wchar_t head_path[MAX_PATH];
|
| 51 |
+
swprintf_s(head_path, MAX_PATH, L"%s\\HEAD", repo->git_dir);
|
| 52 |
+
|
| 53 |
+
HANDLE hf = CreateFileW(head_path, GENERIC_READ, FILE_SHARE_READ,
|
| 54 |
+
NULL, OPEN_EXISTING, 0, NULL);
|
| 55 |
+
if (hf == INVALID_HANDLE_VALUE) return SOV_ERR_IO;
|
| 56 |
+
|
| 57 |
+
char buf[256] = {0};
|
| 58 |
+
DWORD nread = 0;
|
| 59 |
+
ReadFile(hf, buf, sizeof(buf) - 1, &nread, NULL);
|
| 60 |
+
CloseHandle(hf);
|
| 61 |
+
|
| 62 |
+
/* "ref: refs/heads/main\n" or detached SHA */
|
| 63 |
+
static const char prefix[] = "ref: refs/heads/";
|
| 64 |
+
if (strncmp(buf, prefix, sizeof(prefix) - 1) == 0) {
|
| 65 |
+
const char *branch = buf + sizeof(prefix) - 1;
|
| 66 |
+
size_t len = strcspn(branch, "\r\n");
|
| 67 |
+
if (len >= 128) len = 127;
|
| 68 |
+
MultiByteToWideChar(CP_UTF8, 0, branch, (int)len,
|
| 69 |
+
repo->branch, 128);
|
| 70 |
+
repo->branch[len] = L'\0';
|
| 71 |
+
repo->detached = false;
|
| 72 |
+
} else {
|
| 73 |
+
/* Detached HEAD — show short SHA */
|
| 74 |
+
size_t len = strcspn(buf, "\r\n");
|
| 75 |
+
if (len > 8) len = 8;
|
| 76 |
+
MultiByteToWideChar(CP_UTF8, 0, buf, (int)len,
|
| 77 |
+
repo->branch, 128);
|
| 78 |
+
repo->branch[len] = L'\0';
|
| 79 |
+
repo->detached = true;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* Dirty detection: compare index mtime to HEAD mtime.
|
| 83 |
+
* If index is newer → working tree likely has staged changes.
|
| 84 |
+
* Good enough for status bar; not a full diff. */
|
| 85 |
+
wchar_t index_path[MAX_PATH];
|
| 86 |
+
swprintf_s(index_path, MAX_PATH, L"%s\\index", repo->git_dir);
|
| 87 |
+
|
| 88 |
+
WIN32_FILE_ATTRIBUTE_DATA fa_index = {0}, fa_head = {0};
|
| 89 |
+
bool has_index = GetFileAttributesExW(index_path, GetFileExInfoStandard, &fa_index);
|
| 90 |
+
bool has_head = GetFileAttributesExW(head_path, GetFileExInfoStandard, &fa_head);
|
| 91 |
+
|
| 92 |
+
if (has_index && has_head) {
|
| 93 |
+
LONG cmp = CompareFileTime(&fa_index.ftLastWriteTime,
|
| 94 |
+
&fa_head.ftLastWriteTime);
|
| 95 |
+
repo->dirty = (cmp > 0);
|
| 96 |
+
} else {
|
| 97 |
+
repo->dirty = false;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
return SOV_OK;
|
| 101 |
+
}
|
ide/native/git/repository.h
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_GIT_REPOSITORY_H
|
| 2 |
+
#define SOVEREIGN_GIT_REPOSITORY_H
|
| 3 |
+
|
| 4 |
+
#include <windows.h>
|
| 5 |
+
#include <stdbool.h>
|
| 6 |
+
#include "../core/errors.h"
|
| 7 |
+
|
| 8 |
+
typedef struct GitRepo {
|
| 9 |
+
wchar_t root[MAX_PATH];
|
| 10 |
+
wchar_t git_dir[MAX_PATH];
|
| 11 |
+
wchar_t branch[128];
|
| 12 |
+
bool detached;
|
| 13 |
+
bool dirty;
|
| 14 |
+
bool valid;
|
| 15 |
+
} GitRepo;
|
| 16 |
+
|
| 17 |
+
SovResult git_repo_open(GitRepo *repo, const wchar_t *path);
|
| 18 |
+
SovResult git_repo_refresh(GitRepo *repo);
|
| 19 |
+
|
| 20 |
+
#endif
|
ide/native/graphics/d2d_renderer.cpp
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Direct2D Renderer (C++ for COM vtable calls)
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
#include <windows.h>
|
| 6 |
+
#include <d2d1_1.h>
|
| 7 |
+
#include <dwrite.h>
|
| 8 |
+
#include <stdint.h>
|
| 9 |
+
|
| 10 |
+
extern "C" {
|
| 11 |
+
#include "d2d_renderer.h"
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
static struct {
|
| 15 |
+
ID2D1Factory *factory;
|
| 16 |
+
ID2D1HwndRenderTarget *target;
|
| 17 |
+
IDWriteFactory *dwrite_factory;
|
| 18 |
+
bool initialized;
|
| 19 |
+
} g_renderer;
|
| 20 |
+
|
| 21 |
+
extern "C" SovResult renderer_init(HWND hwnd) {
|
| 22 |
+
HRESULT hr = D2D1CreateFactory(
|
| 23 |
+
D2D1_FACTORY_TYPE_SINGLE_THREADED,
|
| 24 |
+
&g_renderer.factory
|
| 25 |
+
);
|
| 26 |
+
if (FAILED(hr)) return SOV_ERR_ALLOC;
|
| 27 |
+
|
| 28 |
+
RECT rc;
|
| 29 |
+
GetClientRect(hwnd, &rc);
|
| 30 |
+
|
| 31 |
+
D2D1_SIZE_U size = D2D1::SizeU(
|
| 32 |
+
(UINT32)(rc.right - rc.left),
|
| 33 |
+
(UINT32)(rc.bottom - rc.top)
|
| 34 |
+
);
|
| 35 |
+
|
| 36 |
+
D2D1_RENDER_TARGET_PROPERTIES rtprops = D2D1::RenderTargetProperties();
|
| 37 |
+
D2D1_HWND_RENDER_TARGET_PROPERTIES hwndprops =
|
| 38 |
+
D2D1::HwndRenderTargetProperties(hwnd, size);
|
| 39 |
+
|
| 40 |
+
hr = g_renderer.factory->CreateHwndRenderTarget(
|
| 41 |
+
rtprops, hwndprops, &g_renderer.target
|
| 42 |
+
);
|
| 43 |
+
if (FAILED(hr)) return SOV_ERR_ALLOC;
|
| 44 |
+
|
| 45 |
+
hr = DWriteCreateFactory(
|
| 46 |
+
DWRITE_FACTORY_TYPE_SHARED,
|
| 47 |
+
__uuidof(IDWriteFactory),
|
| 48 |
+
reinterpret_cast<IUnknown **>(&g_renderer.dwrite_factory)
|
| 49 |
+
);
|
| 50 |
+
if (FAILED(hr)) return SOV_ERR_ALLOC;
|
| 51 |
+
|
| 52 |
+
g_renderer.initialized = true;
|
| 53 |
+
return SOV_OK;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
extern "C" void renderer_begin_frame(void) {
|
| 57 |
+
if (!g_renderer.initialized) return;
|
| 58 |
+
g_renderer.target->BeginDraw();
|
| 59 |
+
D2D1_COLOR_F bg = { 0.12f, 0.12f, 0.14f, 1.0f };
|
| 60 |
+
g_renderer.target->Clear(&bg);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
extern "C" void renderer_end_frame(void) {
|
| 64 |
+
if (!g_renderer.initialized) return;
|
| 65 |
+
HRESULT hr = g_renderer.target->EndDraw();
|
| 66 |
+
if (hr == (HRESULT)D2DERR_RECREATE_TARGET) {
|
| 67 |
+
g_renderer.initialized = false;
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
extern "C" void renderer_resize(uint32_t width, uint32_t height) {
|
| 72 |
+
if (!g_renderer.initialized) return;
|
| 73 |
+
D2D1_SIZE_U s = D2D1::SizeU(width, height);
|
| 74 |
+
g_renderer.target->Resize(s);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
extern "C" void renderer_shutdown(void) {
|
| 78 |
+
if (g_renderer.target) { g_renderer.target->Release(); g_renderer.target = NULL; }
|
| 79 |
+
if (g_renderer.factory) { g_renderer.factory->Release(); g_renderer.factory = NULL; }
|
| 80 |
+
if (g_renderer.dwrite_factory) { g_renderer.dwrite_factory->Release(); g_renderer.dwrite_factory = NULL; }
|
| 81 |
+
g_renderer.initialized = false;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
extern "C" void *renderer_get_target(void) {
|
| 85 |
+
return g_renderer.target;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
extern "C" void *renderer_get_dwrite(void) {
|
| 89 |
+
return g_renderer.dwrite_factory;
|
| 90 |
+
}
|
ide/native/graphics/d2d_renderer.h
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_D2D_RENDERER_H
|
| 2 |
+
#define SOVEREIGN_D2D_RENDERER_H
|
| 3 |
+
|
| 4 |
+
#include <windows.h>
|
| 5 |
+
#include <stdint.h>
|
| 6 |
+
#include "../core/errors.h"
|
| 7 |
+
|
| 8 |
+
SovResult renderer_init(HWND hwnd);
|
| 9 |
+
void renderer_begin_frame(void);
|
| 10 |
+
void renderer_end_frame(void);
|
| 11 |
+
void renderer_resize(uint32_t width, uint32_t height);
|
| 12 |
+
void renderer_shutdown(void);
|
| 13 |
+
|
| 14 |
+
void *renderer_get_target(void);
|
| 15 |
+
void *renderer_get_dwrite(void);
|
| 16 |
+
|
| 17 |
+
#endif /* SOVEREIGN_D2D_RENDERER_H */
|
ide/native/lsp/client.c
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — LSP Client
|
| 3 |
+
* JSON-RPC 2.0 over stdio pipes to clangd (or any LSP server).
|
| 4 |
+
* Handles: initialize, initialized, textDocument/didOpen, didChange,
|
| 5 |
+
* shutdown, and inbound publishDiagnostics notifications.
|
| 6 |
+
*
|
| 7 |
+
* JSON is hand-built (no external lib). Parsing is minimal: we walk
|
| 8 |
+
* the response bytes looking for the fields we need.
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
#include "client.h"
|
| 12 |
+
#include <stdlib.h>
|
| 13 |
+
#include <string.h>
|
| 14 |
+
#include <stdio.h>
|
| 15 |
+
|
| 16 |
+
/* ── JSON helpers ────────────────────────────────────────────── */
|
| 17 |
+
|
| 18 |
+
/* Escape a UTF-8 string into a JSON string (caller provides buf). */
|
| 19 |
+
static int json_escape(const char *src, char *dst, int cap) {
|
| 20 |
+
int n = 0;
|
| 21 |
+
for (; *src && n < cap - 2; src++) {
|
| 22 |
+
unsigned char c = (unsigned char)*src;
|
| 23 |
+
if (c == '"') { dst[n++]='\\'; dst[n++]='"'; }
|
| 24 |
+
else if (c == '\\') { dst[n++]='\\'; dst[n++]='\\'; }
|
| 25 |
+
else if (c == '\n') { dst[n++]='\\'; dst[n++]='n'; }
|
| 26 |
+
else if (c == '\r') { dst[n++]='\\'; dst[n++]='r'; }
|
| 27 |
+
else if (c == '\t') { dst[n++]='\\'; dst[n++]='t'; }
|
| 28 |
+
else { dst[n++]=(char)c; }
|
| 29 |
+
}
|
| 30 |
+
dst[n] = '\0';
|
| 31 |
+
return n;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/* Wide path → file:/// URI in UTF-8 */
|
| 35 |
+
static void path_to_uri(const wchar_t *path, char *uri, int cap) {
|
| 36 |
+
char narrow[MAX_PATH * 2];
|
| 37 |
+
WideCharToMultiByte(CP_UTF8, 0, path, -1, narrow, sizeof(narrow), NULL, NULL);
|
| 38 |
+
/* Replace backslashes */
|
| 39 |
+
for (char *p = narrow; *p; p++) if (*p == '\\') *p = '/';
|
| 40 |
+
snprintf(uri, (size_t)cap, "file:///%s", narrow);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* ── Minimal JSON parser helpers ─────────────────────────────── */
|
| 44 |
+
|
| 45 |
+
static const char *json_find_key(const char *json, const char *key) {
|
| 46 |
+
char needle[128];
|
| 47 |
+
snprintf(needle, sizeof(needle), "\"%s\"", key);
|
| 48 |
+
return strstr(json, needle);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
static int json_int_after_key(const char *json, const char *key, int def) {
|
| 52 |
+
const char *p = json_find_key(json, key);
|
| 53 |
+
if (!p) return def;
|
| 54 |
+
p += strlen(key) + 2;
|
| 55 |
+
while (*p == ' ' || *p == ':' || *p == '"') p++;
|
| 56 |
+
if (*p == '\0') return def;
|
| 57 |
+
return atoi(p);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
static bool json_str_after_key(const char *json, const char *key,
|
| 61 |
+
char *out, int cap) {
|
| 62 |
+
const char *p = json_find_key(json, key);
|
| 63 |
+
if (!p) return false;
|
| 64 |
+
p += strlen(key) + 2;
|
| 65 |
+
while (*p == ' ' || *p == ':') p++;
|
| 66 |
+
if (*p != '"') return false;
|
| 67 |
+
p++;
|
| 68 |
+
int n = 0;
|
| 69 |
+
while (*p && *p != '"' && n < cap - 1) {
|
| 70 |
+
if (*p == '\\' && *(p+1)) { p++; }
|
| 71 |
+
out[n++] = *p++;
|
| 72 |
+
}
|
| 73 |
+
out[n] = '\0';
|
| 74 |
+
return true;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* ── RPC framing ─────────────────────────────────────────────── */
|
| 78 |
+
|
| 79 |
+
struct LspClient {
|
| 80 |
+
HANDLE pipe_stdin; /* write to child stdin */
|
| 81 |
+
HANDLE pipe_stdout; /* read from child stdout */
|
| 82 |
+
PROCESS_INFORMATION pi;
|
| 83 |
+
bool alive;
|
| 84 |
+
|
| 85 |
+
int next_id;
|
| 86 |
+
lsp_diag_cb on_diag;
|
| 87 |
+
void *cb_ctx;
|
| 88 |
+
|
| 89 |
+
/* Read buffer for partial Content-Length frames */
|
| 90 |
+
char rbuf[1 << 20]; /* 1 MB */
|
| 91 |
+
int rbuf_len;
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
static SovResult lsp_send(LspClient *c, const char *body, int body_len) {
|
| 95 |
+
char header[64];
|
| 96 |
+
int hlen = snprintf(header, sizeof(header),
|
| 97 |
+
"Content-Length: %d\r\n\r\n", body_len);
|
| 98 |
+
DWORD written;
|
| 99 |
+
if (!WriteFile(c->pipe_stdin, header, (DWORD)hlen, &written, NULL))
|
| 100 |
+
return SOV_ERR_IO;
|
| 101 |
+
if (!WriteFile(c->pipe_stdin, body, (DWORD)body_len, &written, NULL))
|
| 102 |
+
return SOV_ERR_IO;
|
| 103 |
+
return SOV_OK;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/* Build a JSON-RPC request and send it */
|
| 107 |
+
static SovResult lsp_request(LspClient *c, const char *method,
|
| 108 |
+
const char *params_json) {
|
| 109 |
+
char body[1 << 18];
|
| 110 |
+
int n = snprintf(body, sizeof(body),
|
| 111 |
+
"{\"jsonrpc\":\"2.0\",\"id\":%d,\"method\":\"%s\",\"params\":%s}",
|
| 112 |
+
c->next_id++, method, params_json ? params_json : "null");
|
| 113 |
+
return lsp_send(c, body, n);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/* Build a JSON-RPC notification (no id) and send it */
|
| 117 |
+
static SovResult lsp_notify(LspClient *c, const char *method,
|
| 118 |
+
const char *params_json) {
|
| 119 |
+
char body[1 << 18];
|
| 120 |
+
int n = snprintf(body, sizeof(body),
|
| 121 |
+
"{\"jsonrpc\":\"2.0\",\"method\":\"%s\",\"params\":%s}",
|
| 122 |
+
method, params_json ? params_json : "{}");
|
| 123 |
+
return lsp_send(c, body, n);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/* ── Diagnostics parser ──────────────────────────────────────── */
|
| 127 |
+
|
| 128 |
+
static void parse_diagnostics(LspClient *c, const char *json) {
|
| 129 |
+
LspDiagList list;
|
| 130 |
+
memset(&list, 0, sizeof(list));
|
| 131 |
+
|
| 132 |
+
/* Extract URI */
|
| 133 |
+
char uri[MAX_PATH * 2] = {0};
|
| 134 |
+
json_str_after_key(json, "uri", uri, sizeof(uri));
|
| 135 |
+
/* Strip file:/// prefix */
|
| 136 |
+
const char *path = uri;
|
| 137 |
+
if (strncmp(path, "file:///", 8) == 0) path += 8;
|
| 138 |
+
MultiByteToWideChar(CP_UTF8, 0, path, -1, list.uri, MAX_PATH);
|
| 139 |
+
/* Restore backslashes on Windows */
|
| 140 |
+
for (wchar_t *p = list.uri; *p; p++) if (*p == L'/') *p = L'\\';
|
| 141 |
+
|
| 142 |
+
/* Walk diagnostics array */
|
| 143 |
+
const char *p = strstr(json, "\"diagnostics\"");
|
| 144 |
+
if (!p) goto done;
|
| 145 |
+
p = strchr(p, '[');
|
| 146 |
+
if (!p) goto done;
|
| 147 |
+
p++;
|
| 148 |
+
|
| 149 |
+
while (*p && list.count < LSP_MAX_DIAG) {
|
| 150 |
+
/* Find next object */
|
| 151 |
+
const char *obj = strchr(p, '{');
|
| 152 |
+
if (!obj) break;
|
| 153 |
+
/* Find matching } — naive single-level scan */
|
| 154 |
+
const char *end = obj + 1;
|
| 155 |
+
int depth = 1;
|
| 156 |
+
while (*end && depth > 0) {
|
| 157 |
+
if (*end == '{') depth++;
|
| 158 |
+
else if (*end == '}') depth--;
|
| 159 |
+
end++;
|
| 160 |
+
}
|
| 161 |
+
if (depth != 0) break;
|
| 162 |
+
|
| 163 |
+
/* Extract a diagnostic object [obj, end) */
|
| 164 |
+
char obj_buf[2048];
|
| 165 |
+
int obj_len = (int)(end - obj);
|
| 166 |
+
if (obj_len > (int)sizeof(obj_buf) - 1) obj_len = (int)sizeof(obj_buf) - 1;
|
| 167 |
+
memcpy(obj_buf, obj, (size_t)obj_len);
|
| 168 |
+
obj_buf[obj_len] = '\0';
|
| 169 |
+
|
| 170 |
+
LspDiagnostic *d = &list.entries[list.count];
|
| 171 |
+
|
| 172 |
+
/* line/character from "start" */
|
| 173 |
+
const char *rng = strstr(obj_buf, "\"range\"");
|
| 174 |
+
if (rng) {
|
| 175 |
+
const char *start = strstr(rng, "\"start\"");
|
| 176 |
+
if (start) {
|
| 177 |
+
d->line = (uint32_t)json_int_after_key(start, "line", 0);
|
| 178 |
+
d->col = (uint32_t)json_int_after_key(start, "character", 0);
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
d->severity = (uint32_t)json_int_after_key(obj_buf, "severity", 1);
|
| 182 |
+
json_str_after_key(obj_buf, "message", d->message, 512);
|
| 183 |
+
wcscpy_s(d->file, MAX_PATH, list.uri);
|
| 184 |
+
|
| 185 |
+
list.count++;
|
| 186 |
+
p = end;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
done:
|
| 190 |
+
if (c->on_diag) c->on_diag(&list, c->cb_ctx);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
/* ── Dispatch one complete JSON message ──────────────────────── */
|
| 194 |
+
|
| 195 |
+
static void dispatch_message(LspClient *c, const char *json) {
|
| 196 |
+
/* Only care about notifications right now */
|
| 197 |
+
const char *method_pos = strstr(json, "\"method\"");
|
| 198 |
+
if (!method_pos) return;
|
| 199 |
+
|
| 200 |
+
char method[128] = {0};
|
| 201 |
+
json_str_after_key(json, "method", method, sizeof(method));
|
| 202 |
+
|
| 203 |
+
if (strcmp(method, "textDocument/publishDiagnostics") == 0) {
|
| 204 |
+
parse_diagnostics(c, json);
|
| 205 |
+
}
|
| 206 |
+
/* Other notifications (window/logMessage, $/progress, etc.) ignored */
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
/* ── Public API ──────────────────────────────────────────────── */
|
| 210 |
+
|
| 211 |
+
SovResult lsp_client_start(LspClient **out, const wchar_t *server_cmd,
|
| 212 |
+
lsp_diag_cb on_diag, void *cb_ctx) {
|
| 213 |
+
LspClient *c = (LspClient *)calloc(1, sizeof(LspClient));
|
| 214 |
+
if (!c) return SOV_ERR_ALLOC;
|
| 215 |
+
c->on_diag = on_diag;
|
| 216 |
+
c->cb_ctx = cb_ctx;
|
| 217 |
+
c->next_id = 1;
|
| 218 |
+
c->alive = false;
|
| 219 |
+
|
| 220 |
+
SECURITY_ATTRIBUTES sa = { sizeof(sa), NULL, TRUE };
|
| 221 |
+
|
| 222 |
+
HANDLE child_stdin_rd, child_stdin_wr;
|
| 223 |
+
HANDLE child_stdout_rd, child_stdout_wr;
|
| 224 |
+
|
| 225 |
+
if (!CreatePipe(&child_stdin_rd, &child_stdin_wr, &sa, 0)) goto fail;
|
| 226 |
+
if (!CreatePipe(&child_stdout_rd, &child_stdout_wr, &sa, 0)) {
|
| 227 |
+
CloseHandle(child_stdin_rd); CloseHandle(child_stdin_wr); goto fail;
|
| 228 |
+
}
|
| 229 |
+
SetHandleInformation(child_stdin_wr, HANDLE_FLAG_INHERIT, 0);
|
| 230 |
+
SetHandleInformation(child_stdout_rd, HANDLE_FLAG_INHERIT, 0);
|
| 231 |
+
|
| 232 |
+
c->pipe_stdin = child_stdin_wr;
|
| 233 |
+
c->pipe_stdout = child_stdout_rd;
|
| 234 |
+
|
| 235 |
+
STARTUPINFOW si = {0};
|
| 236 |
+
si.cb = sizeof(si);
|
| 237 |
+
si.dwFlags = STARTF_USESTDHANDLES;
|
| 238 |
+
si.hStdInput = child_stdin_rd;
|
| 239 |
+
si.hStdOutput = child_stdout_wr;
|
| 240 |
+
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
| 241 |
+
|
| 242 |
+
wchar_t cmd[MAX_PATH + 64];
|
| 243 |
+
wcscpy_s(cmd, MAX_PATH + 64, server_cmd);
|
| 244 |
+
|
| 245 |
+
BOOL ok = CreateProcessW(NULL, cmd, NULL, NULL, TRUE,
|
| 246 |
+
CREATE_NO_WINDOW, NULL, NULL, &si, &c->pi);
|
| 247 |
+
|
| 248 |
+
CloseHandle(child_stdin_rd);
|
| 249 |
+
CloseHandle(child_stdout_wr);
|
| 250 |
+
|
| 251 |
+
if (!ok) goto fail;
|
| 252 |
+
|
| 253 |
+
c->alive = true;
|
| 254 |
+
*out = c;
|
| 255 |
+
return SOV_OK;
|
| 256 |
+
|
| 257 |
+
fail:
|
| 258 |
+
free(c);
|
| 259 |
+
return SOV_ERR_IO;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
SovResult lsp_client_initialize(LspClient *c, const wchar_t *root_path) {
|
| 263 |
+
if (!c->alive) return SOV_ERR_IO;
|
| 264 |
+
|
| 265 |
+
char root_uri[MAX_PATH * 3];
|
| 266 |
+
path_to_uri(root_path, root_uri, sizeof(root_uri));
|
| 267 |
+
|
| 268 |
+
char params[MAX_PATH * 3 + 256];
|
| 269 |
+
snprintf(params, sizeof(params),
|
| 270 |
+
"{\"processId\":%lu,"
|
| 271 |
+
"\"rootUri\":\"%s\","
|
| 272 |
+
"\"capabilities\":{"
|
| 273 |
+
"\"textDocument\":{"
|
| 274 |
+
"\"publishDiagnostics\":{\"relatedInformation\":false}"
|
| 275 |
+
"}"
|
| 276 |
+
"}}",
|
| 277 |
+
(unsigned long)GetCurrentProcessId(), root_uri);
|
| 278 |
+
|
| 279 |
+
SovResult r = lsp_request(c, "initialize", params);
|
| 280 |
+
if (r != SOV_OK) return r;
|
| 281 |
+
|
| 282 |
+
/* Send initialized after a tick (clangd expects response first,
|
| 283 |
+
* but we're async — send immediately, clangd handles it fine) */
|
| 284 |
+
return lsp_notify(c, "initialized", "{}");
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
SovResult lsp_client_open(LspClient *c, const wchar_t *path,
|
| 288 |
+
const char *text, size_t len) {
|
| 289 |
+
if (!c->alive) return SOV_ERR_IO;
|
| 290 |
+
|
| 291 |
+
char uri[MAX_PATH * 3];
|
| 292 |
+
path_to_uri(path, uri, sizeof(uri));
|
| 293 |
+
|
| 294 |
+
/* Detect language from extension */
|
| 295 |
+
const char *lang = "c";
|
| 296 |
+
const wchar_t *ext = wcsrchr(path, L'.');
|
| 297 |
+
if (ext) {
|
| 298 |
+
if (_wcsicmp(ext, L".cpp") == 0 || _wcsicmp(ext, L".hpp") == 0 ||
|
| 299 |
+
_wcsicmp(ext, L".cc") == 0 || _wcsicmp(ext, L".cxx") == 0)
|
| 300 |
+
lang = "cpp";
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/* Escape text into JSON string */
|
| 304 |
+
char *esc = (char *)malloc(len * 2 + 8);
|
| 305 |
+
if (!esc) return SOV_ERR_ALLOC;
|
| 306 |
+
json_escape(text, esc, (int)(len * 2 + 8));
|
| 307 |
+
|
| 308 |
+
char *params = (char *)malloc(len * 2 + 512);
|
| 309 |
+
if (!params) { free(esc); return SOV_ERR_ALLOC; }
|
| 310 |
+
snprintf(params, len * 2 + 512,
|
| 311 |
+
"{\"textDocument\":{"
|
| 312 |
+
"\"uri\":\"%s\","
|
| 313 |
+
"\"languageId\":\"%s\","
|
| 314 |
+
"\"version\":1,"
|
| 315 |
+
"\"text\":\"%s\""
|
| 316 |
+
"}}",
|
| 317 |
+
uri, lang, esc);
|
| 318 |
+
|
| 319 |
+
SovResult r = lsp_notify(c, "textDocument/didOpen", params);
|
| 320 |
+
free(esc);
|
| 321 |
+
free(params);
|
| 322 |
+
return r;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
SovResult lsp_client_change(LspClient *c, const wchar_t *path,
|
| 326 |
+
const char *text, size_t len, int version) {
|
| 327 |
+
if (!c->alive) return SOV_ERR_IO;
|
| 328 |
+
|
| 329 |
+
char uri[MAX_PATH * 3];
|
| 330 |
+
path_to_uri(path, uri, sizeof(uri));
|
| 331 |
+
|
| 332 |
+
char *esc = (char *)malloc(len * 2 + 8);
|
| 333 |
+
if (!esc) return SOV_ERR_ALLOC;
|
| 334 |
+
json_escape(text, esc, (int)(len * 2 + 8));
|
| 335 |
+
|
| 336 |
+
char *params = (char *)malloc(len * 2 + 512);
|
| 337 |
+
if (!params) { free(esc); return SOV_ERR_ALLOC; }
|
| 338 |
+
snprintf(params, len * 2 + 512,
|
| 339 |
+
"{\"textDocument\":{"
|
| 340 |
+
"\"uri\":\"%s\","
|
| 341 |
+
"\"version\":%d"
|
| 342 |
+
"},"
|
| 343 |
+
"\"contentChanges\":[{\"text\":\"%s\"}]}",
|
| 344 |
+
uri, version, esc);
|
| 345 |
+
|
| 346 |
+
SovResult r = lsp_notify(c, "textDocument/didChange", params);
|
| 347 |
+
free(esc);
|
| 348 |
+
free(params);
|
| 349 |
+
return r;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
void lsp_client_tick(LspClient *c) {
|
| 353 |
+
if (!c->alive) return;
|
| 354 |
+
|
| 355 |
+
/* Non-blocking drain of stdout pipe */
|
| 356 |
+
DWORD avail = 0;
|
| 357 |
+
while (PeekNamedPipe(c->pipe_stdout, NULL, 0, NULL, &avail, NULL) && avail > 0) {
|
| 358 |
+
int space = (int)sizeof(c->rbuf) - c->rbuf_len - 1;
|
| 359 |
+
if (space <= 0) { c->rbuf_len = 0; break; } /* overflow: reset */
|
| 360 |
+
DWORD to_read = (avail < (DWORD)space) ? avail : (DWORD)space;
|
| 361 |
+
DWORD nread = 0;
|
| 362 |
+
if (!ReadFile(c->pipe_stdout, c->rbuf + c->rbuf_len, to_read, &nread, NULL))
|
| 363 |
+
break;
|
| 364 |
+
c->rbuf_len += (int)nread;
|
| 365 |
+
c->rbuf[c->rbuf_len] = '\0';
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
/* Parse complete Content-Length frames */
|
| 369 |
+
char *p = c->rbuf;
|
| 370 |
+
int remaining = c->rbuf_len;
|
| 371 |
+
|
| 372 |
+
while (remaining > 0) {
|
| 373 |
+
/* Look for "Content-Length: N\r\n\r\n" */
|
| 374 |
+
/* strnstr not in MinGW — manual search with length guard */
|
| 375 |
+
char *hdr = NULL;
|
| 376 |
+
for (int si = 0; si <= remaining - 16; si++) {
|
| 377 |
+
if (memcmp(p + si, "Content-Length: ", 16) == 0) {
|
| 378 |
+
hdr = p + si; break;
|
| 379 |
+
}
|
| 380 |
+
}
|
| 381 |
+
if (!hdr) break;
|
| 382 |
+
|
| 383 |
+
int content_len = atoi(hdr + 16);
|
| 384 |
+
if (content_len <= 0) { remaining = 0; break; }
|
| 385 |
+
|
| 386 |
+
/* Find end of header (\r\n\r\n) */
|
| 387 |
+
char *body = strstr(hdr, "\r\n\r\n");
|
| 388 |
+
if (!body) break;
|
| 389 |
+
body += 4;
|
| 390 |
+
|
| 391 |
+
int consumed = (int)(body - p);
|
| 392 |
+
int body_avail = remaining - consumed;
|
| 393 |
+
if (body_avail < content_len) break; /* incomplete body, wait */
|
| 394 |
+
|
| 395 |
+
/* Process this message */
|
| 396 |
+
body[content_len] = '\0';
|
| 397 |
+
dispatch_message(c, body);
|
| 398 |
+
|
| 399 |
+
p = body + content_len;
|
| 400 |
+
remaining = remaining - consumed - content_len;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
/* Compact buffer */
|
| 404 |
+
if (remaining > 0 && p != c->rbuf)
|
| 405 |
+
memmove(c->rbuf, p, (size_t)remaining);
|
| 406 |
+
c->rbuf_len = remaining;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
void lsp_client_shutdown(LspClient *c) {
|
| 410 |
+
if (!c) return;
|
| 411 |
+
if (c->alive) {
|
| 412 |
+
lsp_request(c, "shutdown", NULL);
|
| 413 |
+
lsp_notify(c, "exit", NULL);
|
| 414 |
+
WaitForSingleObject(c->pi.hProcess, 2000);
|
| 415 |
+
TerminateProcess(c->pi.hProcess, 0);
|
| 416 |
+
CloseHandle(c->pi.hProcess);
|
| 417 |
+
CloseHandle(c->pi.hThread);
|
| 418 |
+
}
|
| 419 |
+
CloseHandle(c->pipe_stdin);
|
| 420 |
+
CloseHandle(c->pipe_stdout);
|
| 421 |
+
free(c);
|
| 422 |
+
}
|
ide/native/lsp/client.h
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_LSP_CLIENT_H
|
| 2 |
+
#define SOVEREIGN_LSP_CLIENT_H
|
| 3 |
+
|
| 4 |
+
#include <windows.h>
|
| 5 |
+
#include <stdbool.h>
|
| 6 |
+
#include <stdint.h>
|
| 7 |
+
#include "../core/errors.h"
|
| 8 |
+
|
| 9 |
+
typedef struct LspClient LspClient;
|
| 10 |
+
|
| 11 |
+
typedef struct LspDiagnostic {
|
| 12 |
+
wchar_t file[MAX_PATH];
|
| 13 |
+
uint32_t line; /* 0-based */
|
| 14 |
+
uint32_t col; /* 0-based */
|
| 15 |
+
uint32_t severity; /* 1=error 2=warning 3=info 4=hint */
|
| 16 |
+
char message[512];
|
| 17 |
+
} LspDiagnostic;
|
| 18 |
+
|
| 19 |
+
#define LSP_MAX_DIAG 256
|
| 20 |
+
|
| 21 |
+
typedef struct LspDiagList {
|
| 22 |
+
LspDiagnostic entries[LSP_MAX_DIAG];
|
| 23 |
+
int count;
|
| 24 |
+
wchar_t uri[MAX_PATH]; /* file these diags belong to */
|
| 25 |
+
} LspDiagList;
|
| 26 |
+
|
| 27 |
+
/* Called on UI thread after each publishDiagnostics notification */
|
| 28 |
+
typedef void (*lsp_diag_cb)(const LspDiagList *diags, void *ctx);
|
| 29 |
+
|
| 30 |
+
SovResult lsp_client_start(LspClient **out, const wchar_t *server_cmd,
|
| 31 |
+
lsp_diag_cb on_diag, void *cb_ctx);
|
| 32 |
+
SovResult lsp_client_initialize(LspClient *c, const wchar_t *root_path);
|
| 33 |
+
SovResult lsp_client_open(LspClient *c, const wchar_t *path,
|
| 34 |
+
const char *text, size_t len);
|
| 35 |
+
SovResult lsp_client_change(LspClient *c, const wchar_t *path,
|
| 36 |
+
const char *text, size_t len, int version);
|
| 37 |
+
void lsp_client_tick(LspClient *c); /* pump: call from message loop */
|
| 38 |
+
void lsp_client_shutdown(LspClient *c);
|
| 39 |
+
|
| 40 |
+
#endif
|
ide/native/main.c
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Entry Point
|
| 3 |
+
* Win32 wWinMain: init → window → message loop → shutdown.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
#include "platform/windows/application.h"
|
| 7 |
+
#include "platform/windows/shell.h"
|
| 8 |
+
#include "core/errors.h"
|
| 9 |
+
|
| 10 |
+
#define INITIAL_WIDTH 1280
|
| 11 |
+
#define INITIAL_HEIGHT 800
|
| 12 |
+
|
| 13 |
+
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrev, LPWSTR cmdLine, int nCmdShow) {
|
| 14 |
+
(void)hPrev; (void)cmdLine; (void)nCmdShow;
|
| 15 |
+
|
| 16 |
+
app_init(hInstance);
|
| 17 |
+
|
| 18 |
+
SovResult r = shell_create(hInstance, INITIAL_WIDTH, INITIAL_HEIGHT);
|
| 19 |
+
SOV_ASSERT(r == SOV_OK, "Failed to create shell window");
|
| 20 |
+
|
| 21 |
+
int exit_code = app_run();
|
| 22 |
+
|
| 23 |
+
app_shutdown();
|
| 24 |
+
return exit_code;
|
| 25 |
+
}
|
ide/native/platform/windows/application.c
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Win32 Application Entry
|
| 3 |
+
* Owns the message loop, DPI awareness, and top-level window creation.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
#include "application.h"
|
| 7 |
+
#include "window.h"
|
| 8 |
+
#include "../../core/events.h"
|
| 9 |
+
#include "../../core/arena.h"
|
| 10 |
+
#include "../../core/strings.h"
|
| 11 |
+
|
| 12 |
+
#include <shellscalingapi.h>
|
| 13 |
+
|
| 14 |
+
static struct {
|
| 15 |
+
HINSTANCE hinstance;
|
| 16 |
+
bool running;
|
| 17 |
+
Arena frame_arena;
|
| 18 |
+
} g_app;
|
| 19 |
+
|
| 20 |
+
static void app_set_dpi_awareness(void) {
|
| 21 |
+
SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
void app_init(HINSTANCE hinstance) {
|
| 25 |
+
g_app.hinstance = hinstance;
|
| 26 |
+
g_app.running = true;
|
| 27 |
+
|
| 28 |
+
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
| 29 |
+
app_set_dpi_awareness();
|
| 30 |
+
arena_init_default(&g_app.frame_arena);
|
| 31 |
+
strings_init();
|
| 32 |
+
events_init();
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
int app_run(void) {
|
| 36 |
+
MSG msg = {0};
|
| 37 |
+
while (g_app.running) {
|
| 38 |
+
while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) {
|
| 39 |
+
if (msg.message == WM_QUIT) {
|
| 40 |
+
g_app.running = false;
|
| 41 |
+
break;
|
| 42 |
+
}
|
| 43 |
+
TranslateMessage(&msg);
|
| 44 |
+
DispatchMessageW(&msg);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
arena_reset(&g_app.frame_arena);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
return (int)msg.wParam;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
void app_request_quit(void) {
|
| 54 |
+
PostQuitMessage(0);
|
| 55 |
+
g_app.running = false;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
void app_shutdown(void) {
|
| 59 |
+
strings_shutdown();
|
| 60 |
+
arena_destroy(&g_app.frame_arena);
|
| 61 |
+
CoUninitialize();
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
HINSTANCE app_get_hinstance(void) {
|
| 65 |
+
return g_app.hinstance;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
Arena *app_frame_arena(void) {
|
| 69 |
+
return &g_app.frame_arena;
|
| 70 |
+
}
|
ide/native/platform/windows/application.h
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_APPLICATION_H
|
| 2 |
+
#define SOVEREIGN_APPLICATION_H
|
| 3 |
+
|
| 4 |
+
#include <windows.h>
|
| 5 |
+
#include <stdbool.h>
|
| 6 |
+
#include "../../core/arena.h"
|
| 7 |
+
|
| 8 |
+
void app_init(HINSTANCE hinstance);
|
| 9 |
+
int app_run(void);
|
| 10 |
+
void app_request_quit(void);
|
| 11 |
+
void app_shutdown(void);
|
| 12 |
+
HINSTANCE app_get_hinstance(void);
|
| 13 |
+
Arena *app_frame_arena(void);
|
| 14 |
+
|
| 15 |
+
#endif /* SOVEREIGN_APPLICATION_H */
|
ide/native/platform/windows/shell.c
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Application Shell
|
| 3 |
+
* Top-level window + child panel HWNDs + layout engine.
|
| 4 |
+
* This replaces window.c as the orchestrator.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
#include "shell.h"
|
| 8 |
+
#include "application.h"
|
| 9 |
+
#include "../../ui/layout.h"
|
| 10 |
+
#include "../../ui/project_tree.h"
|
| 11 |
+
#include "../../ui/status_bar.h"
|
| 12 |
+
#include "../../ui/output_panel.h"
|
| 13 |
+
#include "../../editor/editor_view.h"
|
| 14 |
+
#include "../../editor/document.h"
|
| 15 |
+
#include "../../terminal/terminal_view.h"
|
| 16 |
+
#include "../../git/repository.h"
|
| 17 |
+
#include "../../build/cmake_runner.h"
|
| 18 |
+
#include "../../lsp/client.h"
|
| 19 |
+
#include "../../core/events.h"
|
| 20 |
+
#include <commdlg.h>
|
| 21 |
+
#include <shlobj.h>
|
| 22 |
+
|
| 23 |
+
#define TIMER_CARET 1
|
| 24 |
+
#define TIMER_PTY 2
|
| 25 |
+
#define TIMER_GIT 3
|
| 26 |
+
|
| 27 |
+
/* Child window IDs */
|
| 28 |
+
#define IDC_SIDEBAR 101
|
| 29 |
+
#define IDC_EDITOR 102
|
| 30 |
+
#define IDC_BOTTOM 103
|
| 31 |
+
#define IDC_STATUSBAR 104
|
| 32 |
+
|
| 33 |
+
#define SHELL_CLASS L"SovShell"
|
| 34 |
+
#define PANEL_CLASS L"SovPanel"
|
| 35 |
+
#define SHELL_TITLE L"Sovereign IDE"
|
| 36 |
+
|
| 37 |
+
static struct {
|
| 38 |
+
HWND frame;
|
| 39 |
+
HWND wnd_sidebar;
|
| 40 |
+
HWND wnd_editor;
|
| 41 |
+
HWND wnd_bottom; /* container only — no D2D on this one */
|
| 42 |
+
HWND wnd_output; /* OutputPanel lives here */
|
| 43 |
+
HWND wnd_terminal; /* TerminalView lives here */
|
| 44 |
+
HWND wnd_status;
|
| 45 |
+
|
| 46 |
+
Layout layout;
|
| 47 |
+
ProjectTree *tree;
|
| 48 |
+
EditorView *editor;
|
| 49 |
+
OutputPanel *output;
|
| 50 |
+
TerminalView *terminal;
|
| 51 |
+
StatusBar *status;
|
| 52 |
+
|
| 53 |
+
GitRepo git;
|
| 54 |
+
CmakeRunner *builder;
|
| 55 |
+
LspClient *lsp;
|
| 56 |
+
int lsp_version;
|
| 57 |
+
|
| 58 |
+
Document doc;
|
| 59 |
+
int client_w;
|
| 60 |
+
int client_h;
|
| 61 |
+
bool terminal_focused;
|
| 62 |
+
} g_shell;
|
| 63 |
+
|
| 64 |
+
/* -----------------------------------------------------------
|
| 65 |
+
* Build runner callbacks
|
| 66 |
+
* ----------------------------------------------------------- */
|
| 67 |
+
static void on_build_line(const char *text, bool is_err, void *ctx) {
|
| 68 |
+
(void)ctx; (void)is_err;
|
| 69 |
+
if (!g_shell.output) return;
|
| 70 |
+
/* Convert UTF-8 line to wide */
|
| 71 |
+
wchar_t wbuf[2048];
|
| 72 |
+
MultiByteToWideChar(CP_UTF8, 0, text, -1, wbuf, 2048);
|
| 73 |
+
output_panel_append(g_shell.output, wbuf);
|
| 74 |
+
output_panel_append(g_shell.output, L"\n");
|
| 75 |
+
/* Show output panel while building */
|
| 76 |
+
g_shell.terminal_focused = false;
|
| 77 |
+
ShowWindow(g_shell.wnd_output, SW_SHOW);
|
| 78 |
+
ShowWindow(g_shell.wnd_terminal, SW_HIDE);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
static void on_build_done(int exit_code, void *ctx) {
|
| 82 |
+
(void)ctx;
|
| 83 |
+
if (!g_shell.output) return;
|
| 84 |
+
wchar_t msg[64];
|
| 85 |
+
if (exit_code == 0)
|
| 86 |
+
swprintf_s(msg, 64, L"✔ Build succeeded\n");
|
| 87 |
+
else
|
| 88 |
+
swprintf_s(msg, 64, L"✘ Build failed (exit %d)\n", exit_code);
|
| 89 |
+
output_panel_append(g_shell.output, msg);
|
| 90 |
+
if (g_shell.status)
|
| 91 |
+
status_bar_set_message(g_shell.status,
|
| 92 |
+
exit_code == 0 ? L"Build OK" : L"Build FAILED");
|
| 93 |
+
InvalidateRect(g_shell.wnd_bottom, NULL, FALSE);
|
| 94 |
+
InvalidateRect(g_shell.wnd_status, NULL, FALSE);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
/* -----------------------------------------------------------
|
| 98 |
+
* LSP diagnostics callback
|
| 99 |
+
* ----------------------------------------------------------- */
|
| 100 |
+
static void on_lsp_diag(const LspDiagList *list, void *ctx) {
|
| 101 |
+
(void)ctx;
|
| 102 |
+
if (!g_shell.editor) return;
|
| 103 |
+
/* Only apply diags that match the current open file */
|
| 104 |
+
if (g_shell.doc.has_path &&
|
| 105 |
+
_wcsicmp(list->uri, g_shell.doc.path) == 0)
|
| 106 |
+
{
|
| 107 |
+
editor_view_set_diagnostics(g_shell.editor, list);
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/* -----------------------------------------------------------
|
| 112 |
+
* File open callback (from project tree click)
|
| 113 |
+
* ----------------------------------------------------------- */
|
| 114 |
+
static void on_open_file(const wchar_t *path, void *ctx) {
|
| 115 |
+
(void)ctx;
|
| 116 |
+
document_destroy(&g_shell.doc);
|
| 117 |
+
if (document_open(&g_shell.doc, path) == SOV_OK) {
|
| 118 |
+
editor_view_set_document(g_shell.editor, &g_shell.doc);
|
| 119 |
+
editor_view_set_diagnostics(g_shell.editor, NULL);
|
| 120 |
+
|
| 121 |
+
/* Notify LSP of the new file */
|
| 122 |
+
if (g_shell.lsp) {
|
| 123 |
+
char utf8[1 << 20];
|
| 124 |
+
size_t blen = buffer_length(g_shell.doc.buffer);
|
| 125 |
+
if (blen > sizeof(utf8) - 1) blen = sizeof(utf8) - 1;
|
| 126 |
+
buffer_read(g_shell.doc.buffer, 0, utf8, blen);
|
| 127 |
+
utf8[blen] = '\0';
|
| 128 |
+
g_shell.lsp_version = 1;
|
| 129 |
+
lsp_client_open(g_shell.lsp, path, utf8, blen);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/* extract filename for status bar */
|
| 133 |
+
const wchar_t *name = wcsrchr(path, L'\\');
|
| 134 |
+
status_bar_set_file(g_shell.status, name ? name + 1 : path, false);
|
| 135 |
+
|
| 136 |
+
output_panel_append(g_shell.output, L"Opened: ");
|
| 137 |
+
output_panel_append(g_shell.output, path);
|
| 138 |
+
output_panel_append(g_shell.output, L"\n");
|
| 139 |
+
} else {
|
| 140 |
+
output_panel_append(g_shell.output, L"Failed to open file\n");
|
| 141 |
+
}
|
| 142 |
+
InvalidateRect(g_shell.wnd_editor, NULL, FALSE);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/* -----------------------------------------------------------
|
| 146 |
+
* Reposition all child windows from layout
|
| 147 |
+
* ----------------------------------------------------------- */
|
| 148 |
+
static void apply_layout(void) {
|
| 149 |
+
PanelRect rects[PANEL_COUNT];
|
| 150 |
+
layout_compute(&g_shell.layout, g_shell.client_w, g_shell.client_h, rects);
|
| 151 |
+
|
| 152 |
+
SetWindowPos(g_shell.wnd_sidebar, NULL,
|
| 153 |
+
rects[PANEL_SIDEBAR].x, rects[PANEL_SIDEBAR].y,
|
| 154 |
+
rects[PANEL_SIDEBAR].w, rects[PANEL_SIDEBAR].h,
|
| 155 |
+
SWP_NOZORDER | SWP_NOACTIVATE);
|
| 156 |
+
|
| 157 |
+
SetWindowPos(g_shell.wnd_editor, NULL,
|
| 158 |
+
rects[PANEL_EDITOR].x, rects[PANEL_EDITOR].y,
|
| 159 |
+
rects[PANEL_EDITOR].w, rects[PANEL_EDITOR].h,
|
| 160 |
+
SWP_NOZORDER | SWP_NOACTIVATE);
|
| 161 |
+
|
| 162 |
+
SetWindowPos(g_shell.wnd_bottom, NULL,
|
| 163 |
+
rects[PANEL_BOTTOM].x, rects[PANEL_BOTTOM].y,
|
| 164 |
+
rects[PANEL_BOTTOM].w, rects[PANEL_BOTTOM].h,
|
| 165 |
+
SWP_NOZORDER | SWP_NOACTIVATE);
|
| 166 |
+
|
| 167 |
+
SetWindowPos(g_shell.wnd_status, NULL,
|
| 168 |
+
rects[PANEL_STATUSBAR].x, rects[PANEL_STATUSBAR].y,
|
| 169 |
+
rects[PANEL_STATUSBAR].w, rects[PANEL_STATUSBAR].h,
|
| 170 |
+
SWP_NOZORDER | SWP_NOACTIVATE);
|
| 171 |
+
|
| 172 |
+
if (g_shell.editor)
|
| 173 |
+
editor_view_resize(g_shell.editor,
|
| 174 |
+
(uint32_t)rects[PANEL_EDITOR].w, (uint32_t)rects[PANEL_EDITOR].h);
|
| 175 |
+
if (g_shell.tree)
|
| 176 |
+
project_tree_resize(g_shell.tree,
|
| 177 |
+
rects[PANEL_SIDEBAR].w, rects[PANEL_SIDEBAR].h);
|
| 178 |
+
/* Resize output+terminal sub-windows to fill wnd_bottom */
|
| 179 |
+
if (g_shell.wnd_output)
|
| 180 |
+
SetWindowPos(g_shell.wnd_output, NULL, 0, 0,
|
| 181 |
+
rects[PANEL_BOTTOM].w, rects[PANEL_BOTTOM].h, SWP_NOZORDER | SWP_NOACTIVATE);
|
| 182 |
+
if (g_shell.wnd_terminal)
|
| 183 |
+
SetWindowPos(g_shell.wnd_terminal, NULL, 0, 0,
|
| 184 |
+
rects[PANEL_BOTTOM].w, rects[PANEL_BOTTOM].h, SWP_NOZORDER | SWP_NOACTIVATE);
|
| 185 |
+
if (g_shell.output)
|
| 186 |
+
output_panel_resize(g_shell.output,
|
| 187 |
+
rects[PANEL_BOTTOM].w, rects[PANEL_BOTTOM].h);
|
| 188 |
+
if (g_shell.terminal)
|
| 189 |
+
terminal_view_resize(g_shell.terminal,
|
| 190 |
+
rects[PANEL_BOTTOM].w, rects[PANEL_BOTTOM].h);
|
| 191 |
+
if (g_shell.status)
|
| 192 |
+
status_bar_resize(g_shell.status,
|
| 193 |
+
rects[PANEL_STATUSBAR].w, rects[PANEL_STATUSBAR].h);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/* -----------------------------------------------------------
|
| 197 |
+
* Panel child window proc (sidebar / editor / bottom / status)
|
| 198 |
+
* ----------------------------------------------------------- */
|
| 199 |
+
static LRESULT CALLBACK panel_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
| 200 |
+
switch (msg) {
|
| 201 |
+
case WM_PAINT: {
|
| 202 |
+
PAINTSTRUCT ps;
|
| 203 |
+
BeginPaint(hwnd, &ps);
|
| 204 |
+
if (hwnd == g_shell.wnd_sidebar && g_shell.tree)
|
| 205 |
+
project_tree_paint(g_shell.tree);
|
| 206 |
+
else if (hwnd == g_shell.wnd_editor && g_shell.editor) {
|
| 207 |
+
RECT rc; GetClientRect(hwnd, &rc);
|
| 208 |
+
editor_view_paint(g_shell.editor, rc);
|
| 209 |
+
}
|
| 210 |
+
else if (hwnd == g_shell.wnd_output && g_shell.output)
|
| 211 |
+
output_panel_paint(g_shell.output);
|
| 212 |
+
else if (hwnd == g_shell.wnd_terminal && g_shell.terminal)
|
| 213 |
+
terminal_view_paint(g_shell.terminal);
|
| 214 |
+
else if (hwnd == g_shell.wnd_status && g_shell.status)
|
| 215 |
+
status_bar_paint(g_shell.status);
|
| 216 |
+
EndPaint(hwnd, &ps);
|
| 217 |
+
return 0;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
case WM_LBUTTONDOWN: {
|
| 221 |
+
int x = (int)(short)LOWORD(lp), y = (int)(short)HIWORD(lp);
|
| 222 |
+
if (hwnd == g_shell.wnd_sidebar && g_shell.tree)
|
| 223 |
+
project_tree_mouse_down(g_shell.tree, x, y);
|
| 224 |
+
else if (hwnd == g_shell.wnd_editor && g_shell.editor) {
|
| 225 |
+
SetFocus(hwnd);
|
| 226 |
+
g_shell.terminal_focused = false;
|
| 227 |
+
editor_view_mouse_down(g_shell.editor, x, y);
|
| 228 |
+
}
|
| 229 |
+
else if (hwnd == g_shell.wnd_terminal || hwnd == g_shell.wnd_output) {
|
| 230 |
+
SetFocus(hwnd);
|
| 231 |
+
g_shell.terminal_focused = (hwnd == g_shell.wnd_terminal);
|
| 232 |
+
}
|
| 233 |
+
return 0;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
case WM_MOUSEMOVE: {
|
| 237 |
+
if (hwnd == g_shell.wnd_editor && g_shell.editor) {
|
| 238 |
+
bool btn = (wp & MK_LBUTTON) != 0;
|
| 239 |
+
editor_view_mouse_move(g_shell.editor,
|
| 240 |
+
(int)(short)LOWORD(lp), (int)(short)HIWORD(lp), btn);
|
| 241 |
+
}
|
| 242 |
+
return 0;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
case WM_KEYDOWN:
|
| 246 |
+
case WM_SYSKEYDOWN: {
|
| 247 |
+
if (hwnd == g_shell.wnd_terminal && g_shell.terminal) {
|
| 248 |
+
UINT mods = 0;
|
| 249 |
+
if (GetKeyState(VK_CONTROL) & 0x8000) mods |= 2;
|
| 250 |
+
terminal_view_key_down(g_shell.terminal, (UINT)wp, mods);
|
| 251 |
+
return 0;
|
| 252 |
+
}
|
| 253 |
+
if (hwnd == g_shell.wnd_editor && g_shell.editor) {
|
| 254 |
+
UINT mods = 0;
|
| 255 |
+
if (GetKeyState(VK_SHIFT) & 0x8000) mods |= 1;
|
| 256 |
+
if (GetKeyState(VK_CONTROL) & 0x8000) mods |= 2;
|
| 257 |
+
if (GetKeyState(VK_MENU) & 0x8000) mods |= 4;
|
| 258 |
+
editor_view_key_down(g_shell.editor, (UINT)wp, mods);
|
| 259 |
+
|
| 260 |
+
/* Update status bar line/col after navigation */
|
| 261 |
+
if (g_shell.status && g_shell.doc.buffer) {
|
| 262 |
+
size_t off = editor_view_caret_offset(g_shell.editor);
|
| 263 |
+
char tmp[1];
|
| 264 |
+
/* count lines up to offset */
|
| 265 |
+
size_t line = 1, col = 1;
|
| 266 |
+
for (size_t i = 0; i < off; i++) {
|
| 267 |
+
size_t n = buffer_read(g_shell.doc.buffer, i, tmp, 1);
|
| 268 |
+
if (n && tmp[0] == '\n') { line++; col = 1; } else col++;
|
| 269 |
+
}
|
| 270 |
+
status_bar_set_pos(g_shell.status, (int)line, (int)col);
|
| 271 |
+
status_bar_set_file(g_shell.status,
|
| 272 |
+
g_shell.doc.has_path ? wcsrchr(g_shell.doc.path, L'\\') + 1 : L"untitled",
|
| 273 |
+
document_is_dirty(&g_shell.doc));
|
| 274 |
+
}
|
| 275 |
+
} else if (hwnd == g_shell.wnd_sidebar && g_shell.tree) {
|
| 276 |
+
project_tree_key_down(g_shell.tree, (UINT)wp);
|
| 277 |
+
}
|
| 278 |
+
return 0;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
case WM_CHAR: {
|
| 282 |
+
if (hwnd == g_shell.wnd_terminal && g_shell.terminal) {
|
| 283 |
+
terminal_view_char(g_shell.terminal, (wchar_t)wp);
|
| 284 |
+
} else if (hwnd == g_shell.wnd_editor && g_shell.editor) {
|
| 285 |
+
editor_view_char(g_shell.editor, (wchar_t)wp);
|
| 286 |
+
/* Notify LSP of change */
|
| 287 |
+
if (g_shell.lsp && g_shell.doc.has_path && g_shell.doc.buffer) {
|
| 288 |
+
char utf8[1 << 20];
|
| 289 |
+
size_t blen = buffer_length(g_shell.doc.buffer);
|
| 290 |
+
if (blen < sizeof(utf8)) {
|
| 291 |
+
buffer_read(g_shell.doc.buffer, 0, utf8, blen);
|
| 292 |
+
utf8[blen] = '\0';
|
| 293 |
+
lsp_client_change(g_shell.lsp, g_shell.doc.path,
|
| 294 |
+
utf8, blen, ++g_shell.lsp_version);
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
return 0;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
case WM_MOUSEWHEEL: {
|
| 302 |
+
if (hwnd == g_shell.wnd_bottom && g_shell.output)
|
| 303 |
+
output_panel_scroll(g_shell.output, GET_WHEEL_DELTA_WPARAM(wp) / 40);
|
| 304 |
+
return 0;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
case WM_TIMER:
|
| 308 |
+
if (wp == TIMER_PTY && hwnd == g_shell.wnd_terminal) {
|
| 309 |
+
terminal_view_tick(g_shell.terminal);
|
| 310 |
+
InvalidateRect(g_shell.wnd_terminal, NULL, FALSE);
|
| 311 |
+
} else {
|
| 312 |
+
InvalidateRect(hwnd, NULL, FALSE);
|
| 313 |
+
}
|
| 314 |
+
return 0;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
return DefWindowProcW(hwnd, msg, wp, lp);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
/* -----------------------------------------------------------
|
| 321 |
+
* Frame window proc (top-level)
|
| 322 |
+
* ----------------------------------------------------------- */
|
| 323 |
+
static LRESULT CALLBACK shell_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
| 324 |
+
switch (msg) {
|
| 325 |
+
|
| 326 |
+
case WM_CREATE: {
|
| 327 |
+
layout_init(&g_shell.layout);
|
| 328 |
+
|
| 329 |
+
/* Measure initial client area so D2D render targets get real dimensions */
|
| 330 |
+
RECT cr;
|
| 331 |
+
GetClientRect(hwnd, &cr);
|
| 332 |
+
g_shell.client_w = cr.right - cr.left;
|
| 333 |
+
g_shell.client_h = cr.bottom - cr.top;
|
| 334 |
+
if (g_shell.client_w < 1) g_shell.client_w = 1280;
|
| 335 |
+
if (g_shell.client_h < 1) g_shell.client_h = 800;
|
| 336 |
+
|
| 337 |
+
/* Compute layout so child panels get non-zero sizes */
|
| 338 |
+
PanelRect rects[PANEL_COUNT];
|
| 339 |
+
layout_compute(&g_shell.layout, g_shell.client_w, g_shell.client_h, rects);
|
| 340 |
+
|
| 341 |
+
/* Create child panels at their real positions */
|
| 342 |
+
HINSTANCE hi = app_get_hinstance();
|
| 343 |
+
g_shell.wnd_sidebar = CreateWindowExW(0, PANEL_CLASS, NULL,
|
| 344 |
+
WS_CHILD | WS_VISIBLE,
|
| 345 |
+
rects[PANEL_SIDEBAR].x, rects[PANEL_SIDEBAR].y,
|
| 346 |
+
rects[PANEL_SIDEBAR].w, rects[PANEL_SIDEBAR].h,
|
| 347 |
+
hwnd, (HMENU)IDC_SIDEBAR, hi, NULL);
|
| 348 |
+
g_shell.wnd_editor = CreateWindowExW(0, PANEL_CLASS, NULL,
|
| 349 |
+
WS_CHILD | WS_VISIBLE,
|
| 350 |
+
rects[PANEL_EDITOR].x, rects[PANEL_EDITOR].y,
|
| 351 |
+
rects[PANEL_EDITOR].w, rects[PANEL_EDITOR].h,
|
| 352 |
+
hwnd, (HMENU)IDC_EDITOR, hi, NULL);
|
| 353 |
+
g_shell.wnd_bottom = CreateWindowExW(0, PANEL_CLASS, NULL,
|
| 354 |
+
WS_CHILD | WS_VISIBLE,
|
| 355 |
+
rects[PANEL_BOTTOM].x, rects[PANEL_BOTTOM].y,
|
| 356 |
+
rects[PANEL_BOTTOM].w, rects[PANEL_BOTTOM].h,
|
| 357 |
+
hwnd, (HMENU)IDC_BOTTOM, hi, NULL);
|
| 358 |
+
g_shell.wnd_status = CreateWindowExW(0, PANEL_CLASS, NULL,
|
| 359 |
+
WS_CHILD | WS_VISIBLE,
|
| 360 |
+
rects[PANEL_STATUSBAR].x, rects[PANEL_STATUSBAR].y,
|
| 361 |
+
rects[PANEL_STATUSBAR].w, rects[PANEL_STATUSBAR].h,
|
| 362 |
+
hwnd, (HMENU)IDC_STATUSBAR, hi, NULL);
|
| 363 |
+
|
| 364 |
+
/* Output and terminal need separate HWNDs (each owns one D2D RT) */
|
| 365 |
+
int bw = rects[PANEL_BOTTOM].w;
|
| 366 |
+
int bh = rects[PANEL_BOTTOM].h;
|
| 367 |
+
g_shell.wnd_output = CreateWindowExW(0, PANEL_CLASS, NULL,
|
| 368 |
+
WS_CHILD | WS_VISIBLE, 0, 0, bw, bh,
|
| 369 |
+
g_shell.wnd_bottom, NULL, hi, NULL);
|
| 370 |
+
g_shell.wnd_terminal = CreateWindowExW(0, PANEL_CLASS, NULL,
|
| 371 |
+
WS_CHILD, 0, 0, bw, bh,
|
| 372 |
+
g_shell.wnd_bottom, NULL, hi, NULL);
|
| 373 |
+
|
| 374 |
+
/* Init subsystems — each on its own HWND */
|
| 375 |
+
#define DBGINIT(call, name) do { \
|
| 376 |
+
SovResult _r = (call); \
|
| 377 |
+
if (_r != SOV_OK) MessageBoxW(NULL, L##name L" FAILED", L"Init Error", MB_OK); \
|
| 378 |
+
} while(0)
|
| 379 |
+
project_tree_create(&g_shell.tree, g_shell.wnd_sidebar);
|
| 380 |
+
editor_view_create(&g_shell.editor, g_shell.wnd_editor);
|
| 381 |
+
output_panel_create(&g_shell.output, g_shell.wnd_output);
|
| 382 |
+
terminal_view_create(&g_shell.terminal, g_shell.wnd_terminal);
|
| 383 |
+
status_bar_create(&g_shell.status, g_shell.wnd_status);
|
| 384 |
+
#undef DBGINIT
|
| 385 |
+
g_shell.terminal_focused = false; /* default: show output panel */
|
| 386 |
+
/* wnd_output visible, wnd_terminal hidden until user toggles */
|
| 387 |
+
ShowWindow(g_shell.wnd_output, SW_SHOW);
|
| 388 |
+
ShowWindow(g_shell.wnd_terminal, SW_HIDE);
|
| 389 |
+
|
| 390 |
+
/* Wire callbacks */
|
| 391 |
+
if (g_shell.tree) {
|
| 392 |
+
project_tree_set_callback(g_shell.tree, on_open_file, NULL);
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
/* Default document */
|
| 396 |
+
document_new(&g_shell.doc);
|
| 397 |
+
buffer_insert(g_shell.doc.buffer, 0,
|
| 398 |
+
"#include <stdio.h>\n\nint main(void) {\n printf(\"Sovereign IDE\\n\");\n return 0;\n}\n",
|
| 399 |
+
88);
|
| 400 |
+
buffer_mark_clean(g_shell.doc.buffer);
|
| 401 |
+
if (g_shell.editor)
|
| 402 |
+
editor_view_set_document(g_shell.editor, &g_shell.doc);
|
| 403 |
+
|
| 404 |
+
/* Open current working directory in project tree */
|
| 405 |
+
wchar_t cwd[MAX_PATH];
|
| 406 |
+
GetCurrentDirectoryW(MAX_PATH, cwd);
|
| 407 |
+
if (g_shell.tree)
|
| 408 |
+
project_tree_open(g_shell.tree, cwd);
|
| 409 |
+
|
| 410 |
+
if (g_shell.status) {
|
| 411 |
+
status_bar_set_file(g_shell.status, L"main.c", false);
|
| 412 |
+
status_bar_set_pos(g_shell.status, 1, 1);
|
| 413 |
+
status_bar_set_branch(g_shell.status, L"master");
|
| 414 |
+
status_bar_set_message(g_shell.status, L"GCC 13.2");
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
/* Output welcome */
|
| 418 |
+
if (g_shell.output) {
|
| 419 |
+
output_panel_append(g_shell.output, L"Sovereign IDE ready.\n");
|
| 420 |
+
output_panel_append(g_shell.output, L"Project: ");
|
| 421 |
+
output_panel_append(g_shell.output, cwd);
|
| 422 |
+
output_panel_append(g_shell.output, L"\n");
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
/* Git branch detection */
|
| 426 |
+
if (git_repo_open(&g_shell.git, cwd) == SOV_OK && g_shell.status) {
|
| 427 |
+
wchar_t branch_disp[136];
|
| 428 |
+
swprintf_s(branch_disp, 136, L"%s%s",
|
| 429 |
+
g_shell.git.detached ? L"detached:" : L"",
|
| 430 |
+
g_shell.git.branch);
|
| 431 |
+
status_bar_set_branch(g_shell.status, branch_disp);
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
/* CMake build runner */
|
| 435 |
+
wchar_t build_dir[MAX_PATH];
|
| 436 |
+
swprintf_s(build_dir, MAX_PATH, L"%s\\build", cwd);
|
| 437 |
+
cmake_runner_create(&g_shell.builder, build_dir,
|
| 438 |
+
on_build_line, on_build_done, NULL);
|
| 439 |
+
|
| 440 |
+
/* Start LSP server (clangd must be on PATH) */
|
| 441 |
+
g_shell.lsp = NULL;
|
| 442 |
+
g_shell.lsp_version = 1;
|
| 443 |
+
if (lsp_client_start(&g_shell.lsp, L"clangd",
|
| 444 |
+
on_lsp_diag, NULL) == SOV_OK) {
|
| 445 |
+
lsp_client_initialize(g_shell.lsp, cwd);
|
| 446 |
+
output_panel_append(g_shell.output, L"LSP: clangd started.\n");
|
| 447 |
+
} else {
|
| 448 |
+
output_panel_append(g_shell.output, L"LSP: clangd not found — install LLVM.\n");
|
| 449 |
+
g_shell.lsp = NULL;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
SetTimer(g_shell.wnd_editor, TIMER_CARET, 530, NULL);
|
| 453 |
+
SetTimer(g_shell.wnd_terminal, TIMER_PTY, 16, NULL); /* ~60fps PTY poll */
|
| 454 |
+
SetTimer(hwnd, TIMER_GIT, 3000, NULL); /* git refresh every 3s */
|
| 455 |
+
return 0;
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
case WM_SIZE: {
|
| 459 |
+
g_shell.client_w = (int)(short)LOWORD(lp);
|
| 460 |
+
g_shell.client_h = (int)(short)HIWORD(lp);
|
| 461 |
+
apply_layout();
|
| 462 |
+
return 0;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
case WM_MOUSEMOVE: {
|
| 466 |
+
int mx = (int)(short)LOWORD(lp);
|
| 467 |
+
int my = (int)(short)HIWORD(lp);
|
| 468 |
+
if (g_shell.layout.dragging) {
|
| 469 |
+
layout_update_drag(&g_shell.layout, mx, my);
|
| 470 |
+
apply_layout();
|
| 471 |
+
return 0;
|
| 472 |
+
}
|
| 473 |
+
int spl = layout_hit_splitter(&g_shell.layout, g_shell.client_w, g_shell.client_h, mx, my);
|
| 474 |
+
if (spl == 0 || spl == 1)
|
| 475 |
+
SetCursor(LoadCursorW(NULL, (LPCWSTR)IDC_SIZEWE));
|
| 476 |
+
else if (spl == 2)
|
| 477 |
+
SetCursor(LoadCursorW(NULL, (LPCWSTR)IDC_SIZENS));
|
| 478 |
+
else
|
| 479 |
+
SetCursor(LoadCursorW(NULL, (LPCWSTR)IDC_ARROW));
|
| 480 |
+
return 0;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
case WM_LBUTTONDOWN: {
|
| 484 |
+
int mx = (int)(short)LOWORD(lp);
|
| 485 |
+
int my = (int)(short)HIWORD(lp);
|
| 486 |
+
int spl = layout_hit_splitter(&g_shell.layout, g_shell.client_w, g_shell.client_h, mx, my);
|
| 487 |
+
if (spl >= 0) {
|
| 488 |
+
layout_begin_drag(&g_shell.layout, spl, mx, my);
|
| 489 |
+
SetCapture(hwnd);
|
| 490 |
+
}
|
| 491 |
+
return 0;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
case WM_LBUTTONUP:
|
| 495 |
+
layout_end_drag(&g_shell.layout);
|
| 496 |
+
ReleaseCapture();
|
| 497 |
+
return 0;
|
| 498 |
+
|
| 499 |
+
case WM_COMMAND: {
|
| 500 |
+
switch (LOWORD(wp)) {
|
| 501 |
+
case 1001: /* File > Open Folder */
|
| 502 |
+
{
|
| 503 |
+
/* Use SHBrowseForFolder for folder selection */
|
| 504 |
+
BROWSEINFOW bi = {0};
|
| 505 |
+
bi.hwndOwner = hwnd;
|
| 506 |
+
bi.lpszTitle = L"Open Project Folder";
|
| 507 |
+
bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
|
| 508 |
+
LPITEMIDLIST pidl = SHBrowseForFolderW(&bi);
|
| 509 |
+
if (pidl) {
|
| 510 |
+
wchar_t path[MAX_PATH];
|
| 511 |
+
SHGetPathFromIDListW(pidl, path);
|
| 512 |
+
CoTaskMemFree(pidl);
|
| 513 |
+
if (g_shell.tree) project_tree_open(g_shell.tree, path);
|
| 514 |
+
output_panel_append(g_shell.output, L"Opened folder: ");
|
| 515 |
+
output_panel_append(g_shell.output, path);
|
| 516 |
+
output_panel_append(g_shell.output, L"\n");
|
| 517 |
+
}
|
| 518 |
+
break;
|
| 519 |
+
}
|
| 520 |
+
case 1002: /* File > Open File */
|
| 521 |
+
{
|
| 522 |
+
OPENFILENAMEW ofn = {0};
|
| 523 |
+
wchar_t file[MAX_PATH] = {0};
|
| 524 |
+
ofn.lStructSize = sizeof(ofn);
|
| 525 |
+
ofn.hwndOwner = hwnd;
|
| 526 |
+
ofn.lpstrFile = file;
|
| 527 |
+
ofn.nMaxFile = MAX_PATH;
|
| 528 |
+
ofn.lpstrFilter = L"C/C++ Files\0*.c;*.cpp;*.h;*.hpp\0All Files\0*.*\0";
|
| 529 |
+
ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
|
| 530 |
+
if (GetOpenFileNameW(&ofn)) {
|
| 531 |
+
on_open_file(file, NULL);
|
| 532 |
+
}
|
| 533 |
+
break;
|
| 534 |
+
}
|
| 535 |
+
case 1003: /* File > Save */
|
| 536 |
+
if (g_shell.doc.has_path) {
|
| 537 |
+
document_save(&g_shell.doc);
|
| 538 |
+
output_panel_append(g_shell.output, L"Saved.\n");
|
| 539 |
+
}
|
| 540 |
+
break;
|
| 541 |
+
case 1010: /* View > Toggle Sidebar */
|
| 542 |
+
g_shell.layout.sidebar_visible = !g_shell.layout.sidebar_visible;
|
| 543 |
+
apply_layout();
|
| 544 |
+
break;
|
| 545 |
+
case 1011: /* View > Toggle Bottom */
|
| 546 |
+
g_shell.layout.bottom_visible = !g_shell.layout.bottom_visible;
|
| 547 |
+
apply_layout();
|
| 548 |
+
break;
|
| 549 |
+
case 1020: /* Run > Build (Ctrl+B) */
|
| 550 |
+
if (g_shell.builder && !cmake_runner_is_running(g_shell.builder)) {
|
| 551 |
+
output_panel_clear(g_shell.output);
|
| 552 |
+
output_panel_append(g_shell.output, L"Building...\n");
|
| 553 |
+
g_shell.terminal_focused = false;
|
| 554 |
+
ShowWindow(g_shell.wnd_output, SW_SHOW);
|
| 555 |
+
ShowWindow(g_shell.wnd_terminal, SW_HIDE);
|
| 556 |
+
if (cmake_runner_build(g_shell.builder) != SOV_OK)
|
| 557 |
+
output_panel_append(g_shell.output, L"cmake not found — is it on PATH?\n");
|
| 558 |
+
}
|
| 559 |
+
break;
|
| 560 |
+
case 1021: /* View > Toggle Terminal/Output */
|
| 561 |
+
g_shell.terminal_focused = !g_shell.terminal_focused;
|
| 562 |
+
ShowWindow(g_shell.wnd_output, g_shell.terminal_focused ? SW_HIDE : SW_SHOW);
|
| 563 |
+
ShowWindow(g_shell.wnd_terminal, g_shell.terminal_focused ? SW_SHOW : SW_HIDE);
|
| 564 |
+
break;
|
| 565 |
+
case 9999: /* File > Exit */
|
| 566 |
+
PostMessageW(hwnd, WM_CLOSE, 0, 0);
|
| 567 |
+
break;
|
| 568 |
+
}
|
| 569 |
+
return 0;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
case WM_TIMER: {
|
| 573 |
+
if (wp == TIMER_GIT) {
|
| 574 |
+
if (git_repo_refresh(&g_shell.git) == SOV_OK && g_shell.status) {
|
| 575 |
+
wchar_t branch_disp[136];
|
| 576 |
+
swprintf_s(branch_disp, 136, L"%s%s",
|
| 577 |
+
g_shell.git.detached ? L"detached:" : L"",
|
| 578 |
+
g_shell.git.branch);
|
| 579 |
+
status_bar_set_branch(g_shell.status, branch_disp);
|
| 580 |
+
InvalidateRect(g_shell.wnd_status, NULL, FALSE);
|
| 581 |
+
}
|
| 582 |
+
if (g_shell.builder) cmake_runner_tick(g_shell.builder);
|
| 583 |
+
if (g_shell.lsp) lsp_client_tick(g_shell.lsp);
|
| 584 |
+
}
|
| 585 |
+
return 0;
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
case WM_DPICHANGED: {
|
| 589 |
+
RECT *r = (RECT *)lp;
|
| 590 |
+
SetWindowPos(hwnd, NULL, r->left, r->top,
|
| 591 |
+
r->right - r->left, r->bottom - r->top,
|
| 592 |
+
SWP_NOZORDER | SWP_NOACTIVATE);
|
| 593 |
+
return 0;
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
case WM_CLOSE:
|
| 597 |
+
KillTimer(hwnd, TIMER_GIT);
|
| 598 |
+
KillTimer(g_shell.wnd_editor, TIMER_CARET);
|
| 599 |
+
KillTimer(g_shell.wnd_terminal, TIMER_PTY);
|
| 600 |
+
if (g_shell.lsp) { lsp_client_shutdown(g_shell.lsp); g_shell.lsp = NULL; }
|
| 601 |
+
if (g_shell.builder) { cmake_runner_destroy(g_shell.builder); g_shell.builder = NULL; }
|
| 602 |
+
if (g_shell.editor) { editor_view_destroy(g_shell.editor); g_shell.editor = NULL; }
|
| 603 |
+
if (g_shell.tree) { project_tree_destroy(g_shell.tree); g_shell.tree = NULL; }
|
| 604 |
+
if (g_shell.terminal) { terminal_view_destroy(g_shell.terminal); g_shell.terminal = NULL; }
|
| 605 |
+
if (g_shell.output) { output_panel_destroy(g_shell.output); g_shell.output = NULL; }
|
| 606 |
+
if (g_shell.status) { status_bar_destroy(g_shell.status); g_shell.status = NULL; }
|
| 607 |
+
document_destroy(&g_shell.doc);
|
| 608 |
+
DestroyWindow(hwnd);
|
| 609 |
+
return 0;
|
| 610 |
+
|
| 611 |
+
case WM_DESTROY:
|
| 612 |
+
app_request_quit();
|
| 613 |
+
PostQuitMessage(0);
|
| 614 |
+
return 0;
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
return DefWindowProcW(hwnd, msg, wp, lp);
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
/* Build the native menu */
|
| 621 |
+
static void build_menu(HWND hwnd) {
|
| 622 |
+
HMENU bar = CreateMenu();
|
| 623 |
+
HMENU file = CreatePopupMenu();
|
| 624 |
+
HMENU view = CreatePopupMenu();
|
| 625 |
+
HMENU run = CreatePopupMenu();
|
| 626 |
+
|
| 627 |
+
AppendMenuW(file, MF_STRING, 1001, L"Open &Folder…\tCtrl+Shift+O");
|
| 628 |
+
AppendMenuW(file, MF_STRING, 1002, L"Open &File…\tCtrl+O");
|
| 629 |
+
AppendMenuW(file, MF_SEPARATOR, 0, NULL);
|
| 630 |
+
AppendMenuW(file, MF_STRING, 1003, L"&Save\tCtrl+S");
|
| 631 |
+
AppendMenuW(file, MF_SEPARATOR, 0, NULL);
|
| 632 |
+
AppendMenuW(file, MF_STRING, 9999, L"E&xit");
|
| 633 |
+
|
| 634 |
+
AppendMenuW(view, MF_STRING, 1010, L"Toggle &Sidebar\tCtrl+\\");
|
| 635 |
+
AppendMenuW(view, MF_STRING, 1011, L"Toggle &Output\tCtrl+J");
|
| 636 |
+
AppendMenuW(view, MF_STRING, 1021, L"Toggle &Terminal\tCtrl+`");
|
| 637 |
+
|
| 638 |
+
AppendMenuW(run, MF_STRING, 1020, L"&Build\tCtrl+B");
|
| 639 |
+
|
| 640 |
+
AppendMenuW(bar, MF_POPUP, (UINT_PTR)file, L"&File");
|
| 641 |
+
AppendMenuW(bar, MF_POPUP, (UINT_PTR)view, L"&View");
|
| 642 |
+
AppendMenuW(bar, MF_POPUP, (UINT_PTR)run, L"&Run");
|
| 643 |
+
SetMenu(hwnd, bar);
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
SovResult shell_create(HINSTANCE hinstance, int width, int height) {
|
| 647 |
+
/* Register panel class */
|
| 648 |
+
WNDCLASSEXW pc = {0};
|
| 649 |
+
pc.cbSize = sizeof(pc);
|
| 650 |
+
pc.style = CS_HREDRAW | CS_VREDRAW;
|
| 651 |
+
pc.lpfnWndProc = panel_proc;
|
| 652 |
+
pc.hInstance = hinstance;
|
| 653 |
+
pc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
|
| 654 |
+
pc.lpszClassName = PANEL_CLASS;
|
| 655 |
+
RegisterClassExW(&pc);
|
| 656 |
+
|
| 657 |
+
/* Register shell class */
|
| 658 |
+
WNDCLASSEXW sc = {0};
|
| 659 |
+
sc.cbSize = sizeof(sc);
|
| 660 |
+
sc.style = CS_HREDRAW | CS_VREDRAW;
|
| 661 |
+
sc.lpfnWndProc = shell_proc;
|
| 662 |
+
sc.hInstance = hinstance;
|
| 663 |
+
sc.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW);
|
| 664 |
+
sc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
|
| 665 |
+
sc.lpszClassName = SHELL_CLASS;
|
| 666 |
+
RegisterClassExW(&sc);
|
| 667 |
+
|
| 668 |
+
g_shell.frame = CreateWindowExW(
|
| 669 |
+
0, SHELL_CLASS, SHELL_TITLE,
|
| 670 |
+
WS_OVERLAPPEDWINDOW,
|
| 671 |
+
CW_USEDEFAULT, CW_USEDEFAULT, width, height,
|
| 672 |
+
NULL, NULL, hinstance, NULL
|
| 673 |
+
);
|
| 674 |
+
|
| 675 |
+
if (!g_shell.frame) return SOV_ERR_IO;
|
| 676 |
+
|
| 677 |
+
build_menu(g_shell.frame);
|
| 678 |
+
ShowWindow(g_shell.frame, SW_SHOW);
|
| 679 |
+
UpdateWindow(g_shell.frame);
|
| 680 |
+
return SOV_OK;
|
| 681 |
+
}
|
ide/native/platform/windows/shell.h
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_SHELL_H
|
| 2 |
+
#define SOVEREIGN_SHELL_H
|
| 3 |
+
|
| 4 |
+
#include <windows.h>
|
| 5 |
+
#include "../../core/errors.h"
|
| 6 |
+
|
| 7 |
+
SovResult shell_create(HINSTANCE hinstance, int width, int height);
|
| 8 |
+
|
| 9 |
+
#endif
|
ide/native/platform/windows/window.c
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — Win32 Window
|
| 3 |
+
* Main window proc. Owns editor view lifecycle, keyboard, mouse, paint.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
#include "window.h"
|
| 7 |
+
#include "application.h"
|
| 8 |
+
#include "../../core/events.h"
|
| 9 |
+
#include "../../editor/editor_view.h"
|
| 10 |
+
#include "../../editor/document.h"
|
| 11 |
+
|
| 12 |
+
#define WINDOW_CLASS_NAME L"SovereignIDE"
|
| 13 |
+
#define WINDOW_TITLE L"Sovereign IDE"
|
| 14 |
+
|
| 15 |
+
static EditorView *g_editor_view = NULL;
|
| 16 |
+
static Document g_doc;
|
| 17 |
+
|
| 18 |
+
static LRESULT CALLBACK window_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
| 19 |
+
switch (msg) {
|
| 20 |
+
|
| 21 |
+
case WM_CREATE: {
|
| 22 |
+
document_new(&g_doc);
|
| 23 |
+
buffer_insert(g_doc.buffer, 0,
|
| 24 |
+
"// Sovereign IDE\n// Type here to start editing.\n\nint main(void) {\n return 0;\n}\n",
|
| 25 |
+
81);
|
| 26 |
+
buffer_mark_clean(g_doc.buffer);
|
| 27 |
+
|
| 28 |
+
if (editor_view_create(&g_editor_view, hwnd) == SOV_OK) {
|
| 29 |
+
editor_view_set_document(g_editor_view, &g_doc);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/* 530ms timer for caret blink */
|
| 33 |
+
SetTimer(hwnd, 1, 530, NULL);
|
| 34 |
+
return 0;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
case WM_TIMER:
|
| 38 |
+
InvalidateRect(hwnd, NULL, FALSE);
|
| 39 |
+
return 0;
|
| 40 |
+
|
| 41 |
+
case WM_PAINT: {
|
| 42 |
+
PAINTSTRUCT ps;
|
| 43 |
+
BeginPaint(hwnd, &ps);
|
| 44 |
+
if (g_editor_view) {
|
| 45 |
+
RECT rc;
|
| 46 |
+
GetClientRect(hwnd, &rc);
|
| 47 |
+
editor_view_paint(g_editor_view, rc);
|
| 48 |
+
}
|
| 49 |
+
EndPaint(hwnd, &ps);
|
| 50 |
+
return 0;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
case WM_SIZE: {
|
| 54 |
+
uint32_t w = LOWORD(lp);
|
| 55 |
+
uint32_t h = HIWORD(lp);
|
| 56 |
+
if (g_editor_view) editor_view_resize(g_editor_view, w, h);
|
| 57 |
+
Event ev = { .kind = EVENT_RESIZE };
|
| 58 |
+
ev.resize.width = w; ev.resize.height = h;
|
| 59 |
+
event_post(ev);
|
| 60 |
+
return 0;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
case WM_KEYDOWN:
|
| 64 |
+
case WM_SYSKEYDOWN: {
|
| 65 |
+
if (g_editor_view) {
|
| 66 |
+
UINT mods = 0;
|
| 67 |
+
if (GetKeyState(VK_SHIFT) & 0x8000) mods |= 1;
|
| 68 |
+
if (GetKeyState(VK_CONTROL) & 0x8000) mods |= 2;
|
| 69 |
+
if (GetKeyState(VK_MENU) & 0x8000) mods |= 4;
|
| 70 |
+
editor_view_key_down(g_editor_view, (UINT)wp, mods);
|
| 71 |
+
}
|
| 72 |
+
return 0;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
case WM_CHAR: {
|
| 76 |
+
if (g_editor_view) {
|
| 77 |
+
editor_view_char(g_editor_view, (wchar_t)wp);
|
| 78 |
+
}
|
| 79 |
+
return 0;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
case WM_LBUTTONDOWN: {
|
| 83 |
+
SetCapture(hwnd);
|
| 84 |
+
if (g_editor_view) {
|
| 85 |
+
editor_view_mouse_down(g_editor_view,
|
| 86 |
+
(int)(short)LOWORD(lp), (int)(short)HIWORD(lp));
|
| 87 |
+
}
|
| 88 |
+
return 0;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
case WM_LBUTTONUP:
|
| 92 |
+
ReleaseCapture();
|
| 93 |
+
return 0;
|
| 94 |
+
|
| 95 |
+
case WM_MOUSEMOVE: {
|
| 96 |
+
if (g_editor_view) {
|
| 97 |
+
bool btn = (wp & MK_LBUTTON) != 0;
|
| 98 |
+
editor_view_mouse_move(g_editor_view,
|
| 99 |
+
(int)(short)LOWORD(lp), (int)(short)HIWORD(lp), btn);
|
| 100 |
+
}
|
| 101 |
+
return 0;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
case WM_MOUSEWHEEL: {
|
| 105 |
+
if (g_editor_view) {
|
| 106 |
+
int delta = GET_WHEEL_DELTA_WPARAM(wp);
|
| 107 |
+
/* Forward as key events for scroll */
|
| 108 |
+
UINT vk = (delta > 0) ? VK_UP : VK_DOWN;
|
| 109 |
+
for (int i = 0; i < SCROLL_LINES; i++) {
|
| 110 |
+
event_post((Event){ .kind = EVENT_MOUSE_WHEEL });
|
| 111 |
+
}
|
| 112 |
+
(void)vk;
|
| 113 |
+
}
|
| 114 |
+
return 0;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
case WM_SETFOCUS:
|
| 118 |
+
event_post((Event){ .kind = EVENT_FOCUS_GAINED });
|
| 119 |
+
return 0;
|
| 120 |
+
|
| 121 |
+
case WM_KILLFOCUS:
|
| 122 |
+
event_post((Event){ .kind = EVENT_FOCUS_LOST });
|
| 123 |
+
return 0;
|
| 124 |
+
|
| 125 |
+
case WM_DPICHANGED: {
|
| 126 |
+
RECT *r = (RECT *)lp;
|
| 127 |
+
SetWindowPos(hwnd, NULL, r->left, r->top,
|
| 128 |
+
r->right - r->left, r->bottom - r->top,
|
| 129 |
+
SWP_NOZORDER | SWP_NOACTIVATE);
|
| 130 |
+
return 0;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
case WM_CLOSE:
|
| 134 |
+
KillTimer(hwnd, 1);
|
| 135 |
+
if (g_editor_view) { editor_view_destroy(g_editor_view); g_editor_view = NULL; }
|
| 136 |
+
document_destroy(&g_doc);
|
| 137 |
+
event_post((Event){ .kind = EVENT_QUIT });
|
| 138 |
+
app_request_quit();
|
| 139 |
+
return 0;
|
| 140 |
+
|
| 141 |
+
case WM_DESTROY:
|
| 142 |
+
PostQuitMessage(0);
|
| 143 |
+
return 0;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
return DefWindowProcW(hwnd, msg, wp, lp);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
HWND window_create(int width, int height) {
|
| 150 |
+
WNDCLASSEXW wc = {0};
|
| 151 |
+
wc.cbSize = sizeof(wc);
|
| 152 |
+
wc.style = CS_HREDRAW | CS_VREDRAW;
|
| 153 |
+
wc.lpfnWndProc = window_proc;
|
| 154 |
+
wc.hInstance = app_get_hinstance();
|
| 155 |
+
wc.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW);
|
| 156 |
+
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
|
| 157 |
+
wc.lpszClassName = WINDOW_CLASS_NAME;
|
| 158 |
+
RegisterClassExW(&wc);
|
| 159 |
+
|
| 160 |
+
HWND hwnd = CreateWindowExW(
|
| 161 |
+
0,
|
| 162 |
+
WINDOW_CLASS_NAME,
|
| 163 |
+
WINDOW_TITLE,
|
| 164 |
+
WS_OVERLAPPEDWINDOW,
|
| 165 |
+
CW_USEDEFAULT, CW_USEDEFAULT,
|
| 166 |
+
width, height,
|
| 167 |
+
NULL, NULL,
|
| 168 |
+
app_get_hinstance(),
|
| 169 |
+
NULL
|
| 170 |
+
);
|
| 171 |
+
|
| 172 |
+
ShowWindow(hwnd, SW_SHOW);
|
| 173 |
+
UpdateWindow(hwnd);
|
| 174 |
+
return hwnd;
|
| 175 |
+
}
|
ide/native/platform/windows/window.h
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#ifndef SOVEREIGN_WINDOW_H
|
| 2 |
+
#define SOVEREIGN_WINDOW_H
|
| 3 |
+
|
| 4 |
+
#include <windows.h>
|
| 5 |
+
|
| 6 |
+
#define SCROLL_LINES 3
|
| 7 |
+
|
| 8 |
+
HWND window_create(int width, int height);
|
| 9 |
+
|
| 10 |
+
#endif
|
ide/native/terminal/conpty.c
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Sovereign IDE — ConPTY Terminal
|
| 3 |
+
* Windows Pseudo Console for embedded terminal.
|
| 4 |
+
* ConPTY APIs are loaded dynamically since MinGW headers may lack them.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
#include "conpty.h"
|
| 8 |
+
#include "../core/arena.h"
|
| 9 |
+
#include <stdlib.h>
|
| 10 |
+
#include <string.h>
|
| 11 |
+
|
| 12 |
+
/* ConPTY types and dynamic loading (Win10 1809+) */
|
| 13 |
+
typedef void *HPCON;
|
| 14 |
+
|
| 15 |
+
typedef HRESULT (WINAPI *PFN_CreatePseudoConsole)(COORD, HANDLE, HANDLE, DWORD, HPCON *);
|
| 16 |
+
typedef HRESULT (WINAPI *PFN_ResizePseudoConsole)(HPCON, COORD);
|
| 17 |
+
typedef void (WINAPI *PFN_ClosePseudoConsole)(HPCON);
|
| 18 |
+
|
| 19 |
+
#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
|
| 20 |
+
#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 0x00020016
|
| 21 |
+
#endif
|
| 22 |
+
|
| 23 |
+
static PFN_CreatePseudoConsole pfnCreatePseudoConsole;
|
| 24 |
+
static PFN_ResizePseudoConsole pfnResizePseudoConsole;
|
| 25 |
+
static PFN_ClosePseudoConsole pfnClosePseudoConsole;
|
| 26 |
+
static bool conpty_loaded = false;
|
| 27 |
+
|
| 28 |
+
static bool conpty_load(void) {
|
| 29 |
+
if (conpty_loaded) return pfnCreatePseudoConsole != NULL;
|
| 30 |
+
conpty_loaded = true;
|
| 31 |
+
HMODULE k32 = GetModuleHandleW(L"kernel32.dll");
|
| 32 |
+
if (!k32) return false;
|
| 33 |
+
pfnCreatePseudoConsole = (PFN_CreatePseudoConsole)(void *)GetProcAddress(k32, "CreatePseudoConsole");
|
| 34 |
+
pfnResizePseudoConsole = (PFN_ResizePseudoConsole)(void *)GetProcAddress(k32, "ResizePseudoConsole");
|
| 35 |
+
pfnClosePseudoConsole = (PFN_ClosePseudoConsole)(void *)GetProcAddress(k32, "ClosePseudoConsole");
|
| 36 |
+
return pfnCreatePseudoConsole != NULL;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
struct Terminal {
|
| 40 |
+
HPCON hpc;
|
| 41 |
+
HANDLE pipe_in;
|
| 42 |
+
HANDLE pipe_out;
|
| 43 |
+
HANDLE pipe_pty_in;
|
| 44 |
+
HANDLE pipe_pty_out;
|
| 45 |
+
PROCESS_INFORMATION pi;
|
| 46 |
+
bool alive;
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
SovResult terminal_create(Terminal **out, uint16_t cols, uint16_t rows) {
|
| 50 |
+
if (!conpty_load()) return SOV_ERR_ALLOC;
|
| 51 |
+
|
| 52 |
+
Terminal *t = (Terminal *)calloc(1, sizeof(Terminal));
|
| 53 |
+
if (!t) return SOV_ERR_ALLOC;
|
| 54 |
+
|
| 55 |
+
HANDLE pipe_pty_in, pipe_in;
|
| 56 |
+
HANDLE pipe_pty_out, pipe_out;
|
| 57 |
+
CreatePipe(&pipe_pty_in, &pipe_in, NULL, 0);
|
| 58 |
+
CreatePipe(&pipe_out, &pipe_pty_out, NULL, 0);
|
| 59 |
+
|
| 60 |
+
COORD size = { .X = (SHORT)cols, .Y = (SHORT)rows };
|
| 61 |
+
HRESULT hr = pfnCreatePseudoConsole(size, pipe_pty_in, pipe_pty_out, 0, &t->hpc);
|
| 62 |
+
if (FAILED(hr)) {
|
| 63 |
+
CloseHandle(pipe_pty_in);
|
| 64 |
+
CloseHandle(pipe_in);
|
| 65 |
+
CloseHandle(pipe_pty_out);
|
| 66 |
+
CloseHandle(pipe_out);
|
| 67 |
+
free(t);
|
| 68 |
+
return SOV_ERR_ALLOC;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
t->pipe_in = pipe_in;
|
| 72 |
+
t->pipe_out = pipe_out;
|
| 73 |
+
t->pipe_pty_in = pipe_pty_in;
|
| 74 |
+
t->pipe_pty_out = pipe_pty_out;
|
| 75 |
+
|
| 76 |
+
STARTUPINFOEXW si;
|
| 77 |
+
memset(&si, 0, sizeof(si));
|
| 78 |
+
si.StartupInfo.cb = sizeof(si);
|
| 79 |
+
|
| 80 |
+
SIZE_T attr_size = 0;
|
| 81 |
+
InitializeProcThreadAttributeList(NULL, 1, 0, &attr_size);
|
| 82 |
+
si.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)malloc(attr_size);
|
| 83 |
+
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attr_size);
|
| 84 |
+
UpdateProcThreadAttribute(si.lpAttributeList, 0,
|
| 85 |
+
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, t->hpc, sizeof(HPCON), NULL, NULL);
|
| 86 |
+
|
| 87 |
+
wchar_t cmd[] = L"cmd.exe";
|
| 88 |
+
CreateProcessW(NULL, cmd, NULL, NULL, FALSE,
|
| 89 |
+
EXTENDED_STARTUPINFO_PRESENT, NULL, NULL,
|
| 90 |
+
&si.StartupInfo, &t->pi);
|
| 91 |
+
|
| 92 |
+
DeleteProcThreadAttributeList(si.lpAttributeList);
|
| 93 |
+
free(si.lpAttributeList);
|
| 94 |
+
|
| 95 |
+
CloseHandle(pipe_pty_in);
|
| 96 |
+
CloseHandle(pipe_pty_out);
|
| 97 |
+
|
| 98 |
+
t->alive = true;
|
| 99 |
+
*out = t;
|
| 100 |
+
return SOV_OK;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
SovResult terminal_write(Terminal *t, const char *data, size_t len) {
|
| 104 |
+
DWORD written;
|
| 105 |
+
if (!WriteFile(t->pipe_in, data, (DWORD)len, &written, NULL))
|
| 106 |
+
return SOV_ERR_IO;
|
| 107 |
+
return SOV_OK;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
SovResult terminal_read(Terminal *t, char *buf, size_t max_len, size_t *out_len) {
|
| 111 |
+
DWORD available = 0;
|
| 112 |
+
if (!PeekNamedPipe(t->pipe_out, NULL, 0, NULL, &available, NULL) || available == 0) {
|
| 113 |
+
*out_len = 0;
|
| 114 |
+
return SOV_OK;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
DWORD to_read = (available < (DWORD)max_len) ? available : (DWORD)max_len;
|
| 118 |
+
DWORD nread = 0;
|
| 119 |
+
if (!ReadFile(t->pipe_out, buf, to_read, &nread, NULL))
|
| 120 |
+
return SOV_ERR_IO;
|
| 121 |
+
|
| 122 |
+
*out_len = nread;
|
| 123 |
+
return SOV_OK;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
void terminal_resize(Terminal *t, uint16_t cols, uint16_t rows) {
|
| 127 |
+
if (pfnResizePseudoConsole) {
|
| 128 |
+
COORD size = { .X = (SHORT)cols, .Y = (SHORT)rows };
|
| 129 |
+
pfnResizePseudoConsole(t->hpc, size);
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
bool terminal_is_alive(const Terminal *t) {
|
| 134 |
+
if (!t->alive) return false;
|
| 135 |
+
DWORD exit_code;
|
| 136 |
+
GetExitCodeProcess(t->pi.hProcess, &exit_code);
|
| 137 |
+
return exit_code == STILL_ACTIVE;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
void terminal_destroy(Terminal *t) {
|
| 141 |
+
if (!t) return;
|
| 142 |
+
if (pfnClosePseudoConsole) pfnClosePseudoConsole(t->hpc);
|
| 143 |
+
CloseHandle(t->pipe_in);
|
| 144 |
+
CloseHandle(t->pipe_out);
|
| 145 |
+
TerminateProcess(t->pi.hProcess, 0);
|
| 146 |
+
CloseHandle(t->pi.hProcess);
|
| 147 |
+
CloseHandle(t->pi.hThread);
|
| 148 |
+
free(t);
|
| 149 |
+
}
|