thinkstack-core 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- thinkstack_core/__init__.py +158 -0
- thinkstack_core/aggphi_textual.py +275 -0
- thinkstack_core/alerts/__init__.py +23 -0
- thinkstack_core/alerts/base.py +46 -0
- thinkstack_core/alerts/config.py +60 -0
- thinkstack_core/alerts/dispatcher.py +110 -0
- thinkstack_core/alerts/jira.py +96 -0
- thinkstack_core/alerts/linear.py +72 -0
- thinkstack_core/alerts/pagerduty.py +66 -0
- thinkstack_core/alerts/slack.py +81 -0
- thinkstack_core/alerts/teams.py +70 -0
- thinkstack_core/audit/__init__.py +43 -0
- thinkstack_core/audit/exporter.py +297 -0
- thinkstack_core/audit/privacy.py +101 -0
- thinkstack_core/audit/scrubber.py +149 -0
- thinkstack_core/audit/service.py +67 -0
- thinkstack_core/audit/signing.py +127 -0
- thinkstack_core/broadcast/__init__.py +4 -0
- thinkstack_core/broadcast/broadcaster.py +100 -0
- thinkstack_core/broadcast/watcher.py +71 -0
- thinkstack_core/capability.py +639 -0
- thinkstack_core/cloud/__init__.py +1 -0
- thinkstack_core/cloud/client_config.py +472 -0
- thinkstack_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- thinkstack_core/cloud/client_configs/.claude-stdio.json +13 -0
- thinkstack_core/cloud/client_configs/.cursor-mcp.json +13 -0
- thinkstack_core/cloud/client_configs/.opencode-bridge.json +13 -0
- thinkstack_core/cloud/client_configs/.opencode.json +15 -0
- thinkstack_core/cloud/client_configs/.vscode-mcp.json +13 -0
- thinkstack_core/cloud/mcp_client.py +229 -0
- thinkstack_core/cloud/setup.py +144 -0
- thinkstack_core/cloud/sync.py +143 -0
- thinkstack_core/cloud/sync_bundle.py +639 -0
- thinkstack_core/cloud/sync_conflicts.py +183 -0
- thinkstack_core/cloud/sync_state.py +159 -0
- thinkstack_core/cloud/team_sync.py +337 -0
- thinkstack_core/cloud/thinkstack-mcp-bridge.js +357 -0
- thinkstack_core/codex/__init__.py +9 -0
- thinkstack_core/codex/__main__.py +97 -0
- thinkstack_core/codex/capture.py +208 -0
- thinkstack_core/codex/proxy.py +412 -0
- thinkstack_core/compat.py +103 -0
- thinkstack_core/concept_catalog.py +209 -0
- thinkstack_core/consolidation/__init__.py +3 -0
- thinkstack_core/consolidation/synthesizer.py +87 -0
- thinkstack_core/consolidation/workflow.py +175 -0
- thinkstack_core/daemon/__init__.py +27 -0
- thinkstack_core/daemon/supervisor.py +293 -0
- thinkstack_core/daemon/watcher.py +244 -0
- thinkstack_core/dashboard_api.py +2012 -0
- thinkstack_core/deltaf.py +97 -0
- thinkstack_core/disclosure.py +50 -0
- thinkstack_core/divergence/__init__.py +3 -0
- thinkstack_core/divergence/detector.py +166 -0
- thinkstack_core/gateway/__init__.py +32 -0
- thinkstack_core/gateway/key_manager.py +124 -0
- thinkstack_core/gateway/metrics_webhook.py +252 -0
- thinkstack_core/gateway/policy.py +262 -0
- thinkstack_core/gateway/server.py +727 -0
- thinkstack_core/gateway/sso.py +233 -0
- thinkstack_core/gcc.py +1246 -0
- thinkstack_core/github/__init__.py +35 -0
- thinkstack_core/github/app.py +240 -0
- thinkstack_core/github/comment_builder.py +113 -0
- thinkstack_core/github/pat.py +76 -0
- thinkstack_core/github/pr_parser.py +82 -0
- thinkstack_core/github/pr_reporter.py +555 -0
- thinkstack_core/gitlab/__init__.py +177 -0
- thinkstack_core/hitl/__init__.py +4 -0
- thinkstack_core/hitl/channels.py +129 -0
- thinkstack_core/hitl/orchestrator.py +95 -0
- thinkstack_core/hooks/__init__.py +17 -0
- thinkstack_core/hooks/claude_code.py +228 -0
- thinkstack_core/hooks/git_capture.py +341 -0
- thinkstack_core/hooks/git_commit.py +182 -0
- thinkstack_core/hooks/installer.py +850 -0
- thinkstack_core/hooks/pre_commit.py +157 -0
- thinkstack_core/hooks/runner.py +386 -0
- thinkstack_core/identity/__init__.py +4 -0
- thinkstack_core/identity/agent.py +86 -0
- thinkstack_core/identity/providers.py +85 -0
- thinkstack_core/invariants.py +182 -0
- thinkstack_core/mcp/__init__.py +10 -0
- thinkstack_core/mcp/auth.py +177 -0
- thinkstack_core/mcp/server.py +1215 -0
- thinkstack_core/metrics/__init__.py +35 -0
- thinkstack_core/metrics/aggregate.py +215 -0
- thinkstack_core/metrics/calibrate.py +198 -0
- thinkstack_core/metrics/calibration.py +125 -0
- thinkstack_core/metrics/credibility.py +288 -0
- thinkstack_core/metrics/delivery_time.py +70 -0
- thinkstack_core/metrics/dhs.py +126 -0
- thinkstack_core/metrics/mcs.py +96 -0
- thinkstack_core/metrics/roi.py +88 -0
- thinkstack_core/metrics/session_writer.py +81 -0
- thinkstack_core/metrics/shadow_ai.py +117 -0
- thinkstack_core/metrics/sprint_writer.py +243 -0
- thinkstack_core/observability/__init__.py +78 -0
- thinkstack_core/observability/datadog.py +157 -0
- thinkstack_core/observability/formatter.py +119 -0
- thinkstack_core/observability/report.py +264 -0
- thinkstack_core/observability/servicenow.py +147 -0
- thinkstack_core/observability/splunk.py +218 -0
- thinkstack_core/observability/webhook.py +227 -0
- thinkstack_core/parser/__init__.py +30 -0
- thinkstack_core/parser/blocks.py +216 -0
- thinkstack_core/parser/inference.py +159 -0
- thinkstack_core/parser/thinking.py +112 -0
- thinkstack_core/projects.py +169 -0
- thinkstack_core/prompt_artifact.py +76 -0
- thinkstack_core/proxy/__init__.py +9 -0
- thinkstack_core/proxy/routes/__init__.py +1 -0
- thinkstack_core/proxy/routes/anthropic.py +264 -0
- thinkstack_core/proxy/routes/azure_openai.py +336 -0
- thinkstack_core/proxy/routes/gemini.py +331 -0
- thinkstack_core/proxy/routes/groq.py +284 -0
- thinkstack_core/proxy/routes/ollama.py +279 -0
- thinkstack_core/proxy/routes/openai.py +287 -0
- thinkstack_core/proxy/server.py +356 -0
- thinkstack_core/query/__init__.py +15 -0
- thinkstack_core/query/grep.py +181 -0
- thinkstack_core/query/hybrid.py +86 -0
- thinkstack_core/query/semantic.py +157 -0
- thinkstack_core/rdp.py +105 -0
- thinkstack_core/reasoning/__init__.py +4 -0
- thinkstack_core/reasoning/entry.py +31 -0
- thinkstack_core/reasoning/store.py +122 -0
- thinkstack_core/reasoning_plus/__init__.py +70 -0
- thinkstack_core/reasoning_plus/augmenter.py +337 -0
- thinkstack_core/reasoning_plus/capture.py +51 -0
- thinkstack_core/reasoning_plus/config.py +313 -0
- thinkstack_core/reasoning_plus/context.py +262 -0
- thinkstack_core/reasoning_plus/learning/__init__.py +125 -0
- thinkstack_core/reasoning_plus/learning/analytics.py +141 -0
- thinkstack_core/reasoning_plus/learning/api.py +784 -0
- thinkstack_core/reasoning_plus/learning/chain.py +285 -0
- thinkstack_core/reasoning_plus/learning/composer.py +141 -0
- thinkstack_core/reasoning_plus/learning/conflicts.py +184 -0
- thinkstack_core/reasoning_plus/learning/context_collector.py +194 -0
- thinkstack_core/reasoning_plus/learning/cross_project.py +234 -0
- thinkstack_core/reasoning_plus/learning/deny_list.py +108 -0
- thinkstack_core/reasoning_plus/learning/embeddings.py +209 -0
- thinkstack_core/reasoning_plus/learning/evolution.py +119 -0
- thinkstack_core/reasoning_plus/learning/extractor.py +271 -0
- thinkstack_core/reasoning_plus/learning/filter_five_layer.py +95 -0
- thinkstack_core/reasoning_plus/learning/models.py +149 -0
- thinkstack_core/reasoning_plus/learning/org_store.py +156 -0
- thinkstack_core/reasoning_plus/learning/pii.py +142 -0
- thinkstack_core/reasoning_plus/learning/promotion.py +58 -0
- thinkstack_core/reasoning_plus/learning/provenance.py +126 -0
- thinkstack_core/reasoning_plus/learning/recorder.py +81 -0
- thinkstack_core/reasoning_plus/learning/relevance.py +122 -0
- thinkstack_core/reasoning_plus/learning/state.py +86 -0
- thinkstack_core/reasoning_plus/learning/store.py +178 -0
- thinkstack_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- thinkstack_core/reasoning_plus/prompt.py +90 -0
- thinkstack_core/rep.py +134 -0
- thinkstack_core/rep_network/__init__.py +25 -0
- thinkstack_core/rep_network/merge.py +70 -0
- thinkstack_core/rep_network/node.py +137 -0
- thinkstack_core/rep_network/server.py +140 -0
- thinkstack_core/rep_network/sync.py +207 -0
- thinkstack_core/sensitivity.py +182 -0
- thinkstack_core/serve.py +258 -0
- thinkstack_core/session/__init__.py +39 -0
- thinkstack_core/session/disagreement.py +188 -0
- thinkstack_core/session/models.py +114 -0
- thinkstack_core/session/orchestrator.py +182 -0
- thinkstack_core/session/planner.py +169 -0
- thinkstack_core/session/simulator.py +132 -0
- thinkstack_core/signing.py +290 -0
- thinkstack_core/sis.py +197 -0
- thinkstack_core/skills/pr-reviewer/SKILL.md +204 -0
- thinkstack_core/skills/thinkstack-auto-sync/SKILL.md +175 -0
- thinkstack_core/skills/thinkstack-session-start/SKILL.md +136 -0
- thinkstack_core/storage.py +308 -0
- thinkstack_core/templates/__init__.py +6 -0
- thinkstack_core/templates/engine.py +122 -0
- thinkstack_core/templates/go.py +18 -0
- thinkstack_core/templates/infra.py +19 -0
- thinkstack_core/templates/library/__init__.py +18 -0
- thinkstack_core/templates/library/api_design.md +27 -0
- thinkstack_core/templates/library/bug_fix.md +27 -0
- thinkstack_core/templates/library/decision_record.md +27 -0
- thinkstack_core/templates/library/engine.py +228 -0
- thinkstack_core/templates/library/security_review.md +30 -0
- thinkstack_core/templates/python.py +19 -0
- thinkstack_core/templates/react.py +18 -0
- thinkstack_core/templates/typescript.py +18 -0
- thinkstack_core/theta.py +221 -0
- thinkstack_core/theta_synthesis.py +268 -0
- thinkstack_core/topics.py +320 -0
- thinkstack_core/variance.py +219 -0
- thinkstack_core/wrapper/__init__.py +52 -0
- thinkstack_core/wrapper/anthropic.py +487 -0
- thinkstack_core/wrapper/base.py +562 -0
- thinkstack_core/wrapper/bedrock.py +342 -0
- thinkstack_core/wrapper/gemini.py +422 -0
- thinkstack_core/wrapper/ollama.py +527 -0
- thinkstack_core/wrapper/openai.py +461 -0
- thinkstack_core-4.0.0.dist-info/METADATA +868 -0
- thinkstack_core-4.0.0.dist-info/RECORD +205 -0
- thinkstack_core-4.0.0.dist-info/WHEEL +5 -0
- thinkstack_core-4.0.0.dist-info/entry_points.txt +2 -0
- thinkstack_core-4.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""
|
|
2
|
+
thinkstack_core.codex.proxy
|
|
3
|
+
=========================
|
|
4
|
+
Lightweight HTTP proxy server for intercepting OpenAI Codex CLI traffic.
|
|
5
|
+
|
|
6
|
+
Uses only stdlib (http.server, urllib.request, threading) — no FastAPI or
|
|
7
|
+
httpx dependency required.
|
|
8
|
+
|
|
9
|
+
Default port: 8766 (main ThinkStack proxy runs on 8765)
|
|
10
|
+
Upstream: https://api.openai.com
|
|
11
|
+
|
|
12
|
+
Routes:
|
|
13
|
+
POST /v1/* → inject RACP, forward, capture response
|
|
14
|
+
GET /health → 200 {"status": "ok"}
|
|
15
|
+
* (everything else) → transparent forward
|
|
16
|
+
|
|
17
|
+
Errors always return 502 JSON — never crash the proxy.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import threading
|
|
24
|
+
import urllib.error
|
|
25
|
+
import urllib.request
|
|
26
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("thinkstack.codex.proxy")
|
|
31
|
+
|
|
32
|
+
DEFAULT_PORT = 8766
|
|
33
|
+
DEFAULT_UPSTREAM = "https://api.openai.com"
|
|
34
|
+
|
|
35
|
+
# Headers that must NOT be forwarded to the upstream (they are hop-by-hop or
|
|
36
|
+
# set by the proxy itself when re-encoding the body).
|
|
37
|
+
_HOP_BY_HOP = frozenset(
|
|
38
|
+
[
|
|
39
|
+
"connection",
|
|
40
|
+
"keep-alive",
|
|
41
|
+
"proxy-authenticate",
|
|
42
|
+
"proxy-authorization",
|
|
43
|
+
"te",
|
|
44
|
+
"trailers",
|
|
45
|
+
"transfer-encoding",
|
|
46
|
+
"upgrade",
|
|
47
|
+
"host",
|
|
48
|
+
"content-length", # recalculated after body modification
|
|
49
|
+
]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Request handler
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
class _CodexRequestHandler(BaseHTTPRequestHandler):
|
|
58
|
+
"""One instance per incoming HTTP connection."""
|
|
59
|
+
|
|
60
|
+
# Injected by CodexProxy on the server object
|
|
61
|
+
upstream: str = DEFAULT_UPSTREAM
|
|
62
|
+
|
|
63
|
+
# Silence the default access log — we write our own on error
|
|
64
|
+
def log_message(self, fmt: str, *args) -> None: # type: ignore[override]
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# Routing
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def do_GET(self) -> None:
|
|
72
|
+
if self.path == "/health":
|
|
73
|
+
self._send_json(200, {"status": "ok", "proxy": "thinkstack-codex"})
|
|
74
|
+
return
|
|
75
|
+
self._forward_transparent()
|
|
76
|
+
|
|
77
|
+
def do_POST(self) -> None:
|
|
78
|
+
if self.path.startswith("/v1/"):
|
|
79
|
+
self._handle_v1_post()
|
|
80
|
+
else:
|
|
81
|
+
self._forward_transparent()
|
|
82
|
+
|
|
83
|
+
# Support additional methods by transparent proxy
|
|
84
|
+
def do_PUT(self) -> None:
|
|
85
|
+
self._forward_transparent()
|
|
86
|
+
|
|
87
|
+
def do_DELETE(self) -> None:
|
|
88
|
+
self._forward_transparent()
|
|
89
|
+
|
|
90
|
+
def do_PATCH(self) -> None:
|
|
91
|
+
self._forward_transparent()
|
|
92
|
+
|
|
93
|
+
def do_HEAD(self) -> None:
|
|
94
|
+
self._forward_transparent()
|
|
95
|
+
|
|
96
|
+
def do_OPTIONS(self) -> None:
|
|
97
|
+
self._forward_transparent()
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
# v1 POST handler — RACP injection + capture
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def _handle_v1_post(self) -> None:
|
|
104
|
+
try:
|
|
105
|
+
body_bytes = self._read_body()
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
self._send_error_502(f"failed to read request body: {exc}")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Parse JSON — if not parseable, forward raw
|
|
111
|
+
try:
|
|
112
|
+
request_body = json.loads(body_bytes) if body_bytes else {}
|
|
113
|
+
except Exception:
|
|
114
|
+
request_body = None
|
|
115
|
+
|
|
116
|
+
# Inject RACP prefix
|
|
117
|
+
modified_body_bytes = body_bytes
|
|
118
|
+
if request_body is not None:
|
|
119
|
+
try:
|
|
120
|
+
from thinkstack_core.codex.capture import CodexCaptureHandler
|
|
121
|
+
import uuid
|
|
122
|
+
|
|
123
|
+
session_id = str(uuid.uuid4())
|
|
124
|
+
handler = CodexCaptureHandler()
|
|
125
|
+
modified_body = handler.pre_execute(request_body, session_id)
|
|
126
|
+
modified_body_bytes = json.dumps(modified_body).encode("utf-8")
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
logger.warning("thinkstack codex RACP injection failed — %s", exc)
|
|
129
|
+
session_id = ""
|
|
130
|
+
handler = None
|
|
131
|
+
else:
|
|
132
|
+
session_id = ""
|
|
133
|
+
handler = None
|
|
134
|
+
|
|
135
|
+
# Forward to upstream
|
|
136
|
+
upstream_url = self.upstream.rstrip("/") + self.path
|
|
137
|
+
fwd_headers = self._build_forward_headers(len(modified_body_bytes))
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
req = urllib.request.Request(
|
|
141
|
+
upstream_url,
|
|
142
|
+
data=modified_body_bytes,
|
|
143
|
+
headers=fwd_headers,
|
|
144
|
+
method="POST",
|
|
145
|
+
)
|
|
146
|
+
with urllib.request.urlopen(req) as resp:
|
|
147
|
+
resp_bytes = resp.read()
|
|
148
|
+
resp_status = resp.status
|
|
149
|
+
resp_headers = dict(resp.headers)
|
|
150
|
+
except urllib.error.HTTPError as exc:
|
|
151
|
+
resp_bytes = exc.read()
|
|
152
|
+
resp_status = exc.code
|
|
153
|
+
resp_headers = dict(exc.headers)
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
self._send_error_502(f"upstream request failed: {exc}")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# Send response back to Codex CLI
|
|
159
|
+
self._relay_response(resp_status, resp_headers, resp_bytes)
|
|
160
|
+
|
|
161
|
+
# Fire post_execute in background — never blocks the response
|
|
162
|
+
if handler is not None and request_body is not None:
|
|
163
|
+
try:
|
|
164
|
+
resp_body = json.loads(resp_bytes) if resp_bytes else {}
|
|
165
|
+
except Exception:
|
|
166
|
+
resp_body = {}
|
|
167
|
+
t = threading.Thread(
|
|
168
|
+
target=_safe_post_execute,
|
|
169
|
+
args=(handler, request_body, resp_body, session_id),
|
|
170
|
+
daemon=True,
|
|
171
|
+
)
|
|
172
|
+
t.start()
|
|
173
|
+
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
# Transparent forward
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def _forward_transparent(self) -> None:
|
|
179
|
+
try:
|
|
180
|
+
body_bytes = self._read_body()
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
self._send_error_502(f"failed to read request body: {exc}")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
upstream_url = self.upstream.rstrip("/") + self.path
|
|
186
|
+
fwd_headers = self._build_forward_headers(len(body_bytes) if body_bytes else 0)
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
req = urllib.request.Request(
|
|
190
|
+
upstream_url,
|
|
191
|
+
data=body_bytes if body_bytes else None,
|
|
192
|
+
headers=fwd_headers,
|
|
193
|
+
method=self.command,
|
|
194
|
+
)
|
|
195
|
+
with urllib.request.urlopen(req) as resp:
|
|
196
|
+
resp_bytes = resp.read()
|
|
197
|
+
resp_status = resp.status
|
|
198
|
+
resp_headers = dict(resp.headers)
|
|
199
|
+
except urllib.error.HTTPError as exc:
|
|
200
|
+
resp_bytes = exc.read()
|
|
201
|
+
resp_status = exc.code
|
|
202
|
+
resp_headers = dict(exc.headers)
|
|
203
|
+
except Exception as exc:
|
|
204
|
+
self._send_error_502(f"upstream request failed: {exc}")
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
self._relay_response(resp_status, resp_headers, resp_bytes)
|
|
208
|
+
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
# Helpers
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
def _read_body(self) -> bytes:
|
|
214
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
215
|
+
if length > 0:
|
|
216
|
+
return self.rfile.read(length)
|
|
217
|
+
return b""
|
|
218
|
+
|
|
219
|
+
def _build_forward_headers(self, content_length: int) -> dict:
|
|
220
|
+
"""Build headers to send upstream, stripping hop-by-hop headers."""
|
|
221
|
+
headers: dict = {}
|
|
222
|
+
for key, val in self.headers.items():
|
|
223
|
+
if key.lower() not in _HOP_BY_HOP:
|
|
224
|
+
headers[key] = val
|
|
225
|
+
if content_length > 0:
|
|
226
|
+
headers["Content-Length"] = str(content_length)
|
|
227
|
+
# Ensure Host is set to the upstream host
|
|
228
|
+
from urllib.parse import urlparse
|
|
229
|
+
parsed = urlparse(self.upstream)
|
|
230
|
+
headers["Host"] = parsed.netloc
|
|
231
|
+
return headers
|
|
232
|
+
|
|
233
|
+
def _relay_response(self, status: int, headers: dict, body: bytes) -> None:
|
|
234
|
+
"""Write upstream response back to the Codex CLI connection."""
|
|
235
|
+
self.send_response(status)
|
|
236
|
+
for key, val in headers.items():
|
|
237
|
+
if key.lower() not in ("transfer-encoding", "connection"):
|
|
238
|
+
self.send_header(key, val)
|
|
239
|
+
self.send_header("Content-Length", str(len(body)))
|
|
240
|
+
self.end_headers()
|
|
241
|
+
if body:
|
|
242
|
+
self.wfile.write(body)
|
|
243
|
+
|
|
244
|
+
def _send_json(self, status: int, payload: dict) -> None:
|
|
245
|
+
body = json.dumps(payload).encode("utf-8")
|
|
246
|
+
self.send_response(status)
|
|
247
|
+
self.send_header("Content-Type", "application/json")
|
|
248
|
+
self.send_header("Content-Length", str(len(body)))
|
|
249
|
+
self.end_headers()
|
|
250
|
+
self.wfile.write(body)
|
|
251
|
+
|
|
252
|
+
def _send_error_502(self, detail: str) -> None:
|
|
253
|
+
logger.warning("thinkstack codex proxy 502 — %s", detail)
|
|
254
|
+
self._send_json(502, {"error": "bad_gateway", "detail": detail})
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
# Background post_execute helper
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def _safe_post_execute(
|
|
262
|
+
handler: object,
|
|
263
|
+
request_body: dict,
|
|
264
|
+
response_body: dict,
|
|
265
|
+
session_id: str,
|
|
266
|
+
) -> None:
|
|
267
|
+
try:
|
|
268
|
+
handler.post_execute(request_body, response_body, session_id) # type: ignore[attr-defined]
|
|
269
|
+
except Exception as exc:
|
|
270
|
+
logger.warning("thinkstack codex post_execute background error — %s", exc)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
# CodexProxy
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
class CodexProxy:
|
|
278
|
+
"""
|
|
279
|
+
Lightweight stdlib-only HTTP proxy for intercepting Codex CLI traffic.
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
port:
|
|
284
|
+
Local port to bind on (default 8766).
|
|
285
|
+
upstream:
|
|
286
|
+
Base URL of the real OpenAI API (default https://api.openai.com).
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
def __init__(
|
|
290
|
+
self,
|
|
291
|
+
port: int = DEFAULT_PORT,
|
|
292
|
+
upstream: str = DEFAULT_UPSTREAM,
|
|
293
|
+
) -> None:
|
|
294
|
+
self.port = port
|
|
295
|
+
self.upstream = upstream
|
|
296
|
+
self._server: Optional[HTTPServer] = None
|
|
297
|
+
self._thread: Optional[threading.Thread] = None
|
|
298
|
+
|
|
299
|
+
def start(self) -> None:
|
|
300
|
+
"""Start the proxy server (blocking — press Ctrl+C to stop)."""
|
|
301
|
+
self._server = self._make_server()
|
|
302
|
+
try:
|
|
303
|
+
self._server.serve_forever()
|
|
304
|
+
except KeyboardInterrupt:
|
|
305
|
+
pass
|
|
306
|
+
finally:
|
|
307
|
+
self._server.server_close()
|
|
308
|
+
|
|
309
|
+
def start_background(self) -> threading.Thread:
|
|
310
|
+
"""
|
|
311
|
+
Start the proxy in a daemon thread.
|
|
312
|
+
|
|
313
|
+
Returns the thread (already started). Call stop() to shut down.
|
|
314
|
+
"""
|
|
315
|
+
self._server = self._make_server()
|
|
316
|
+
t = threading.Thread(target=self._server.serve_forever, daemon=True)
|
|
317
|
+
t.start()
|
|
318
|
+
self._thread = t
|
|
319
|
+
return t
|
|
320
|
+
|
|
321
|
+
def stop(self) -> None:
|
|
322
|
+
"""Shut down the proxy server gracefully."""
|
|
323
|
+
if self._server is not None:
|
|
324
|
+
self._server.shutdown()
|
|
325
|
+
self._server.server_close()
|
|
326
|
+
self._server = None
|
|
327
|
+
|
|
328
|
+
# ------------------------------------------------------------------
|
|
329
|
+
# Internal
|
|
330
|
+
# ------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
def _make_server(self) -> HTTPServer:
|
|
333
|
+
upstream = self.upstream
|
|
334
|
+
|
|
335
|
+
class _Handler(_CodexRequestHandler):
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
_Handler.upstream = upstream # type: ignore[assignment]
|
|
339
|
+
server = HTTPServer(("127.0.0.1", self.port), _Handler)
|
|
340
|
+
server.timeout = 30
|
|
341
|
+
return server
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
# install_codex_hook
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
def install_codex_hook(project_root: str) -> tuple[bool, str]:
|
|
349
|
+
"""
|
|
350
|
+
Create `.codex/config.json` under *project_root* pointing at the local
|
|
351
|
+
ThinkStack Codex proxy.
|
|
352
|
+
|
|
353
|
+
Idempotent — does not overwrite an existing config.
|
|
354
|
+
|
|
355
|
+
Returns (True, config_path) on success, (False, error_message) on failure.
|
|
356
|
+
"""
|
|
357
|
+
try:
|
|
358
|
+
codex_dir = Path(project_root) / ".codex"
|
|
359
|
+
codex_dir.mkdir(parents=True, exist_ok=True)
|
|
360
|
+
|
|
361
|
+
config_path = codex_dir / "config.json"
|
|
362
|
+
|
|
363
|
+
# Idempotent — do not overwrite if already present
|
|
364
|
+
if config_path.exists():
|
|
365
|
+
return True, str(config_path)
|
|
366
|
+
|
|
367
|
+
config = {
|
|
368
|
+
"apiBase": f"http://localhost:{DEFAULT_PORT}/v1",
|
|
369
|
+
"provider": "openai",
|
|
370
|
+
}
|
|
371
|
+
config_path.write_text(json.dumps(config, indent=2), encoding="utf-8")
|
|
372
|
+
return True, str(config_path)
|
|
373
|
+
|
|
374
|
+
except Exception as exc:
|
|
375
|
+
return False, str(exc)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
# get_codex_hook_status
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
def get_codex_hook_status(project_root: str) -> dict:
|
|
383
|
+
"""
|
|
384
|
+
Return the install and runtime status of the ThinkStack Codex proxy.
|
|
385
|
+
|
|
386
|
+
Returns::
|
|
387
|
+
|
|
388
|
+
{
|
|
389
|
+
"codex_config": bool, # .codex/config.json exists
|
|
390
|
+
"codex_config_path": str, # absolute path to config file
|
|
391
|
+
"proxy_running": bool, # True if GET /health returns 200
|
|
392
|
+
}
|
|
393
|
+
"""
|
|
394
|
+
config_path = Path(project_root) / ".codex" / "config.json"
|
|
395
|
+
codex_config = config_path.exists()
|
|
396
|
+
|
|
397
|
+
proxy_running = False
|
|
398
|
+
try:
|
|
399
|
+
req = urllib.request.Request(
|
|
400
|
+
f"http://localhost:{DEFAULT_PORT}/health",
|
|
401
|
+
method="GET",
|
|
402
|
+
)
|
|
403
|
+
with urllib.request.urlopen(req, timeout=2) as resp:
|
|
404
|
+
proxy_running = resp.status == 200
|
|
405
|
+
except Exception:
|
|
406
|
+
proxy_running = False
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
"codex_config": codex_config,
|
|
410
|
+
"codex_config_path": str(config_path),
|
|
411
|
+
"proxy_running": proxy_running,
|
|
412
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backward-compatibility shims for the thinkstack → ThinkStack rename.
|
|
3
|
+
|
|
4
|
+
Provides fallbacks for deprecated environment variables, MCP tool names,
|
|
5
|
+
and auth headers so existing users can migrate without breaking.
|
|
6
|
+
Shims emit deprecation warnings and will be removed in v5.0.0.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import warnings
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Environment variable shims
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
_thinkstack_TO_THINKSTACK = {
|
|
20
|
+
"THINKSTACK_DISABLE": "thinkstack_DISABLE",
|
|
21
|
+
"THINKSTACK_MCP_URL": "thinkstack_MCP_URL",
|
|
22
|
+
"THINKSTACK_API_KEY": "thinkstack_API_KEY",
|
|
23
|
+
"THINKSTACK_MCP_DOMAIN": "thinkstack_MCP_DOMAIN",
|
|
24
|
+
"THINKSTACK_ORG_ID": "thinkstack_ORG_ID",
|
|
25
|
+
"THINKSTACK_GCC_PATH": "thinkstack_GCC_PATH",
|
|
26
|
+
"THINKSTACK_REGISTRY_PATH": "thinkstack_REGISTRY_PATH",
|
|
27
|
+
"THINKSTACK_SIGN_LOG": "thinkstack_SIGN_LOG",
|
|
28
|
+
"THINKSTACK_STORAGE": "thinkstack_STORAGE",
|
|
29
|
+
"THINKSTACK_BUCKET": "thinkstack_BUCKET",
|
|
30
|
+
"THINKSTACK_ACCESS_KEY_ID": "thinkstack_ACCESS_KEY_ID",
|
|
31
|
+
"THINKSTACK_SECRET_ACCESS_KEY": "thinkstack_SECRET_ACCESS_KEY",
|
|
32
|
+
"THINKSTACK_AZURE_CONTAINER": "thinkstack_AZURE_CONTAINER",
|
|
33
|
+
"THINKSTACK_SESSION_CLOUD_URL": "thinkstack_SESSION_CLOUD_URL",
|
|
34
|
+
"THINKSTACK_AGGPHI": "thinkstack_AGGPHI",
|
|
35
|
+
"THINKSTACK_AGGPHI_LLM": "thinkstack_AGGPHI_LLM",
|
|
36
|
+
"THINKSTACK_AGGPHI_MODEL": "thinkstack_AGGPHI_MODEL",
|
|
37
|
+
"THINKSTACK_JWKS_URLS": "thinkstack_JWKS_URLS",
|
|
38
|
+
"THINKSTACK_METRICS_WEBHOOK_URL": "thinkstack_METRICS_WEBHOOK_URL",
|
|
39
|
+
"THINKSTACK_DATADOG_API_KEY": "thinkstack_DATADOG_API_KEY",
|
|
40
|
+
"THINKSTACK_SPLUNK_HEC_URL": "thinkstack_SPLUNK_HEC_URL",
|
|
41
|
+
"THINKSTACK_SERVICENOW_INSTANCE": "thinkstack_SERVICENOW_INSTANCE",
|
|
42
|
+
"THINKSTACK_GITHUB_WEBHOOK_SECRET": "thinkstack_GITHUB_WEBHOOK_SECRET",
|
|
43
|
+
"THINKSTACK_DAEMON_ROOT": "thinkstack_DAEMON_ROOT",
|
|
44
|
+
"THINKSTACK_AGENT_ID": "thinkstack_AGENT_ID",
|
|
45
|
+
"THINKSTACK_AGENT_NAME": "thinkstack_AGENT_NAME",
|
|
46
|
+
"THINKSTACK_AGENT_SCOPE": "thinkstack_AGENT_SCOPE",
|
|
47
|
+
"THINKSTACK_PAT_ENCRYPTION_KEY": "thinkstack_PAT_ENCRYPTION_KEY",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def getenv(
|
|
52
|
+
name: str, default: str | None = None, *, _warned: set[str] | None = None
|
|
53
|
+
) -> str | None:
|
|
54
|
+
"""Read an environment variable with backward-compat fallback.
|
|
55
|
+
|
|
56
|
+
Tries *name* first (e.g. ``THINKSTACK_MCP_URL``). If unset, falls
|
|
57
|
+
back to the old ``thinkstack_*`` name and issues a deprecation warning.
|
|
58
|
+
"""
|
|
59
|
+
value = os.environ.get(name)
|
|
60
|
+
if value is not None:
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
old_name = _thinkstack_TO_THINKSTACK.get(name)
|
|
64
|
+
if old_name is not None:
|
|
65
|
+
old_value = os.environ.get(old_name)
|
|
66
|
+
if old_value is not None:
|
|
67
|
+
if _warned is None or old_name not in _warned:
|
|
68
|
+
warnings.warn(
|
|
69
|
+
f"{old_name} is deprecated, use {name} instead",
|
|
70
|
+
DeprecationWarning,
|
|
71
|
+
stacklevel=2,
|
|
72
|
+
)
|
|
73
|
+
if _warned is not None:
|
|
74
|
+
_warned.add(old_name)
|
|
75
|
+
return old_value
|
|
76
|
+
|
|
77
|
+
return default
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# Local runtime directory auto-migration
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
_OLD_thinkstack_DIR = Path.home() / ".thinkstack"
|
|
85
|
+
_NEW_THINKSTACK_DIR = Path.home() / ".thinkstack"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def migrate_runtime_dir() -> None:
|
|
89
|
+
"""Move ``~/.thinkstack/`` to ``~/.thinkstack/`` if the old dir exists
|
|
90
|
+
and the new one doesn't. Called once at import time."""
|
|
91
|
+
if _OLD_thinkstack_DIR.exists() and not _NEW_THINKSTACK_DIR.exists():
|
|
92
|
+
import shutil
|
|
93
|
+
shutil.move(str(_OLD_thinkstack_DIR), str(_NEW_THINKSTACK_DIR))
|
|
94
|
+
warnings.warn(
|
|
95
|
+
f"Moved {_OLD_thinkstack_DIR} → {_NEW_THINKSTACK_DIR} "
|
|
96
|
+
"(thinkstack is now thinkstack)",
|
|
97
|
+
DeprecationWarning,
|
|
98
|
+
stacklevel=2,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Run auto-migration on import
|
|
103
|
+
migrate_runtime_dir()
|