This commit is contained in:
2026-05-25 16:03:00 +08:00
parent da81c57db2
commit 23ec354eb5
8 changed files with 863 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
One-shot: create a dedicated NewAPI sk- token for the downstream
"蜂群程序" (swarm bee program) and print the base URL + key.
Why direct DB INSERT instead of the /api/token POST endpoint:
- that endpoint requires a logged-in admin session cookie we
don't have a clean way to mint headless;
- the token table shape is stable (new-api fork has tracked
these columns for two years).
Safety:
- Attaches the token to the platform owner user (whose email
we look up first).
- Marks UnlimitedQuota=true so this token consumes user-row
quota (no separate billing surface to manage).
- HideFromUserUI=false so the operator can revoke it from the
/keys page if needed.
- Sets a descriptive Name + Remark per the user's "备注好"
requirement.
"""
import os
import sys
import secrets
import string
import paramiko
HOST = "20.24.50.121"
USER = "heicode"
PASS = os.environ.get("MGR_PASS", "")
if not PASS:
print("MGR_PASS not set", file=sys.stderr)
sys.exit(2)
DB_PASS = "Myadmin@123456."
DB_HOST = "heicode.postgres.database.azure.com"
DB_USER = "heicode"
DB_NAME = "heicode"
# new-api token keys are 48-char alphanum (no padding). Match that
# exactly so admin tools / dashboards parse it.
ALPHABET = string.ascii_letters + string.digits
RAW_KEY = "".join(secrets.choice(ALPHABET) for _ in range(48))
# Token bearer format on the wire is "sk-<48 chars>".
SQL_FIND = (
"SELECT id, username, email FROM users "
"WHERE email IN ('zsbgnw@gmail.com','chenchen@xinghanlab.com') "
" OR username IN ('chenchen','root') "
" OR role >= 10 "
"ORDER BY role DESC, id ASC LIMIT 1;"
)
INSERT_SQL_TEMPLATE = (
"INSERT INTO tokens "
'(user_id, name, "key", status, created_time, accessed_time, '
"expired_time, remain_quota, used_quota, unlimited_quota, "
"model_limits_enabled, model_limits, allow_ips, "
'"group", cross_group_retry, hide_from_user_ui) '
"VALUES "
"({user_id}, '蜂群程序 (Swarm Bot)', '{key}', 1, "
"EXTRACT(EPOCH FROM NOW())::bigint, EXTRACT(EPOCH FROM NOW())::bigint, "
"-1, 0, 0, true, false, '', '', '', false, false) "
"RETURNING id, name, \"key\";"
)
def main() -> None:
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect(HOST, 22, USER, PASS, look_for_keys=False, allow_agent=False, timeout=30)
def psql(sql: str) -> str:
# Pipe SQL via stdin so quoted identifiers like "group" survive
# without shell-escape hell.
cmd = (
f"PGPASSWORD='{DB_PASS}' psql "
f"'sslmode=require host={DB_HOST} user={DB_USER} dbname={DB_NAME}' "
f"-At -F'|' -v ON_ERROR_STOP=1"
)
stdin, stdout, stderr = c.exec_command(cmd, timeout=60, get_pty=False)
stdin.write(sql)
stdin.channel.shutdown_write()
out = stdout.read().decode("utf-8", "replace").strip()
err = stderr.read().decode("utf-8", "replace").strip()
if err:
print(f"[psql stderr] {err}", file=sys.stderr)
return out
# Step 1: find owner user
print(f"--- Looking up platform owner user ---", flush=True)
owner_row = psql(SQL_FIND)
if not owner_row:
print("ERROR: no owner user found", file=sys.stderr)
sys.exit(3)
fields = owner_row.split("|")
owner_id = int(fields[0])
owner_username = fields[1] if len(fields) > 1 else ""
owner_email = fields[2] if len(fields) > 2 else ""
print(f"Owner: id={owner_id} username={owner_username} email={owner_email}")
# Step 2: insert the token row
print(f"\n--- Inserting dedicated 蜂群程序 token ---", flush=True)
insert_sql = INSERT_SQL_TEMPLATE.format(user_id=owner_id, key=RAW_KEY)
inserted = psql(insert_sql)
if not inserted:
print("ERROR: INSERT returned nothing", file=sys.stderr)
sys.exit(4)
fields = inserted.split("|")
token_id = int(fields[0])
token_name = fields[1]
token_key = fields[2]
print(f"Token row inserted: id={token_id} name={token_name}")
# Step 3: print result
base_url = "https://code.xinghanlab.com"
print("\n" + "=" * 60)
print("NEWAPI CREDENTIAL — 蜂群程序 (SWARM BOT)")
print("=" * 60)
print(f"Base URL (OpenAI compat): {base_url}/v1")
print(f"Base URL (Anthropic compat): {base_url}/v1/messages")
print(f"Base URL (raw root): {base_url}")
print(f"")
print(f"API Key (Authorization: Bearer ...):")
print(f" sk-{token_key}")
print(f"")
print(f"Token row id: {token_id}")
print(f"Owner user id: {owner_id} ({owner_email})")
print(f"Quota model: unlimited (consumes the owner-account quota)")
print(f"Expires: never")
print(f"Manage / revoke from: {base_url}/keys")
print("=" * 60)
if __name__ == "__main__":
main()
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""
One-shot deploy script for the Heicode Manager Azure VM. Designed to be
idempotent — running it twice in a row converges to the same state.
What it does:
1. SSH into the VM with credentials supplied via env (NEVER hard-coded
in the repo — the launcher passes them on stdin).
2. cd into the manager checkout, git pull origin main.
3. docker compose pull + up -d --build, then prune dangling images.
4. Wait for /api/status to return 200.
5. Print container status + tail of the manager logs.
Failure mode: if any step exits non-zero, the whole script aborts and
prints the offending command's stderr — caller (this Claude session)
escalates to the user instead of silently moving on.
"""
import os
import sys
import time
import paramiko
HOST = os.environ.get("MGR_HOST", "20.24.50.121")
USER = os.environ.get("MGR_USER", "heicode")
PASS = os.environ.get("MGR_PASS", "")
PORT = int(os.environ.get("MGR_PORT", "22"))
if not PASS:
print("MGR_PASS not set", file=sys.stderr)
sys.exit(2)
def run(client: paramiko.SSHClient, cmd: str, timeout: int = 600) -> int:
"""Stream a remote command's output to local stdout/stderr."""
print(f"\n$ {cmd}", flush=True)
stdin, stdout, stderr = client.exec_command(cmd, timeout=timeout, get_pty=True)
for line in iter(stdout.readline, ""):
if not line:
break
sys.stdout.write(line)
sys.stdout.flush()
rc = stdout.channel.recv_exit_status()
err = stderr.read().decode("utf-8", errors="replace")
if err.strip():
sys.stderr.write(err)
return rc
def must(rc: int, label: str) -> None:
if rc != 0:
print(f"\nFAIL: {label} exit={rc}", file=sys.stderr)
sys.exit(rc or 1)
def main() -> None:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
print(f"Connecting {USER}@{HOST}:{PORT} ...", flush=True)
client.connect(
hostname=HOST,
port=PORT,
username=USER,
password=PASS,
look_for_keys=False,
allow_agent=False,
timeout=30,
)
# Locate the manager checkout. We try a few likely paths instead of
# hardcoding one — first match wins.
probe = (
"for p in "
"~/heicode-mananger ~/heicode ~/heicode-mananger/heicode "
"/opt/heicode-mananger /home/heicode/heicode-mananger; do "
" if [ -d $p/.git ]; then echo FOUND=$p; break; fi; "
"done"
)
stdin, stdout, _ = client.exec_command(probe, timeout=20)
out = stdout.read().decode().strip()
print(out)
repo = None
for line in out.splitlines():
if line.startswith("FOUND="):
repo = line.split("=", 1)[1]
break
if not repo:
print("Could not locate manager git checkout on VM", file=sys.stderr)
sys.exit(3)
print(f"Using repo: {repo}")
# Find docker-compose file we'll target.
compose = "docker-compose.azure-vm.yml"
must(run(client, f"test -f {repo}/heicode/{compose} || ls {repo}/heicode/docker-compose*.yml"),
"locate compose file")
# Pull latest code.
must(run(client, f"cd {repo} && git fetch origin && git checkout main && git pull --ff-only origin main"),
"git pull")
# Show what we have locally now.
run(client, f"cd {repo} && git log --oneline -3")
# Rebuild + restart.
must(run(client,
f"cd {repo}/heicode && sudo docker compose -f {compose} up -d --build 2>&1",
timeout=1800),
"compose up")
# Clean dangling images per CLAUDE.md.
run(client, "sudo docker image prune -f")
# Wait for /api/status to come back.
print("\nWaiting for /api/status to return 200 (max 90s) ...")
deadline = time.time() + 90
status_ok = False
while time.time() < deadline:
rc = run(client,
"curl -fsS -o /dev/null -w 'HTTP_%{http_code}\\n' https://code.xinghanlab.com/api/status")
if rc == 0:
status_ok = True
break
time.sleep(5)
if not status_ok:
print("FAIL: /api/status did not return 200 within 90s", file=sys.stderr)
run(client, f"cd {repo}/heicode && sudo docker compose -f {compose} logs --tail=80 heicode")
sys.exit(4)
print("\n--- Final state ---")
run(client, "sudo docker ps --format 'table {{.Names}}\\t{{.Status}}\\t{{.Image}}'")
run(client, f"cd {repo}/heicode && sudo docker compose -f {compose} logs --tail=20 heicode")
client.close()
print("\nDEPLOY OK")
if __name__ == "__main__":
main()
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Look up which user account to attach the swarm-program token to."""
import os, sys, paramiko
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect("20.24.50.121", 22, "heicode", os.environ["MGR_PASS"],
look_for_keys=False, allow_agent=False, timeout=30)
sql = '''
SELECT id, username, email, "group", role, status
FROM users
WHERE email IN ('zsbgnw@gmail.com', 'chenchen@xinghanlab.com')
OR username = 'chenchen'
OR role >= 10
ORDER BY id
LIMIT 10;
'''
# Wrap via psql exec inside the heicode container.
cmd = (
'sudo docker exec heicode sh -c '
+ '"PGPASSWORD=\\"Myadmin@123456.\\" psql '
+ '-h heicode.postgres.database.azure.com -U heicode heicode '
+ "-c \\\"" + sql.replace("\n", " ").replace('"', '\\"\\"') + "\\\"\""
)
_, o, e = c.exec_command(cmd, timeout=60, get_pty=True)
for line in iter(o.readline, ""):
if not line: break
sys.stdout.write(line.encode("ascii","replace").decode("ascii"))
sys.stdout.flush()
err = e.read().decode("ascii","replace")
if err.strip():
sys.stderr.write(err)
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Force-recreate the manager container after a build that didn't swap.
Why this exists: `docker compose up -d --build` rebuilds the image but
won't recreate the running container if compose decides the spec hasn't
changed. On the Azure VM we've hit cases where the smoke test shows the
old container is still up. This script:
1. Inspects current container start time
2. Runs `compose up -d --force-recreate --build`
3. Confirms new start time
"""
import os
import sys
import paramiko
HOST = os.environ.get("MGR_HOST", "20.24.50.121")
USER = os.environ.get("MGR_USER", "heicode")
PASS = os.environ.get("MGR_PASS", "")
if not PASS:
print("MGR_PASS not set", file=sys.stderr)
sys.exit(2)
def run(client, cmd, timeout=900):
print(f"\n$ {cmd}", flush=True)
stdin, stdout, stderr = client.exec_command(cmd, timeout=timeout, get_pty=True)
out_chunks = []
for raw in iter(stdout.readline, ""):
if not raw:
break
# Strip non-ASCII to dodge Windows cp936 encoding errors.
clean = raw.encode("ascii", "replace").decode("ascii")
out_chunks.append(clean)
sys.stdout.write(clean)
sys.stdout.flush()
rc = stdout.channel.recv_exit_status()
return rc, "".join(out_chunks)
def main():
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
hostname=HOST, port=22, username=USER, password=PASS,
look_for_keys=False, allow_agent=False, timeout=30,
)
# Locate repo + compose file
rc, repo_out = run(client,
"for p in ~/heicode-mananger ~/heicode ~/heicode-mananger/heicode "
"/opt/heicode-mananger /home/heicode/heicode-mananger; do "
" if [ -d $p/.git ]; then echo FOUND=$p; break; fi; done")
repo = None
for line in repo_out.splitlines():
if "FOUND=" in line:
repo = line.split("=", 1)[1].strip()
break
if not repo:
print("repo not found", file=sys.stderr); sys.exit(3)
run(client, "sudo docker ps --filter name=heicode --format 'table {{.Names}}\\t{{.Status}}\\t{{.Image}}\\t{{.CreatedAt}}'")
run(client, f"cd {repo} && git log --oneline -1")
# Pin to latest commit + force recreate.
run(client,
f"cd {repo}/heicode && sudo docker compose -f docker-compose.azure-vm.yml up -d --force-recreate --build heicode 2>&1",
timeout=1800)
run(client, "sleep 8")
run(client, "sudo docker ps --filter name=heicode --format 'table {{.Names}}\\t{{.Status}}\\t{{.Image}}\\t{{.CreatedAt}}'")
run(client, "sudo docker image prune -f")
client.close()
print("\nFORCE RECREATE DONE")
if __name__ == "__main__":
main()
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""
Post-deploy smoke test for Heicode Manager. Runs entirely from this
laptop against https://code.xinghanlab.com — no SSH required.
Checks:
1. /api/status returns 200 with success:true (proves the binary
is running and the DB is reachable).
2. New build hash present (HTML title or static asset includes a
fresh content hash, indirectly confirming the new image is live).
3. /api/devices returns 401 when called without auth (proves
the route is wired AND not accidentally public).
4. /api/devices/pair returns 401 without auth (same reasoning).
5. /v1/messages with a clearly-invalid V2 envelope returns 401
AND carries X-Heicode-Auth-Error + X-Heicode-Server-Time headers
— this is the V2 diagnostic-header contract we shipped this round.
Exit code: 0 == all green, 1 == any failure (caller escalates).
"""
import sys
import time
import urllib.request
import urllib.error
import json
BASE = "https://code.xinghanlab.com"
SESSION_UA = "heicode-smoke/1.0"
def http(method: str, path: str, headers: dict | None = None, body: bytes | None = None):
"""Returns (status_code, headers_dict, body_text). Does NOT raise on 4xx/5xx."""
req = urllib.request.Request(BASE + path, method=method, data=body)
req.add_header("User-Agent", SESSION_UA)
for k, v in (headers or {}).items():
req.add_header(k, v)
try:
resp = urllib.request.urlopen(req, timeout=15)
return resp.status, dict(resp.headers), resp.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
return e.code, dict(e.headers), e.read().decode("utf-8", "replace")
FAILS: list[str] = []
def check(label: str, ok: bool, detail: str = "") -> None:
mark = "OK " if ok else "FAIL"
print(f"[{mark}] {label}" + (f" -- {detail}" if detail else ""))
if not ok:
FAILS.append(label)
print(f"Smoke testing {BASE} ...\n")
# 1. /api/status reachable + healthy
code, hdrs, body = http("GET", "/api/status")
ok = code == 200 and '"success":true' in body
check("/api/status returns 200 with success:true", ok,
f"HTTP {code}, body starts with {body[:80]!r}")
# 2. /api/devices without auth → 401 (route exists + protected)
code, hdrs, body = http("GET", "/api/devices/")
ok = code in (401, 403)
check("/api/devices unauthenticated → 401/403", ok, f"HTTP {code}")
# 3. /api/devices/pair without auth → 401
code, hdrs, body = http("POST", "/api/devices/pair",
headers={"Content-Type": "application/json"},
body=b'{"device_id":"x","public_key":"","fingerprint":""}')
ok = code in (401, 403)
check("/api/devices/pair unauthenticated → 401/403", ok, f"HTTP {code}")
# 4. /v1/messages with bogus V2 envelope → 401 + diagnostic headers.
# We construct a request that:
# - sets Content-Encoding: heicode-aead-v1 (forces V2 dispatcher)
# - sets junk for the 6 X-Heicode-* headers
# - sends a too-short body (12 bytes — fails AEAD nonce check)
# Expected: HTTP 401, X-Heicode-Auth-Error header present,
# X-Heicode-Server-Time header present and parseable as int.
bogus_headers = {
"Content-Encoding": "heicode-aead-v1",
"X-Heicode-Device-Id": "smoke-test-bogus",
"X-Heicode-Timestamp": str(int(time.time() * 1000)),
"X-Heicode-Nonce": "deadbeef" * 4,
"X-Heicode-Fingerprint": "0" * 64,
"X-Heicode-Eph-Pubkey": "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=",
"X-Heicode-Signature": "BBBB",
"Content-Type": "application/json",
}
code, hdrs, body = http("POST", "/v1/messages",
headers=bogus_headers,
body=b"\x00" * 12)
ok_401 = code == 401
check("/v1/messages with bogus V2 envelope → 401", ok_401, f"HTTP {code}")
# Header lookup is case-insensitive in HTTP but our http() dict isn't.
# Build a lowercase view.
hdrs_lc = {k.lower(): v for k, v in hdrs.items()}
diag_code = hdrs_lc.get("x-heicode-auth-error", "")
diag_time = hdrs_lc.get("x-heicode-server-time", "")
check("V2 401 carries X-Heicode-Auth-Error header", bool(diag_code), f"value={diag_code!r}")
ok_time = False
try:
if diag_time and abs(int(diag_time) - int(time.time() * 1000)) < 60_000:
ok_time = True
except ValueError:
pass
check("V2 401 carries X-Heicode-Server-Time within 60s of local clock",
ok_time, f"value={diag_time!r}")
# 5. Sanity: status data has live numbers (start_time freshly set if rebuild
# happened just now).
code, hdrs, body = http("GET", "/api/status")
try:
obj = json.loads(body)
st = obj.get("data", {}).get("start_time", 0)
age = int(time.time()) - st
print(f"\nManager start_time = {st} (age {age}s)")
check("Manager start_time looks recent (<30 min)", 0 <= age < 30 * 60,
f"age={age}s")
except Exception as e:
check("Manager status JSON parseable", False, repr(e))
if FAILS:
print(f"\n{len(FAILS)} smoke check(s) FAILED:")
for f in FAILS:
print(f" - {f}")
sys.exit(1)
print("\nALL SMOKE CHECKS PASSED")
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""
Extended live smoke test against the production Manager. Exercises:
A. Routing surface
- /api/status → 200 healthy
- /api/devices/ → 401 (auth-protected, route mounted)
- /api/devices/pair → 401 without auth
- /api/devices/12345 PATCH/DELETE → 401 without auth
- Web SPA index → 200 (assets loadable)
- /v1/models with no auth → 401 (legacy v1 path still gated)
B. V2 diagnostic header contract
Exercises FIVE distinct V2 failure modes and asserts that each
returns:
- HTTP 401
- X-Heicode-Server-Time present, parseable, within 60s of local
- X-Heicode-Auth-Error = expected machine-readable code
This pins the whole error-code mapping we shipped in fix(devices).
C. Legacy compat
- V1 sk- request to /v1/models without bearer → 401
- V1 sk- request with bogus bearer "sk-xxx" → 401
Confirms the 30-day legacy compat path still runs (V2 dispatcher
isn't accidentally swallowing non-V2 traffic).
Exit code: 0 == all green, 1 == any failure.
"""
import sys
import time
import urllib.request
import urllib.error
BASE = "https://code.xinghanlab.com"
UA = "heicode-smoke-extended/1.0"
FAILS = []
# Honour HTTPS_PROXY env (clash/wireguard/etc) — urllib doesn't pick
# it up automatically on Windows. Needed when the local machine has a
# DNS hijack returning sinkhole IPs for code.xinghanlab.com.
import os
_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
if _proxy:
proxy_handler = urllib.request.ProxyHandler({"https": _proxy, "http": _proxy})
_opener = urllib.request.build_opener(proxy_handler)
else:
_opener = urllib.request.build_opener()
def http(method, path, headers=None, body=None):
req = urllib.request.Request(BASE + path, method=method, data=body)
req.add_header("User-Agent", UA)
for k, v in (headers or {}).items():
req.add_header(k, v)
try:
r = _opener.open(req, timeout=15)
return r.status, {k.lower(): v for k, v in r.headers.items()}, r.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
return e.code, {k.lower(): v for k, v in e.headers.items()}, e.read().decode("utf-8", "replace")
def check(label, ok, detail=""):
print(f"[{'OK ' if ok else 'FAIL'}] {label}" + (f" -- {detail}" if detail else ""))
if not ok:
FAILS.append(label)
import base64
# Exactly 32 bytes (0x41 * 32) — valid X25519 pubkey LENGTH (every
# 32-byte sequence is a valid X25519 point per spec). Using "wrong
# byte count" pubkey strings here would short-circuit the dispatcher
# at eph_pubkey_malformed before we can test the downstream code
# paths, so always start from a 32-byte default.
VALID_LEN_EPH_PUBKEY = base64.standard_b64encode(b"\x41" * 32).decode()
def v2_headers(eph_pubkey_b64=VALID_LEN_EPH_PUBKEY,
timestamp_ms=None, nonce="dead" * 8):
return {
"Content-Encoding": "heicode-aead-v1",
"X-Heicode-Device-Id": "smoke-bogus",
"X-Heicode-Timestamp": str(timestamp_ms or int(time.time() * 1000)),
"X-Heicode-Nonce": nonce,
"X-Heicode-Fingerprint": "0" * 64,
"X-Heicode-Eph-Pubkey": eph_pubkey_b64,
"X-Heicode-Signature": "BBBBBBBB",
"Content-Type": "application/json",
}
def assert_v2_failure(label, headers, body, expected_code):
code, hdrs, _ = http("POST", "/v1/messages", headers=headers, body=body)
diag = hdrs.get("x-heicode-auth-error", "")
ts = hdrs.get("x-heicode-server-time", "")
ts_ok = False
try:
ts_ok = ts and abs(int(ts) - int(time.time() * 1000)) < 60_000
except ValueError:
pass
ok = code == 401 and diag == expected_code and ts_ok
check(label, ok, f"HTTP {code} code={diag!r} time={ts!r}")
# ---------------------------------------------------------------- A. Routing
print(f"\n=== A. Routing surface ({BASE}) ===\n")
code, _, body = http("GET", "/api/status")
check("/api/status returns 200 + success:true",
code == 200 and '"success":true' in body, f"HTTP {code}")
code, _, _ = http("GET", "/api/devices/")
check("/api/devices/ unauthenticated → 401", code == 401, f"HTTP {code}")
code, _, _ = http("POST", "/api/devices/pair",
headers={"Content-Type": "application/json"},
body=b'{"device_id":"x","public_key":"","fingerprint":""}')
check("/api/devices/pair unauthenticated → 401", code == 401, f"HTTP {code}")
code, _, _ = http("PATCH", "/api/devices/99999",
headers={"Content-Type": "application/json"},
body=b'{"device_name":"hax"}')
check("/api/devices/:id PATCH unauthenticated → 401", code == 401, f"HTTP {code}")
code, _, _ = http("DELETE", "/api/devices/99999")
check("/api/devices/:id DELETE unauthenticated → 401", code == 401, f"HTTP {code}")
code, _, body = http("GET", "/")
check("Web SPA index → 200", code == 200 and "<html" in body.lower(),
f"HTTP {code} len={len(body)}")
code, _, _ = http("GET", "/v1/models")
check("/v1/models without auth → 401", code == 401, f"HTTP {code}")
# ---------------------------------------------------------- B. V2 diag codes
print("\n=== B. V2 diagnostic header contract ===\n")
# B1: malformed ephemeral pubkey (not base64 → eph_pubkey_malformed)
assert_v2_failure(
"V2 malformed eph_pubkey → eph_pubkey_malformed",
{**v2_headers(eph_pubkey_b64="not_base64_!!!"), },
b"\x00" * 12,
"eph_pubkey_malformed",
)
# B2: pubkey base64-decodes but wrong length (not 32 bytes) → eph_pubkey_malformed
assert_v2_failure(
"V2 wrong-length eph_pubkey → eph_pubkey_malformed",
{**v2_headers(eph_pubkey_b64="QUE=")}, # decodes to 2 bytes
b"\x00" * 12,
"eph_pubkey_malformed",
)
# B3: body too short (<28 bytes for nonce+tag) → body_too_short
assert_v2_failure(
"V2 body too short → body_too_short",
v2_headers(),
b"\x00" * 5, # too short
"body_too_short",
)
# B4: missing X-Heicode-Eph-Pubkey header entirely → eph_pubkey_missing
hdrs_no_eph = v2_headers()
hdrs_no_eph.pop("X-Heicode-Eph-Pubkey")
assert_v2_failure(
"V2 missing eph_pubkey header → eph_pubkey_missing",
hdrs_no_eph,
b"\x00" * 12,
"eph_pubkey_missing",
)
# B5: valid-shape but unknown device + decoy ciphertext → after decrypt
# gets `body_decrypt_failed` (AEAD tag mismatch). This is the most
# common real-world failure mode for a tampered request.
assert_v2_failure(
"V2 valid-shape but undecryptable → body_decrypt_failed",
v2_headers(), # 32-byte AAAAAA... pubkey is valid X25519
b"\xAA" * 28, # 12 nonce + 16 tag, but ciphertext won't authenticate
"body_decrypt_failed",
)
# ---------------------------------------------------------- C. Legacy compat
print("\n=== C. Legacy sk- bearer compat path ===\n")
# No Content-Encoding header → legacy dispatcher → ValidateUserToken
code, hdrs, _ = http("POST", "/v1/messages",
headers={"Content-Type": "application/json",
"Authorization": "Bearer sk-bogus-token-not-real"},
body=b'{"model":"claude-haiku","messages":[]}')
# V2 diagnostic headers MUST NOT be present on legacy path responses.
diag = hdrs.get("x-heicode-auth-error", "")
check("Legacy sk- request → 401 (no Content-Encoding)", code == 401, f"HTTP {code}")
check("Legacy path does NOT leak V2 diagnostic headers", diag == "",
f"x-heicode-auth-error={diag!r}")
# ---------------------------------------------------------- summary
if FAILS:
print(f"\n{len(FAILS)} smoke check(s) FAILED:")
for f in FAILS:
print(f" - {f}")
sys.exit(1)
print("\nALL EXTENDED SMOKE CHECKS PASSED")
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Unstick a manager deploy where the container name collides with a
half-removed sibling. Removes everything named heicode* (except the
openbao auxiliary), then `compose up -d` to recreate cleanly.
"""
import os, sys, paramiko, time
HOST = os.environ.get("MGR_HOST", "20.24.50.121")
USER = os.environ.get("MGR_USER", "heicode")
PASS = os.environ.get("MGR_PASS", "")
if not PASS:
print("MGR_PASS not set", file=sys.stderr); sys.exit(2)
def run(client, cmd, timeout=900):
print(f"\n$ {cmd}", flush=True)
_, stdout, _ = client.exec_command(cmd, timeout=timeout, get_pty=True)
for raw in iter(stdout.readline, ""):
if not raw: break
sys.stdout.write(raw.encode("ascii","replace").decode("ascii"))
sys.stdout.flush()
return stdout.channel.recv_exit_status()
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(hostname=HOST, port=22, username=USER, password=PASS,
look_for_keys=False, allow_agent=False, timeout=30)
# Find repo
run(client, "ls -d ~/heicode-mananger ~/heicode 2>/dev/null")
# Show all containers (running + stopped) related to heicode
run(client, "sudo docker ps -a --filter name=heicode --format 'table {{.Names}}\\t{{.Status}}\\t{{.Image}}'")
# Remove any container that is exactly 'heicode' (the conflicting old one)
# AND any *_heicode temp.
run(client, "sudo docker rm -f heicode 2>/dev/null || true")
run(client, "sudo docker ps -aq --filter name=_heicode | xargs -r sudo docker rm -f")
run(client, "sudo docker ps -a --filter name=heicode --format 'table {{.Names}}\\t{{.Status}}'")
# Re-up
repo = "/home/heicode/heicode-mananger"
# Try both common paths
run(client, f"test -d {repo} || ls -d ~/heicode-mananger 2>/dev/null")
run(client, f"cd ~/heicode-mananger/heicode && sudo docker compose -f docker-compose.azure-vm.yml up -d heicode 2>&1", timeout=600)
time.sleep(8)
run(client, "sudo docker ps --filter name=heicode --format 'table {{.Names}}\\t{{.Status}}\\t{{.CreatedAt}}'")
run(client, "sudo docker logs --tail=20 heicode")
client.close()
print("\nUNSTICK DONE")