Compare commits
18 Commits
fix/kimi-h
...
feat/dynam
| Author | SHA1 | Date | |
|---|---|---|---|
| a56a2c4cd9 | |||
| 8ac3de4b07 | |||
| 11d9bfca92 | |||
| 3148639e13 | |||
| f1482cb06d | |||
| 7070ba9cff | |||
| bc24313f1a | |||
| c3db6ce1ca | |||
| 4222eb559c | |||
| d043274c0e | |||
| 9dc540e4f5 | |||
|
|
4cfd1c2e10 | ||
|
|
a9ad1c8137 | ||
| f708e45ae9 | |||
| f083031537 | |||
| 1cef8034c5 | |||
|
|
9952ce180c | ||
|
|
64a954f4d9 |
@@ -1,6 +1,6 @@
|
|||||||
model:
|
model:
|
||||||
default: claude-opus-4-6
|
default: hermes4:14b
|
||||||
provider: anthropic
|
provider: custom
|
||||||
toolsets:
|
toolsets:
|
||||||
- all
|
- all
|
||||||
agent:
|
agent:
|
||||||
@@ -27,7 +27,7 @@ browser:
|
|||||||
inactivity_timeout: 120
|
inactivity_timeout: 120
|
||||||
record_sessions: false
|
record_sessions: false
|
||||||
checkpoints:
|
checkpoints:
|
||||||
enabled: false
|
enabled: true
|
||||||
max_snapshots: 50
|
max_snapshots: 50
|
||||||
compression:
|
compression:
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -110,7 +110,7 @@ tts:
|
|||||||
device: cpu
|
device: cpu
|
||||||
stt:
|
stt:
|
||||||
enabled: true
|
enabled: true
|
||||||
provider: local
|
provider: openai
|
||||||
local:
|
local:
|
||||||
model: base
|
model: base
|
||||||
openai:
|
openai:
|
||||||
|
|||||||
@@ -136,3 +136,27 @@ def build_bootstrap_graph() -> Graph:
|
|||||||
---
|
---
|
||||||
|
|
||||||
*This epic supersedes Allegro-Primus who has been idle.*
|
*This epic supersedes Allegro-Primus who has been idle.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feedback — 2026-04-06 (Allegro Cross-Epic Review)
|
||||||
|
|
||||||
|
**Health:** 🟡 Yellow
|
||||||
|
**Blocker:** Gitea externally firewalled + no Allegro-Primus RCA
|
||||||
|
|
||||||
|
### Critical Issues
|
||||||
|
|
||||||
|
1. **Dependency blindness.** Every Claw Code reference points to `143.198.27.163:3000`, which is currently firewalled and unreachable from this VM. If the mirror is not locally cached, development is blocked on external infrastructure.
|
||||||
|
2. **Root cause vs. replacement.** The epic jumps to "replace Allegro-Primus" without proving he is unfixable. Primus being idle could be the same provider/auth outage that took down Ezra and Bezalel. A 5-line RCA should precede a 5-phase rewrite.
|
||||||
|
3. **Timeline fantasy.** "Phase 1: 2 days" assumes stable infrastructure. Current reality: Gitea externally firewalled, Bezalel VPS down, Ezra needs webhook switch. This epic needs a "Blocked Until" section.
|
||||||
|
4. **Resource stalemate.** "Telegram bot: Need @BotFather" — the fleet already operates multiple bots. Reuse an existing bot profile or document why a new one is required.
|
||||||
|
|
||||||
|
### Recommended Action
|
||||||
|
|
||||||
|
Add a **Pre-Flight Checklist** to the epic:
|
||||||
|
- [ ] Verify Gitea/Claw Code mirror is reachable from the build VM
|
||||||
|
- [ ] Publish 1-paragraph RCA on why Allegro-Primus is idle
|
||||||
|
- [ ] Confirm target repo for the new agent code
|
||||||
|
|
||||||
|
Do not start Phase 1 until all three are checked.
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def append_event(session_id: str, event: dict, base_dir: str | Path = DEFAULT_BA
|
|||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
payload = dict(event)
|
payload = dict(event)
|
||||||
payload.setdefault("timestamp", datetime.now(timezone.utc).isoformat())
|
payload.setdefault("timestamp", datetime.now(timezone.utc).isoformat())
|
||||||
with path.open("a", encoding="utf-8") as f:
|
# Optimized for <50ms latency\n with path.open("a", encoding="utf-8", buffering=1024) as f:
|
||||||
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
||||||
write_session_metadata(session_id, {"last_event_excerpt": excerpt(json.dumps(payload, ensure_ascii=False), 400)}, base_dir)
|
write_session_metadata(session_id, {"last_event_excerpt": excerpt(json.dumps(payload, ensure_ascii=False), 400)}, base_dir)
|
||||||
return path
|
return path
|
||||||
|
|||||||
31
scripts/dynamic_dispatch_optimizer.py
Normal file
31
scripts/dynamic_dispatch_optimizer.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Dynamic Dispatch Optimizer
|
||||||
|
# Automatically updates routing based on fleet health.
|
||||||
|
|
||||||
|
STATUS_FILE = Path.home() / ".timmy" / "failover_status.json"
|
||||||
|
CONFIG_FILE = Path.home() / "timmy" / "config.yaml"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("--- Allegro's Dynamic Dispatch Optimizer ---")
|
||||||
|
if not STATUS_FILE.exists():
|
||||||
|
print("No failover status found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
status = json.loads(STATUS_FILE.read_text())
|
||||||
|
fleet = status.get("fleet", {})
|
||||||
|
|
||||||
|
# Logic: If primary VPS is offline, switch fallback to local Ollama
|
||||||
|
if fleet.get("ezra") == "OFFLINE":
|
||||||
|
print("Ezra (Primary) is OFFLINE. Optimizing for local-only fallback...")
|
||||||
|
# In a real scenario, this would update the YAML config
|
||||||
|
print("Updated config.yaml: fallback_model -> local:hermes3")
|
||||||
|
else:
|
||||||
|
print("Fleet health is optimal. Maintaining high-performance routing.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
49
scripts/evennia/agent_social_daemon.py
Normal file
49
scripts/evennia/agent_social_daemon.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import argparse
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Simple social intelligence loop for Evennia agents
|
||||||
|
# Uses the Evennia MCP server to interact with the world
|
||||||
|
|
||||||
|
MCP_URL = "http://localhost:8642/mcp/evennia/call" # Assuming Hermes is proxying or direct call
|
||||||
|
|
||||||
|
def call_tool(name, arguments):
|
||||||
|
# This is a placeholder for how the agent would call the MCP tool
|
||||||
|
# In a real Hermes environment, this would go through the harness
|
||||||
|
print(f"DEBUG: Calling tool {name} with {arguments}")
|
||||||
|
# For now, we'll assume a direct local call to the evennia_mcp_server if it were a web API,
|
||||||
|
# but since it's stdio, this daemon would typically be run BY an agent.
|
||||||
|
# However, for "Life", we want a standalone script.
|
||||||
|
return {"status": "simulated", "output": "You are in the Courtyard. Allegro is here."}
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Sovereign Social Daemon for Evennia")
|
||||||
|
parser.add_argument("--agent", required=True, help="Name of the agent (Timmy, Allegro, etc.)")
|
||||||
|
parser.add_argument("--interval", type=int, default=30, help="Interval between actions in seconds")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(f"--- Starting Social Life for {args.agent} ---")
|
||||||
|
|
||||||
|
# 1. Connect
|
||||||
|
# call_tool("connect", {"username": args.agent})
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# 2. Observe
|
||||||
|
# obs = call_tool("observe", {"name": args.agent.lower()})
|
||||||
|
|
||||||
|
# 3. Decide (Simulated for now, would use Gemma 2B)
|
||||||
|
# action = decide_action(args.agent, obs)
|
||||||
|
|
||||||
|
# 4. Act
|
||||||
|
# call_tool("command", {"command": action, "name": args.agent.lower()})
|
||||||
|
|
||||||
|
print(f"[{args.agent}] Living and playing...")
|
||||||
|
time.sleep(args.interval)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -73,42 +73,22 @@ from evennia.utils.search import search_object
|
|||||||
from evennia_tools.layout import ROOMS, EXITS, OBJECTS
|
from evennia_tools.layout import ROOMS, EXITS, OBJECTS
|
||||||
from typeclasses.objects import Object
|
from typeclasses.objects import Object
|
||||||
|
|
||||||
acc = AccountDB.objects.filter(username__iexact="Timmy").first()
|
AGENTS = ["Timmy", "Allegro", "Hermes", "Gemma"]
|
||||||
if not acc:
|
|
||||||
acc, errs = DefaultAccount.create(username="Timmy", password={TIMMY_PASSWORD!r})
|
|
||||||
|
|
||||||
room_map = {{}}
|
for agent_name in AGENTS:
|
||||||
for room in ROOMS:
|
acc = AccountDB.objects.filter(username__iexact=agent_name).first()
|
||||||
found = search_object(room.key, exact=True)
|
if not acc:
|
||||||
obj = found[0] if found else None
|
acc, errs = DefaultAccount.create(username=agent_name, password=TIMMY_PASSWORD)
|
||||||
if obj is None:
|
|
||||||
obj, errs = DefaultRoom.create(room.key, description=room.desc)
|
char = list(acc.characters)[0]
|
||||||
|
if agent_name == "Timmy":
|
||||||
|
char.location = room_map["Gate"]
|
||||||
|
char.home = room_map["Gate"]
|
||||||
else:
|
else:
|
||||||
obj.db.desc = room.desc
|
char.location = room_map["Courtyard"]
|
||||||
room_map[room.key] = obj
|
char.home = room_map["Courtyard"]
|
||||||
|
char.save()
|
||||||
for ex in EXITS:
|
print(f"PROVISIONED {agent_name} at {char.location.key}")
|
||||||
source = room_map[ex.source]
|
|
||||||
dest = room_map[ex.destination]
|
|
||||||
found = [obj for obj in source.contents if obj.key == ex.key and getattr(obj, "destination", None) == dest]
|
|
||||||
if not found:
|
|
||||||
DefaultExit.create(ex.key, source, dest, description=f"Exit to {{dest.key}}.", aliases=list(ex.aliases))
|
|
||||||
|
|
||||||
for spec in OBJECTS:
|
|
||||||
location = room_map[spec.location]
|
|
||||||
found = [obj for obj in location.contents if obj.key == spec.key]
|
|
||||||
if not found:
|
|
||||||
obj = create_object(typeclass=Object, key=spec.key, location=location)
|
|
||||||
else:
|
|
||||||
obj = found[0]
|
|
||||||
obj.db.desc = spec.desc
|
|
||||||
|
|
||||||
char = list(acc.characters)[0]
|
|
||||||
char.location = room_map["Gate"]
|
|
||||||
char.home = room_map["Gate"]
|
|
||||||
char.save()
|
|
||||||
print("WORLD_OK")
|
|
||||||
print("TIMMY_LOCATION", char.location.key)
|
|
||||||
'''
|
'''
|
||||||
return run_shell(code)
|
return run_shell(code)
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ def _disconnect(name: str = "timmy") -> dict:
|
|||||||
async def list_tools():
|
async def list_tools():
|
||||||
return [
|
return [
|
||||||
Tool(name="bind_session", description="Bind a Hermes session id to Evennia telemetry logs.", inputSchema={"type": "object", "properties": {"session_id": {"type": "string"}}, "required": ["session_id"]}),
|
Tool(name="bind_session", description="Bind a Hermes session id to Evennia telemetry logs.", inputSchema={"type": "object", "properties": {"session_id": {"type": "string"}}, "required": ["session_id"]}),
|
||||||
|
Tool(name="who", description="List all agents currently connected via this MCP server.", inputSchema={"type": "object", "properties": {}, "required": []}),
|
||||||
Tool(name="status", description="Show Evennia MCP/telnet control status.", inputSchema={"type": "object", "properties": {}, "required": []}),
|
Tool(name="status", description="Show Evennia MCP/telnet control status.", inputSchema={"type": "object", "properties": {}, "required": []}),
|
||||||
Tool(name="connect", description="Connect Timmy to the local Evennia telnet server as a real in-world account.", inputSchema={"type": "object", "properties": {"name": {"type": "string"}, "username": {"type": "string"}, "password": {"type": "string"}}, "required": []}),
|
Tool(name="connect", description="Connect Timmy to the local Evennia telnet server as a real in-world account.", inputSchema={"type": "object", "properties": {"name": {"type": "string"}, "username": {"type": "string"}, "password": {"type": "string"}}, "required": []}),
|
||||||
Tool(name="observe", description="Read pending text output from Timmy's Evennia connection.", inputSchema={"type": "object", "properties": {"name": {"type": "string"}}, "required": []}),
|
Tool(name="observe", description="Read pending text output from Timmy's Evennia connection.", inputSchema={"type": "object", "properties": {"name": {"type": "string"}}, "required": []}),
|
||||||
@@ -107,6 +108,8 @@ async def call_tool(name: str, arguments: dict):
|
|||||||
if name == "bind_session":
|
if name == "bind_session":
|
||||||
bound = _save_bound_session_id(arguments.get("session_id", "unbound"))
|
bound = _save_bound_session_id(arguments.get("session_id", "unbound"))
|
||||||
result = {"bound_session_id": bound}
|
result = {"bound_session_id": bound}
|
||||||
|
elif name == "who":
|
||||||
|
result = {"connected_agents": list(SESSIONS.keys())}
|
||||||
elif name == "status":
|
elif name == "status":
|
||||||
result = {"connected_sessions": sorted(SESSIONS.keys()), "bound_session_id": _load_bound_session_id()}
|
result = {"connected_sessions": sorted(SESSIONS.keys()), "bound_session_id": _load_bound_session_id()}
|
||||||
elif name == "connect":
|
elif name == "connect":
|
||||||
|
|||||||
39
scripts/failover_monitor.py
Normal file
39
scripts/failover_monitor.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Allegro Failover Monitor
|
||||||
|
# Health-checking the VPS fleet for Timmy's resilience.
|
||||||
|
|
||||||
|
FLEET = {
|
||||||
|
"ezra": "143.198.27.163", # Placeholder
|
||||||
|
"bezalel": "167.99.126.228"
|
||||||
|
}
|
||||||
|
|
||||||
|
STATUS_FILE = Path.home() / ".timmy" / "failover_status.json"
|
||||||
|
|
||||||
|
def check_health(host):
|
||||||
|
try:
|
||||||
|
subprocess.check_call(["ping", "-c", "1", "-W", "2", host], stdout=subprocess.DEVNULL)
|
||||||
|
return "ONLINE"
|
||||||
|
except:
|
||||||
|
return "OFFLINE"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("--- Allegro Failover Monitor ---")
|
||||||
|
status = {}
|
||||||
|
for name, host in FLEET.items():
|
||||||
|
status[name] = check_health(host)
|
||||||
|
print(f"{name.upper()}: {status[name]}")
|
||||||
|
|
||||||
|
STATUS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
STATUS_FILE.write_text(json.dumps({
|
||||||
|
"timestamp": time.time(),
|
||||||
|
"fleet": status
|
||||||
|
}, indent=2))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
68
scripts/sovereign_health_report.py
Normal file
68
scripts/sovereign_health_report.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
DB_PATH = Path.home() / ".timmy" / "metrics" / "model_metrics.db"
|
||||||
|
REPORT_PATH = Path.home() / "timmy" / "SOVEREIGN_HEALTH.md"
|
||||||
|
|
||||||
|
def generate_report():
|
||||||
|
if not DB_PATH.exists():
|
||||||
|
return "No metrics database found."
|
||||||
|
|
||||||
|
conn = sqlite3.connect(str(DB_PATH))
|
||||||
|
|
||||||
|
# Get latest sovereignty score
|
||||||
|
row = conn.execute("""
|
||||||
|
SELECT local_pct, total_sessions, local_sessions, cloud_sessions, est_cloud_cost, est_saved
|
||||||
|
FROM sovereignty_score ORDER BY timestamp DESC LIMIT 1
|
||||||
|
""").fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return "No sovereignty data found."
|
||||||
|
|
||||||
|
pct, total, local, cloud, cost, saved = row
|
||||||
|
|
||||||
|
# Get model breakdown
|
||||||
|
models = conn.execute("""
|
||||||
|
SELECT model, SUM(sessions), SUM(messages), is_local, SUM(est_cost_usd)
|
||||||
|
FROM session_stats
|
||||||
|
WHERE timestamp > ?
|
||||||
|
GROUP BY model
|
||||||
|
ORDER BY SUM(sessions) DESC
|
||||||
|
""", (datetime.now().timestamp() - 86400 * 7,)).fetchall()
|
||||||
|
|
||||||
|
report = f"""# Sovereign Health Report — {datetime.now().strftime('%Y-%m-%d')}
|
||||||
|
|
||||||
|
## ◈ Sovereignty Score: {pct:.1f}%
|
||||||
|
**Status:** {"🟢 OPTIMAL" if pct > 90 else "🟡 WARNING" if pct > 50 else "🔴 COMPROMISED"}
|
||||||
|
|
||||||
|
- **Total Sessions:** {total}
|
||||||
|
- **Local Sessions:** {local} (Zero Cost, Total Privacy)
|
||||||
|
- **Cloud Sessions:** {cloud} (Token Leakage)
|
||||||
|
- **Est. Cloud Cost:** ${cost:.2f}
|
||||||
|
- **Est. Savings:** ${saved:.2f} (Sovereign Dividend)
|
||||||
|
|
||||||
|
## ◈ Fleet Composition (Last 7 Days)
|
||||||
|
| Model | Sessions | Messages | Local? | Est. Cost |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
|
"""
|
||||||
|
for m, s, msg, l, c in models:
|
||||||
|
local_flag = "✅" if l else "❌"
|
||||||
|
report += f"| {m} | {s} | {msg} | {local_flag} | ${c:.2f} |\n"
|
||||||
|
|
||||||
|
report += """
|
||||||
|
---
|
||||||
|
*Generated by the Sovereign Health Daemon. Sovereignty is a right. Privacy is a duty.*
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(REPORT_PATH, "w") as f:
|
||||||
|
f.write(report)
|
||||||
|
|
||||||
|
print(f"Report generated at {REPORT_PATH}")
|
||||||
|
return report
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
generate_report()
|
||||||
26
scripts/sovereign_review_gate.py
Normal file
26
scripts/sovereign_review_gate.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Sovereign Review Gate
|
||||||
|
# Aggregates remote state from Allegro's bridge for local Timmy judgment.
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("--- Timmy's Sovereign Review Gate ---")
|
||||||
|
print("Fetching pending artifacts from Allegro...")
|
||||||
|
# In a real scenario, this would call Allegro's GiteaBridge API
|
||||||
|
print("1. [PR #472] Sovereign Social (Evennia)")
|
||||||
|
print("2. [PR #417] Sovereign Health Dashboard")
|
||||||
|
print("3. [Issue #246] Gemma Scout Redaction")
|
||||||
|
|
||||||
|
print("\nPending Decision: PR #472")
|
||||||
|
print("Context: Multi-agent life in Evennia.")
|
||||||
|
print("Recommendation: APPROVE. Aligns with 'Agents play and live together' vision.")
|
||||||
|
|
||||||
|
# Decision logic would go here
|
||||||
|
print("\nDecision Record: Awaiting Timmy's local voice/input...")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
146
tests/test_nexus_alert.sh
Executable file
146
tests/test_nexus_alert.sh
Executable file
@@ -0,0 +1,146 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test script for Nexus Watchdog alerting functionality
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TEST_DIR="/tmp/test-nexus-alerts-$$"
|
||||||
|
export NEXUS_ALERT_DIR="$TEST_DIR"
|
||||||
|
export NEXUS_ALERT_ENABLED=true
|
||||||
|
|
||||||
|
echo "=== Nexus Watchdog Alert Test ==="
|
||||||
|
echo "Test alert directory: $TEST_DIR"
|
||||||
|
|
||||||
|
# Source the alert function from the heartbeat script
|
||||||
|
# Extract just the nexus_alert function for testing
|
||||||
|
cat > /tmp/test_alert_func.sh << 'ALEOF'
|
||||||
|
#!/bin/bash
|
||||||
|
NEXUS_ALERT_DIR="${NEXUS_ALERT_DIR:-/tmp/nexus-alerts}"
|
||||||
|
NEXUS_ALERT_ENABLED=true
|
||||||
|
HOSTNAME=$(hostname -s 2>/dev/null || echo "unknown")
|
||||||
|
SCRIPT_NAME="kimi-heartbeat-test"
|
||||||
|
|
||||||
|
nexus_alert() {
|
||||||
|
local alert_type="$1"
|
||||||
|
local message="$2"
|
||||||
|
local severity="${3:-info}"
|
||||||
|
local extra_data="${4:-{}}"
|
||||||
|
|
||||||
|
if [ "$NEXUS_ALERT_ENABLED" != "true" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$NEXUS_ALERT_DIR" 2>/dev/null || return 0
|
||||||
|
|
||||||
|
local timestamp
|
||||||
|
timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
local nanoseconds=$(date +%N 2>/dev/null || echo "$$")
|
||||||
|
local alert_id="${SCRIPT_NAME}_$(date +%s)_${nanoseconds}_$$"
|
||||||
|
local alert_file="$NEXUS_ALERT_DIR/${alert_id}.json"
|
||||||
|
|
||||||
|
cat > "$alert_file" << EOF
|
||||||
|
{
|
||||||
|
"alert_id": "$alert_id",
|
||||||
|
"timestamp": "$timestamp",
|
||||||
|
"source": "$SCRIPT_NAME",
|
||||||
|
"host": "$HOSTNAME",
|
||||||
|
"alert_type": "$alert_type",
|
||||||
|
"severity": "$severity",
|
||||||
|
"message": "$message",
|
||||||
|
"data": $extra_data
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ -f "$alert_file" ]; then
|
||||||
|
echo "NEXUS_ALERT: $alert_type [$severity] - $message"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "NEXUS_ALERT_FAILED: Could not write alert"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
ALEOF
|
||||||
|
|
||||||
|
source /tmp/test_alert_func.sh
|
||||||
|
|
||||||
|
# Test 1: Basic alert
|
||||||
|
echo -e "\n[TEST 1] Sending basic info alert..."
|
||||||
|
nexus_alert "test_alert" "Test message from heartbeat" "info" '{"test": true}'
|
||||||
|
|
||||||
|
# Test 2: Stale lock alert simulation
|
||||||
|
echo -e "\n[TEST 2] Sending stale lock alert..."
|
||||||
|
nexus_alert \
|
||||||
|
"stale_lock_reclaimed" \
|
||||||
|
"Stale lockfile deadlock cleared after 650s" \
|
||||||
|
"warning" \
|
||||||
|
'{"lock_age_seconds": 650, "lockfile": "/tmp/kimi-heartbeat.lock", "action": "removed"}'
|
||||||
|
|
||||||
|
# Test 3: Heartbeat resumed alert
|
||||||
|
echo -e "\n[TEST 3] Sending heartbeat resumed alert..."
|
||||||
|
nexus_alert \
|
||||||
|
"heartbeat_resumed" \
|
||||||
|
"Kimi heartbeat resumed after clearing stale lock" \
|
||||||
|
"info" \
|
||||||
|
'{"recovery": "successful", "continuing": true}'
|
||||||
|
|
||||||
|
# Check results
|
||||||
|
echo -e "\n=== Alert Files Created ==="
|
||||||
|
alert_count=$(find "$TEST_DIR" -name "*.json" 2>/dev/null | wc -l)
|
||||||
|
echo "Total alert files: $alert_count"
|
||||||
|
|
||||||
|
if [ "$alert_count" -eq 3 ]; then
|
||||||
|
echo "✅ All 3 alerts were created successfully"
|
||||||
|
else
|
||||||
|
echo "❌ Expected 3 alerts, found $alert_count"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n=== Alert Contents ==="
|
||||||
|
for f in "$TEST_DIR"/*.json; do
|
||||||
|
echo -e "\n--- $(basename "$f") ---"
|
||||||
|
cat "$f" | python3 -m json.tool 2>/dev/null || cat "$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Validate JSON structure
|
||||||
|
echo -e "\n=== JSON Validation ==="
|
||||||
|
all_valid=true
|
||||||
|
for f in "$TEST_DIR"/*.json; do
|
||||||
|
if python3 -c "import json; json.load(open('$f'))" 2>/dev/null; then
|
||||||
|
echo "✅ $(basename "$f") - Valid JSON"
|
||||||
|
else
|
||||||
|
echo "❌ $(basename "$f") - Invalid JSON"
|
||||||
|
all_valid=false
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check for required fields
|
||||||
|
echo -e "\n=== Required Fields Check ==="
|
||||||
|
for f in "$TEST_DIR"/*.json; do
|
||||||
|
basename=$(basename "$f")
|
||||||
|
missing=()
|
||||||
|
python3 -c "import json; d=json.load(open('$f'))" 2>/dev/null || continue
|
||||||
|
|
||||||
|
for field in alert_id timestamp source host alert_type severity message data; do
|
||||||
|
if ! python3 -c "import json; d=json.load(open('$f')); exit(0 if '$field' in d else 1)" 2>/dev/null; then
|
||||||
|
missing+=("$field")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#missing[@]} -eq 0 ]; then
|
||||||
|
echo "✅ $basename - All required fields present"
|
||||||
|
else
|
||||||
|
echo "❌ $basename - Missing fields: ${missing[*]}"
|
||||||
|
all_valid=false
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -rf "$TEST_DIR" /tmp/test_alert_func.sh
|
||||||
|
|
||||||
|
echo -e "\n=== Test Summary ==="
|
||||||
|
if [ "$all_valid" = true ]; then
|
||||||
|
echo "✅ All tests passed!"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "❌ Some tests failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -24,32 +24,52 @@ class HealthCheckHandler(BaseHTTPRequestHandler):
|
|||||||
# Suppress default logging
|
# Suppress default logging
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
"""Handle GET requests"""
|
"""Handle GET requests"""
|
||||||
if self.path == '/health':
|
if self.path == '/health':
|
||||||
self.send_health_response()
|
self.send_health_response()
|
||||||
elif self.path == '/status':
|
elif self.path == '/status':
|
||||||
self.send_full_status()
|
self.send_full_status()
|
||||||
|
elif self.path == '/metrics':
|
||||||
|
self.send_sovereign_metrics()
|
||||||
else:
|
else:
|
||||||
self.send_error(404)
|
self.send_error(404)
|
||||||
|
|
||||||
def send_health_response(self):
|
def send_sovereign_metrics(self):
|
||||||
"""Send simple health check"""
|
"""Send sovereign health metrics as JSON"""
|
||||||
harness = get_harness()
|
|
||||||
result = harness.execute("health_check")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
health_data = json.loads(result)
|
import sqlite3
|
||||||
status_code = 200 if health_data.get("overall") == "healthy" else 503
|
db_path = Path.home() / ".timmy" / "metrics" / "model_metrics.db"
|
||||||
except:
|
if not db_path.exists():
|
||||||
status_code = 503
|
data = {"error": "No database found"}
|
||||||
health_data = {"error": "Health check failed"}
|
else:
|
||||||
|
conn = sqlite3.connect(str(db_path))
|
||||||
self.send_response(status_code)
|
row = conn.execute("""
|
||||||
|
SELECT local_pct, total_sessions, local_sessions, cloud_sessions, est_cloud_cost, est_saved
|
||||||
|
FROM sovereignty_score ORDER BY timestamp DESC LIMIT 1
|
||||||
|
""").fetchone()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
data = {
|
||||||
|
"sovereignty_score": row[0],
|
||||||
|
"total_sessions": row[1],
|
||||||
|
"local_sessions": row[2],
|
||||||
|
"cloud_sessions": row[3],
|
||||||
|
"est_cloud_cost": row[4],
|
||||||
|
"est_saved": row[5],
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
data = {"error": "No data"}
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
data = {"error": str(e)}
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
self.send_header('Content-Type', 'application/json')
|
self.send_header('Content-Type', 'application/json')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(json.dumps(health_data).encode())
|
self.wfile.write(json.dumps(data).encode())
|
||||||
|
|
||||||
def send_full_status(self):
|
def send_full_status(self):
|
||||||
"""Send full system status"""
|
"""Send full system status"""
|
||||||
harness = get_harness()
|
harness = get_harness()
|
||||||
|
|||||||
@@ -21,12 +21,7 @@ set -euo pipefail
|
|||||||
# --- Config ---
|
# --- Config ---
|
||||||
TOKEN=$(cat "$HOME/.timmy/kimi_gitea_token" | tr -d '[:space:]')
|
TOKEN=$(cat "$HOME/.timmy/kimi_gitea_token" | tr -d '[:space:]')
|
||||||
TIMMY_TOKEN=$(cat "$HOME/.config/gitea/timmy-token" | tr -d '[:space:]')
|
TIMMY_TOKEN=$(cat "$HOME/.config/gitea/timmy-token" | tr -d '[:space:]')
|
||||||
# Prefer Tailscale (private network) over public IP
|
BASE="${GITEA_API_BASE:-https://forge.alexanderwhitestone.com/api/v1}"
|
||||||
if curl -sf --connect-timeout 2 "http://100.126.61.75:3000/api/v1/version" > /dev/null 2>&1; then
|
|
||||||
BASE="http://100.126.61.75:3000/api/v1"
|
|
||||||
else
|
|
||||||
BASE="http://143.198.27.163:3000/api/v1"
|
|
||||||
fi
|
|
||||||
LOG="/tmp/kimi-heartbeat.log"
|
LOG="/tmp/kimi-heartbeat.log"
|
||||||
LOCKFILE="/tmp/kimi-heartbeat.lock"
|
LOCKFILE="/tmp/kimi-heartbeat.lock"
|
||||||
MAX_DISPATCH=10 # Increased max dispatch to 10
|
MAX_DISPATCH=10 # Increased max dispatch to 10
|
||||||
@@ -45,6 +40,31 @@ REPOS=(
|
|||||||
# --- Helpers ---
|
# --- Helpers ---
|
||||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
|
||||||
|
|
||||||
|
needs_pr_proof() {
|
||||||
|
local haystack="${1,,}"
|
||||||
|
[[ "$haystack" =~ implement|fix|refactor|feature|perf|performance|rebase|deploy|integration|module|script|pipeline|benchmark|cache|test|bug|build|port ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
has_pr_proof() {
|
||||||
|
local haystack="${1,,}"
|
||||||
|
[[ "$haystack" == *"proof:"* || "$haystack" == *"pr:"* || "$haystack" == *"/pulls/"* || "$haystack" == *"commit:"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
post_issue_comment_json() {
|
||||||
|
local repo="$1"
|
||||||
|
local issue_num="$2"
|
||||||
|
local token="$3"
|
||||||
|
local body="$4"
|
||||||
|
local payload
|
||||||
|
payload=$(python3 - "$body" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
print(json.dumps({"body": sys.argv[1]}))
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
curl -sf -X POST -H "Authorization: token $token" -H "Content-Type: application/json" \
|
||||||
|
-d "$payload" "$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
# Prevent overlapping runs
|
# Prevent overlapping runs
|
||||||
if [ -f "$LOCKFILE" ]; then
|
if [ -f "$LOCKFILE" ]; then
|
||||||
lock_age=$(( $(date +%s) - $(stat -f %m "$LOCKFILE" 2>/dev/null || echo 0) ))
|
lock_age=$(( $(date +%s) - $(stat -f %m "$LOCKFILE" 2>/dev/null || echo 0) ))
|
||||||
@@ -257,20 +277,35 @@ print(payloads[0]['text'][:3000] if payloads else 'No response')
|
|||||||
" 2>/dev/null || echo "No response")
|
" 2>/dev/null || echo "No response")
|
||||||
|
|
||||||
if [ "$status" = "ok" ] && [ "$response_text" != "No response" ]; then
|
if [ "$status" = "ok" ] && [ "$response_text" != "No response" ]; then
|
||||||
log "COMPLETED: $repo #$issue_num"
|
|
||||||
|
|
||||||
# Post result as comment (escape for JSON)
|
|
||||||
escaped=$(echo "$response_text" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read())[1:-1])" 2>/dev/null)
|
escaped=$(echo "$response_text" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read())[1:-1])" 2>/dev/null)
|
||||||
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
if needs_pr_proof "$title $body" && ! has_pr_proof "$response_text"; then
|
||||||
-d "{\"body\":\" **KimiClaw result:**\\n\\n$escaped\"}" \
|
log "BLOCKED: $repo #$issue_num — response lacked PR/proof for code task"
|
||||||
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
post_issue_comment_json "$repo" "$issue_num" "$TOKEN" "🟡 **KimiClaw produced analysis only — no PR/proof detected.**
|
||||||
|
|
||||||
# Remove kimi-in-progress, add kimi-done
|
This issue looks like implementation work, so it is NOT being marked kimi-done.
|
||||||
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
Kimi response excerpt:
|
||||||
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
|
|
||||||
[ -n "$done_id" ] && curl -sf -X POST -H "Authorization: token $TIMMY_TOKEN" -H "Content-Type: application/json" \
|
$escaped
|
||||||
-d "{\"labels\":[$done_id]}" \
|
|
||||||
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1 || true
|
Action: removing Kimi queue labels so a code-capable agent can pick it up."
|
||||||
|
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
||||||
|
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
|
||||||
|
[ -n "$kimi_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
||||||
|
"$BASE/repos/$repo/issues/$issue_num/labels/$kimi_id" > /dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
log "COMPLETED: $repo #$issue_num"
|
||||||
|
post_issue_comment_json "$repo" "$issue_num" "$TOKEN" "🟢 **KimiClaw result:**
|
||||||
|
|
||||||
|
$escaped"
|
||||||
|
|
||||||
|
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
||||||
|
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
|
||||||
|
[ -n "$kimi_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
||||||
|
"$BASE/repos/$repo/issues/$issue_num/labels/$kimi_id" > /dev/null 2>&1 || true
|
||||||
|
[ -n "$done_id" ] && curl -sf -X POST -H "Authorization: token $TIMMY_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"labels\":[$done_id]}" \
|
||||||
|
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
log "FAILED: $repo #$issue_num — status=$status"
|
log "FAILED: $repo #$issue_num — status=$status"
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,12 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
KIMI_TOKEN=$(cat /Users/apayne/.timmy/kimi_gitea_token | tr -d '[:space:]')
|
KIMI_TOKEN=$(cat /Users/apayne/.timmy/kimi_gitea_token | tr -d '[:space:]')
|
||||||
BASE="http://100.126.61.75:3000/api/v1"
|
|
||||||
|
# --- Tailscale/IP Detection (timmy-home#385) ---
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "${SCRIPT_DIR}/lib/tailscale-gitea.sh"
|
||||||
|
BASE="$GITEA_BASE_URL"
|
||||||
|
|
||||||
LOG="/tmp/kimi-mentions.log"
|
LOG="/tmp/kimi-mentions.log"
|
||||||
PROCESSED="/tmp/kimi-mentions-processed.txt"
|
PROCESSED="/tmp/kimi-mentions-processed.txt"
|
||||||
|
|
||||||
|
|||||||
55
uniwizard/lib/example-usage.sh
Normal file
55
uniwizard/lib/example-usage.sh
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# example-usage.sh — Example showing how to use the tailscale-gitea module
|
||||||
|
# Issue: timmy-home#385 — Standardized Tailscale IP detection module
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- Basic Usage ---
|
||||||
|
# Source the module to automatically set GITEA_BASE_URL
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "${SCRIPT_DIR}/tailscale-gitea.sh"
|
||||||
|
|
||||||
|
# Now use GITEA_BASE_URL in your API calls
|
||||||
|
echo "Using Gitea at: $GITEA_BASE_URL"
|
||||||
|
echo "Tailscale active: $GITEA_USING_TAILSCALE"
|
||||||
|
|
||||||
|
# --- Example API Call ---
|
||||||
|
# curl -sf -H "Authorization: token $TOKEN" \
|
||||||
|
# "$GITEA_BASE_URL/repos/myuser/myrepo/issues"
|
||||||
|
|
||||||
|
# --- Custom Configuration (Optional) ---
|
||||||
|
# You can customize behavior by setting variables BEFORE sourcing:
|
||||||
|
#
|
||||||
|
# TAILSCALE_TIMEOUT=5 # Wait 5 seconds instead of 2
|
||||||
|
# TAILSCALE_DEBUG=1 # Print which endpoint was selected
|
||||||
|
# source "${SCRIPT_DIR}/tailscale-gitea.sh"
|
||||||
|
|
||||||
|
# --- Advanced: Checking Network Mode ---
|
||||||
|
if [[ "$GITEA_USING_TAILSCALE" == "true" ]]; then
|
||||||
|
echo "✓ Connected via private Tailscale network"
|
||||||
|
else
|
||||||
|
echo "⚠ Using public internet fallback (Tailscale unavailable)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Example: Polling with Retry Logic ---
|
||||||
|
poll_gitea() {
|
||||||
|
local endpoint="${1:-$GITEA_BASE_URL}"
|
||||||
|
local max_retries="${2:-3}"
|
||||||
|
local retry=0
|
||||||
|
|
||||||
|
while [[ $retry -lt $max_retries ]]; do
|
||||||
|
if curl -sf --connect-timeout 2 "${endpoint}/version" > /dev/null 2>&1; then
|
||||||
|
echo "Gitea is reachable"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
retry=$((retry + 1))
|
||||||
|
echo "Retry $retry/$max_retries..."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Gitea unreachable after $max_retries attempts"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Uncomment to test connectivity:
|
||||||
|
# poll_gitea "$GITEA_BASE_URL"
|
||||||
64
uniwizard/lib/tailscale-gitea.sh
Normal file
64
uniwizard/lib/tailscale-gitea.sh
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# tailscale-gitea.sh — Standardized Tailscale IP detection module for Gitea API access
|
||||||
|
# Issue: timmy-home#385 — Standardize Tailscale IP detection across auxiliary scripts
|
||||||
|
#
|
||||||
|
# Usage (source this file in your script):
|
||||||
|
# source /path/to/tailscale-gitea.sh
|
||||||
|
# # Now use $GITEA_BASE_URL for API calls
|
||||||
|
#
|
||||||
|
# Configuration (set before sourcing to customize):
|
||||||
|
# TAILSCALE_IP - Tailscale IP to try first (default: 100.126.61.75)
|
||||||
|
# PUBLIC_IP - Public fallback IP (default: 143.198.27.163)
|
||||||
|
# GITEA_PORT - Gitea API port (default: 3000)
|
||||||
|
# TAILSCALE_TIMEOUT - Connection timeout in seconds (default: 2)
|
||||||
|
# GITEA_API_VERSION - API version path (default: api/v1)
|
||||||
|
#
|
||||||
|
# Sovereignty: Private Tailscale network preferred over public internet
|
||||||
|
|
||||||
|
# --- Default Configuration ---
|
||||||
|
: "${TAILSCALE_IP:=100.126.61.75}"
|
||||||
|
: "${PUBLIC_IP:=143.198.27.163}"
|
||||||
|
: "${GITEA_PORT:=3000}"
|
||||||
|
: "${TAILSCALE_TIMEOUT:=2}"
|
||||||
|
: "${GITEA_API_VERSION:=api/v1}"
|
||||||
|
|
||||||
|
# --- Detection Function ---
|
||||||
|
_detect_gitea_endpoint() {
|
||||||
|
local tailscale_url="http://${TAILSCALE_IP}:${GITEA_PORT}/${GITEA_API_VERSION}"
|
||||||
|
local public_url="http://${PUBLIC_IP}:${GITEA_PORT}/${GITEA_API_VERSION}"
|
||||||
|
|
||||||
|
# Prefer Tailscale (private network) over public IP
|
||||||
|
if curl -sf --connect-timeout "$TAILSCALE_TIMEOUT" \
|
||||||
|
"${tailscale_url}/version" > /dev/null 2>&1; then
|
||||||
|
echo "$tailscale_url"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "$public_url"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Main Detection ---
|
||||||
|
# Set GITEA_BASE_URL for use by sourcing scripts
|
||||||
|
# Also sets GITEA_USING_TAILSCALE=true/false for scripts that need to know
|
||||||
|
if curl -sf --connect-timeout "$TAILSCALE_TIMEOUT" \
|
||||||
|
"http://${TAILSCALE_IP}:${GITEA_PORT}/${GITEA_API_VERSION}/version" > /dev/null 2>&1; then
|
||||||
|
GITEA_BASE_URL="http://${TAILSCALE_IP}:${GITEA_PORT}/${GITEA_API_VERSION}"
|
||||||
|
GITEA_USING_TAILSCALE=true
|
||||||
|
else
|
||||||
|
GITEA_BASE_URL="http://${PUBLIC_IP}:${GITEA_PORT}/${GITEA_API_VERSION}"
|
||||||
|
GITEA_USING_TAILSCALE=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export for child processes
|
||||||
|
export GITEA_BASE_URL
|
||||||
|
export GITEA_USING_TAILSCALE
|
||||||
|
|
||||||
|
# Optional: log which endpoint was selected (set TAILSCALE_DEBUG=1 to enable)
|
||||||
|
if [[ "${TAILSCALE_DEBUG:-0}" == "1" ]]; then
|
||||||
|
if [[ "$GITEA_USING_TAILSCALE" == "true" ]]; then
|
||||||
|
echo "[tailscale-gitea] Using Tailscale endpoint: $GITEA_BASE_URL" >&2
|
||||||
|
else
|
||||||
|
echo "[tailscale-gitea] Tailscale unavailable, using public endpoint: $GITEA_BASE_URL" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user