Files
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

106 lines
4.1 KiB
Python

# -*- coding: utf-8 -*-
"""把 WorkBuddy 网关口令**投递**给设备接入垫片(A 方案 · 2026-09-30 用户拍板)。
🔴 为什么需要它:垫片原设计「口令只在 env」⇒ 由**脱离会话**的上下文(计划任务/常驻)拉起时
读不到口令 ⇒ 请求一路 fail-closed 成 503,链路永远差最后一跳。
本脚本跑在**有口令的那一侧**(宿主进程树:钩子/`--tick`)⇒ 天然读得到 env ⇒ 负责「送」。
🔴 安全口径(与本项目红线一致):
· ⛔ 口令**不落盘**(本文件不写任何凭据;只在进程内从 env 读到内存,直接进请求体)
· ⛔ 口令**不进日志**(只写「投了/没投/被拒」+ HTTP 码)
· ⛔ 只投回环(127.0.0.1);垫片侧还有"**探活通过才采纳**"的第二道闸(错口令必被拒)
⚠️ 节流:≥ `MIN_GAP` 秒才尝试一次(钩子触发很频繁,⛔ 不能每次工具调用都去探)。
⛔ 失败一律**静默退出 0** —— 它是旁路,⛔ 不得影响钩子本身、更不得影响 WorkBuddy 运行。
"""
from __future__ import annotations
import json
import os
import sys
import time
import urllib.request
PORT = 20090
SELFCHECK = "/__device_access/selfcheck"
TOKEN_PATH = "/__device_access/token"
TOKEN_ENV = "CODEBUDDY_GATEWAY_PASSWORD"
MIN_GAP = 60.0 # 秒
WS = os.environ.get("DSH_COLLAB_WS") or "E:/ProgramData/AIProject/ai1net-dsh-server"
STAMP = os.path.join(WS, "tmp", "supervise-inbox", "_token-deliver.stamp")
LOG = os.path.join(WS, "tmp", "supervise-inbox", "_token-deliver.log")
def _log(msg: str) -> None:
try:
os.makedirs(os.path.dirname(LOG), exist_ok=True)
with open(LOG, "a", encoding="utf-8") as f:
f.write("[%s] %s\n" % (time.strftime("%Y-%m-%d %H:%M:%S"), msg))
except Exception:
pass
def _http(url: str, data: bytes | None = None, timeout: float = 4.0):
req = urllib.request.Request(url, data=data, method=("POST" if data is not None else "GET"))
if data is not None:
req.add_header("Content-Type", "text/plain")
with urllib.request.urlopen(req, timeout=timeout) as r: # noqa: S310(只打回环)
return r.status, r.read().decode("utf-8", "replace")
def main() -> int:
# ① 节流:⛔ 每次工具调用都探一遍会平白增加负担
try:
if os.path.isfile(STAMP) and (time.time() - os.path.getmtime(STAMP)) < MIN_GAP:
return 0
os.makedirs(os.path.dirname(STAMP), exist_ok=True)
with open(STAMP, "w", encoding="utf-8") as f:
f.write(str(time.time()))
except Exception:
pass
token = os.environ.get(TOKEN_ENV) or ""
if token == "":
# 本进程没有口令 ⇒ 不可能是"送的一方"(钩子/--tick 才有);⛔ 不报错、不落 NEED-USER
return 0
base = "http://127.0.0.1:%d" % PORT
# ② 垫片不在 ⇒ 静默走人(⛔ 不起它;起它是另一件事)
try:
code, body = _http(base + SELFCHECK)
except Exception:
return 0
if code != 200:
return 0
try:
present = bool((json.loads(body).get("credential") or {}).get("present"))
except Exception:
present = False
if present:
return 0 # 已就位(env 或已投过)⇒ 不必重复投
# ③ 投(⛔ 口令只在请求体里,⛔ 绝不进日志/URL)
try:
code, body = _http(base + TOKEN_PATH, data=token.encode("utf-8"))
except Exception as e:
_log("投递异常(已静默): %s" % type(e).__name__)
return 0
try:
j = json.loads(body)
except Exception:
j = {}
if code == 200 and j.get("ok"):
_log("投递成功 → source=%s" % ((j.get("credential") or {}).get("source") or "?"))
else:
# 400 token-rejected-by-gateway = 口令过期/网关重启(正常会自愈,⛔ 不惊动用户)
_log("投递被拒 http=%s reason=%s" % (code, j.get("reason") or "?"))
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except Exception:
sys.exit(0) # ⛔ 旁路脚本绝不以非零码影响钩子