fix(ai): external responder on its own listener with random turn ids and reply tokens
보안 리뷰 반영. 외부 응답은 곧 진단이고 자동 승인 아래서는 실행 조치가 되므로, /external/next·/external/reply 를 에이전트 포트에서 떼어 EXTERNAL_ADDR 리스너(기본 꺼짐, VM compose 는 127.0.0.1:8090 에만 매핑해 IAP SSH 터널로만 도달)로 옮기고, 턴 id 를 난수로 만들며, 응답마다 그 턴을 가져간 클라이언트만 아는 reply_token 을 요구해 상수 시간으로 비교한다. /selfcheck 는 리스너가 꺼져 있으면 external_responder: "off" 를 보인다.
This commit is contained in:
1 parent
ed3d41d399
commit
b0ff118841
3 files changed
+67
-28
No files matched your search
+16
-6
@@ -122,9 +122,17 @@ func main() {
|
||||
// external-* model ids (reference runs, 2026-10-16): each loop turn is served to an outside client over
|
||||
// GET /external/next and answered through POST /external/reply (internal/external). The diagnosis deadline is
|
||||
// EXTERNAL_DIAGNOSE_TIMEOUT_S (the harness waits a little longer for external-* models), one turn at most
|
||||
// EXTERNAL_TURN_TIMEOUT_S. The endpoints are reachable only inside the VPC / over the IAP SSH tunnel.
|
||||
ext := external.New(time.Duration(envInt("EXTERNAL_TURN_TIMEOUT_S", 1200)) * time.Second)
|
||||
ag.SetExternal(ext)
|
||||
// EXTERNAL_TURN_TIMEOUT_S. Off unless EXTERNAL_ADDR is set; the endpoints live on that separate listener, which
|
||||
// the VM compose file maps to 127.0.0.1 only (reachable through the IAP SSH tunnel, never from the VPC): a reply
|
||||
// is a diagnosis and, under auto-approval, an executed action.
|
||||
var ext *external.Responder
|
||||
if addr := env("EXTERNAL_ADDR", ""); addr != "" {
|
||||
ext = external.New(time.Duration(envInt("EXTERNAL_TURN_TIMEOUT_S", 1200)) * time.Second)
|
||||
ag.SetExternal(ext)
|
||||
esrv := &http.Server{Addr: addr, Handler: logging(ext.Handler()), ReadHeaderTimeout: 10 * time.Second}
|
||||
go func() { log.Fatal(esrv.ListenAndServe()) }()
|
||||
log.Printf("external responder: listening on %s", addr)
|
||||
}
|
||||
externalDiagnoseTimeout := time.Duration(envInt("EXTERNAL_DIAGNOSE_TIMEOUT_S", 1500)) * time.Second
|
||||
jevMode := env("JEV_MODE", "off") // off | shadow | enforce (shadow records Jev answers next to the diagnosis; enforce also applies the two review rules)
|
||||
var jc *jev.Client
|
||||
@@ -166,7 +174,11 @@ func main() {
|
||||
if f := anth.Federation(); f != nil {
|
||||
out["anthropic_federation"] = f.Probe(c, anth.BaseURL) // one real token exchange; the Console history page shows the deny reason on failure
|
||||
}
|
||||
out["external_responder"] = ext.Status()
|
||||
if ext != nil {
|
||||
out["external_responder"] = ext.Status()
|
||||
} else {
|
||||
out["external_responder"] = "off"
|
||||
}
|
||||
out["jev_mode"] = jevMode
|
||||
if jc != nil {
|
||||
out["jev_key_present"] = jc.KeyPresent()
|
||||
@@ -222,8 +234,6 @@ func main() {
|
||||
ag.SaveShadow(context.Background(), req, res.SessionID, res, tri, rev)
|
||||
writeJSON(w, 200, res)
|
||||
})
|
||||
mux.HandleFunc("GET /external/next", ext.Next)
|
||||
mux.HandleFunc("POST /external/reply", ext.ReplyHandler)
|
||||
srv := &http.Server{Addr: env("AGENT_ADDR", ":8080"), Handler: logging(mux), ReadHeaderTimeout: 10 * time.Second}
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user