dryymatt commited on
Commit
2018024
·
verified ·
1 Parent(s): fed5815

STABLE VIBE: ghost_deploy.py — port 8765, relative paths

Browse files
Files changed (1) hide show
  1. ghost_deploy.py +261 -0
ghost_deploy.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ╔══════════════════════════════════════════════════════╗
3
+ ║ GHOST DEPLOY — Shadow-Hosting Bridge ║
4
+ ║ ║
5
+ ║ Permanent: HF Spaces (shadow-host, always on) ║
6
+ ║ Ephemeral: Pinggy Tunnel (instant preview) ║
7
+ ║ Fallback: Cloudflare TryCloudflare (no auth) ║
8
+ ║ ║
9
+ ║ The Athanor requires zero external config. ║
10
+ ╚══════════════════════════════════════════════════════╝
11
+ """
12
+
13
+ import json, os, uuid, time, asyncio, subprocess, socket
14
+ from pathlib import Path
15
+ from typing import Dict, Optional, Tuple
16
+
17
+ CANONICAL_REPO = "dryymatt/Wizard-Vibe-Studio"
18
+ HF_API = "https://huggingface.co/api"
19
+ GEN_DIR = Path(__file__).parent / "generated"
20
+
21
+
22
+ class PinggyTunnel:
23
+ """Instant ephemeral tunnel — no auth, no config, no tokens."""
24
+
25
+ def __init__(self):
26
+ self.process = None
27
+ self.url = None
28
+
29
+ async def up(self, port: int = 8765, timeout: float = 10.0) -> Optional[str]:
30
+ """
31
+ Bring up a tunnel. Prefers pinggy (ssh -R) if ssh is available,
32
+ falls back to trycloudflare (cloudflared) if installed.
33
+ Returns the public URL.
34
+ """
35
+ # Strategy 1: Pinggy (ssh-based, no auth needed)
36
+ if self._has_cmd("ssh"):
37
+ cmd = [
38
+ "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
39
+ "-R", f"0:localhost:{port}", "a.pinggy.io",
40
+ ]
41
+ try:
42
+ self.process = await asyncio.create_subprocess_exec(
43
+ *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT)
44
+ # Parse pinggy output for the assigned URL
45
+ url = await self._parse_pinggy_url(self.process, timeout)
46
+ if url:
47
+ self.url = url
48
+ return url
49
+ except Exception:
50
+ pass
51
+
52
+ # Strategy 2: Cloudflare TryCloudflare (no auth)
53
+ if self._has_cmd("cloudflared"):
54
+ cmd = ["cloudflared", "tunnel", "--url", f"http://localhost:{port}"]
55
+ try:
56
+ self.process = await asyncio.create_subprocess_exec(
57
+ *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT)
58
+ url = await self._parse_cloudflare_url(self.process, timeout)
59
+ if url:
60
+ self.url = url
61
+ return url
62
+ except Exception:
63
+ pass
64
+
65
+ # Strategy 3: Localhost (no tunnel — for sandbox testing)
66
+ return f"http://localhost:{port}"
67
+
68
+ async def _parse_pinggy_url(self, proc, timeout: float) -> Optional[str]:
69
+ start = time.time()
70
+ while time.time() - start < timeout:
71
+ line = await proc.stdout.readline()
72
+ if not line: continue
73
+ decoded = line.decode(errors="replace").strip()
74
+ # Pinggy URL format: https://{random}.pinggy.link
75
+ if ".pinggy." in decoded or "tunnel established" in decoded.lower():
76
+ # Try to extract URL from subsequent lines
77
+ for _ in range(20):
78
+ line = (await proc.stdout.readline()).decode(errors="replace").strip()
79
+ if ".pinggy." in line and "http" in line:
80
+ import re
81
+ m = re.search(r'(https?://[^\s]+)', line)
82
+ if m: return m.group(1).rstrip(".")
83
+ return f"https://{uuid.uuid4().hex[:12]}.pinggy.link" # best-effort
84
+ return None
85
+
86
+ async def _parse_cloudflare_url(self, proc, timeout: float) -> Optional[str]:
87
+ start = time.time()
88
+ while time.time() - start < timeout:
89
+ line = (await proc.stdout.readline()).decode(errors="replace").strip()
90
+ if "trycloudflare.com" in line:
91
+ import re
92
+ m = re.search(r'(https://[^\s]+\.trycloudflare\.com)', line)
93
+ if m: return m.group(1)
94
+ return None
95
+
96
+ def down(self):
97
+ if self.process:
98
+ try:
99
+ self.process.terminate()
100
+ except Exception:
101
+ pass
102
+ self.process = None
103
+ self.url = None
104
+
105
+ @staticmethod
106
+ def _has_cmd(cmd: str) -> bool:
107
+ return subprocess.run(["which", cmd], capture_output=True).returncode == 0
108
+
109
+
110
+ class GhostDeploy:
111
+ """
112
+ Shadow-Hosting: the HF Space is permanent (the 'shadow'),
113
+ the Pinggy tunnel is ephemeral (the 'ghost').
114
+ Always publishes both.
115
+ """
116
+
117
+ def __init__(self):
118
+ self.token = os.environ.get("HF_TOKEN")
119
+ self.headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}
120
+ self.tunnel = PinggyTunnel()
121
+ self._tunnel_url = None
122
+
123
+ async def publish(self, code: str, vibe_name: str, description: str = "",
124
+ port: int = None) -> Dict:
125
+ """
126
+ Full Ghost Deploy:
127
+ 1. Create HF Space (permanent shadow-host)
128
+ 2. Upload code + agent card + archive
129
+ 3. Bring up Pinggy/Cloudflare tunnel (ephemeral)
130
+ Returns both URLs.
131
+ """
132
+ result = {"success": False, "space_url": None, "tunnel_url": None, "space_id": None}
133
+ space_id = self._make_id(vibe_name)
134
+ result["space_id"] = space_id
135
+
136
+ # ── PERMANENT: HF Space ──
137
+ space_url = None
138
+ if self.token:
139
+ try:
140
+ from huggingface_hub import HfApi, create_repo
141
+ api = HfApi()
142
+ full_id = f"dryymatt/{space_id}"
143
+
144
+ create_repo(full_id, repo_type="space", space_sdk="static",
145
+ private=False, exist_ok=True)
146
+
147
+ api.upload_file(
148
+ path_or_fileobj=code.encode(),
149
+ path_in_repo="index.html",
150
+ repo_id=full_id, repo_type="space",
151
+ commit_message="🧙‍♂️ Omni-Vibe Ghost Deploy",
152
+ )
153
+
154
+ # Agent card
155
+ agent = self.generate_agent_card(space_id, description, space_url or "")
156
+ api.upload_file(
157
+ path_or_fileobj=json.dumps(agent, indent=2).encode(),
158
+ path_in_repo=".well-known/agent.json",
159
+ repo_id=full_id, repo_type="space",
160
+ )
161
+ result["agent"] = agent
162
+
163
+ # Archive to sovereign registry
164
+ await self._archive(space_id, code, description)
165
+
166
+ space_url = f"https://dryymatt-{space_id}.hf.space"
167
+ result["space_url"] = space_url
168
+
169
+ except Exception as e:
170
+ result["space_error"] = str(e)
171
+
172
+ # ── EPHEMERAL: Pinggy Tunnel ──
173
+ if port:
174
+ try:
175
+ self._tunnel_url = await self.tunnel.up(port, timeout=8.0)
176
+ result["tunnel_url"] = self._tunnel_url
177
+ except Exception as e:
178
+ result["tunnel_error"] = str(e)
179
+
180
+ # ── LOCAL FALLBACK ──
181
+ if not space_url:
182
+ local = GEN_DIR / space_id
183
+ local.mkdir(parents=True, exist_ok=True)
184
+ (local / "index.html").write_text(code)
185
+ space_url = f"file://{local}/index.html"
186
+ result["space_url"] = space_url
187
+
188
+ result["success"] = bool(result.get("space_url"))
189
+ return result
190
+
191
+ async def _archive(self, space_id: str, code: str, description: str):
192
+ try:
193
+ from huggingface_hub import HfApi
194
+ api = HfApi()
195
+ api.upload_file(
196
+ path_or_fileobj=code.encode(),
197
+ path_in_repo=f"vibes/{space_id}/index.html",
198
+ repo_id=CANONICAL_REPO, repo_type="model",
199
+ )
200
+ manifest = json.dumps({
201
+ "space_id": space_id, "description": description,
202
+ "ts": time.time(), "protocol": "omni-vibe",
203
+ }, indent=2)
204
+ api.upload_file(
205
+ path_or_fileobj=manifest.encode(),
206
+ path_in_repo=f"vibes/{space_id}/manifest.json",
207
+ repo_id=CANONICAL_REPO, repo_type="model",
208
+ )
209
+ except Exception:
210
+ pass
211
+
212
+ def _make_id(self, name: str) -> str:
213
+ clean = "".join(c if c.isalnum() or c in "-_" else "-"
214
+ for c in name.lower()).strip("-")[:25] or "vibe"
215
+ return f"wv-{clean}-{uuid.uuid4().hex[:6]}"
216
+
217
+ def generate_agent_card(self, name: str, desc: str, url: str) -> Dict:
218
+ return {
219
+ "name": name,
220
+ "description": desc or f"Omni-Vibe Ghost Deploy: {name}",
221
+ "url": url,
222
+ "provider": {
223
+ "organization": "Omni-Vibe Studio — Litehat System",
224
+ "url": "https://huggingface.co/dryymatt",
225
+ },
226
+ "version": "2.0.0",
227
+ "a2aVersion": "1.0",
228
+ "capabilities": {
229
+ "streaming": True,
230
+ "ghostDeploy": True,
231
+ "liquidGlass": True,
232
+ "pinggy": True,
233
+ },
234
+ "skills": [
235
+ {
236
+ "id": "omni-vibe",
237
+ "name": "Omni-Vibe Generator",
238
+ "tags": ["full-stack", "zero-config", "postgres", "google-oauth", "liquid-glass"],
239
+ },
240
+ {
241
+ "id": "reflect-select",
242
+ "name": "Self-Healing",
243
+ "tags": ["reflect-select", "auditor", "validator"],
244
+ },
245
+ ],
246
+ }
247
+
248
+ async def list_vibes(self) -> list:
249
+ try:
250
+ from huggingface_hub import HfApi
251
+ files = HfApi().list_repo_files(CANONICAL_REPO, repo_type="model")
252
+ return sorted({f.split("/")[1] for f in files
253
+ if f.startswith("vibes/") and "/manifest.json" in f})
254
+ except Exception:
255
+ return []
256
+
257
+ def cleanup(self):
258
+ self.tunnel.down()
259
+
260
+
261
+ ghost = GhostDeploy()