Files
knowledge-governance/internal/libraryagent/client.go
T
姚章浩 910c0fcd76
build / build (push) Failing after 16s
feat(library): 页面发起封存的最小权限主机侧执行边界
- libraryagent:0700 目录内 0600 unix socket + SO_PEERCRED 同 uid,
  固定 binary/归档/日志/项目/写入者声明,请求只能选范围与作业号
- API:POST/GET /api/v1/library/captures(入队 library.capture + 读队列状态)
- worker:library.capture handler 走 socket;启动先 recover;不挂 Docker socket、
  不持有来源凭据
- web:整库版本页显示真实作业状态并可发起封存(未接线/无权限给出原因)
- scripts/library-agent-install.sh:构建、安装 systemd user 服务并读回权限
2026-10-28 12:11:08 +08:00

93 lines
2.9 KiB
Go

package libraryagent
import (
"context"
"encoding/json"
"errors"
"net"
"time"
)
// ClientConfig is what a governance process needs: one socket path. It never
// carries source credentials or Docker access — those stay with the agent.
type ClientConfig struct {
Socket string
Timeout time.Duration
}
// Result is the agent's answer to one capture/status/recover request.
type Result struct {
OK bool
Scope string
Job string
SnapshotID string
Replay bool
Error string
Payload json.RawMessage
}
// Capture asks the agent to seal one library scope. The job id makes the call
// idempotent: repeating it returns the already sealed candidate.
func Capture(ctx context.Context, config ClientConfig, scope, job string) (Result, error) {
return call(ctx, config, map[string]string{"action": "capture", "scope": scope, "job": job})
}
// Status probes the agent (source wiring, writer group, archive usage).
func Status(ctx context.Context, config ClientConfig) (Result, error) {
return call(ctx, config, map[string]string{"action": "status"})
}
// Recover restores writers from an interrupted freeze journal.
func Recover(ctx context.Context, config ClientConfig) (Result, error) {
return call(ctx, config, map[string]string{"action": "recover"})
}
func call(ctx context.Context, config ClientConfig, request map[string]string) (Result, error) {
if config.Socket == "" {
return Result{}, errors.New("library agent socket is not configured")
}
timeout := config.Timeout
if timeout <= 0 {
timeout = 2 * time.Minute
}
bounded, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
var dialer net.Dialer
conn, err := dialer.DialContext(bounded, "unix", config.Socket)
if err != nil {
return Result{}, errors.New("library agent is unreachable")
}
defer conn.Close()
if deadline, ok := bounded.Deadline(); ok {
_ = conn.SetDeadline(deadline)
}
if err = json.NewEncoder(conn).Encode(request); err != nil {
return Result{}, errors.New("library agent request failed")
}
var response struct {
OK bool `json:"ok"`
Scope string `json:"scope"`
Job string `json:"job"`
Error string `json:"error"`
Payload json.RawMessage `json:"payload"`
}
if err = json.NewDecoder(conn).Decode(&response); err != nil {
return Result{}, errors.New("library agent returned an invalid response")
}
result := Result{OK: response.OK, Scope: response.Scope, Job: response.Job, Error: response.Error, Payload: response.Payload}
var payload struct {
SnapshotID string `json:"snapshot_id"`
Replay bool `json:"replay"`
}
if len(response.Payload) > 0 && json.Unmarshal(response.Payload, &payload) == nil {
result.SnapshotID, result.Replay = payload.SnapshotID, payload.Replay
}
if !result.OK {
if result.Error == "" {
result.Error = "library agent refused the request"
}
return result, errors.New(result.Error)
}
return result, nil
}