Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
faeae16
docs: define personal Dot task event bridge and routing boundaries
VeraPyuyi Oct 19, 2026
f51a7a3
docs: stage Dot event bridge and automatic routing implementation
VeraPyuyi Oct 19, 2026
d279dee
feat(dotqueue): durable encrypted task queue with tests
VeraPyuyi Oct 19, 2026
1144745
fix(dotqueue): enforce reviewed queue security and lifecycle invariants
VeraPyuyi Oct 19, 2026
9555035
fix(dotqueue): reject browser numeric artifact host aliases
VeraPyuyi Oct 19, 2026
1893738
feat(dotevents): persist secure finite webhook subscriptions
VeraPyuyi Oct 19, 2026
8f01eed
fix(dotevents): align occurrence identity and enforce private store b…
VeraPyuyi Oct 19, 2026
accc755
feat(dotbridge): integrate queue/events runtime bridge
VeraPyuyi Oct 19, 2026
2024033
fix(dotbridge): wire queue notices and stabilize runtime tests
VeraPyuyi Oct 19, 2026
5628c72
fix(dotbridge): authorize paired queues and repair HTTP discovery
VeraPyuyi Oct 19, 2026
41f2748
feat(dotqueue): add scoped List for owner job history
VeraPyuyi Oct 19, 2026
8d15a46
fix(dotevents): address minor refresh and persistence rereview findings
VeraPyuyi Oct 19, 2026
219add2
dotcontrol: operator bridge bootstrap, pairing files, and CLI hardening
VeraPyuyi Oct 19, 2026
8acb176
feat(dotbridge): verify independent background task capability
VeraPyuyi Oct 19, 2026
29d22bc
fix(dotbridge): forward operator probe mode to runtime
VeraPyuyi Oct 19, 2026
b22df48
feat(dotqueue): expose capacity status and queue_full at limit
VeraPyuyi Oct 19, 2026
dde3121
test(dotqueue): align retained-job limit with queue_full
VeraPyuyi Oct 19, 2026
186ef95
fix(dotevents): separate persisted cache shape from clock expiry
VeraPyuyi Oct 19, 2026
7e96120
test(dotbridge): verify complete proof and artifact receipts
VeraPyuyi Oct 19, 2026
581eb55
docs(dotbridge): explain independent answer and task probes
VeraPyuyi Oct 19, 2026
07db630
docs(dotbridge): clarify pairing file command lifecycle
VeraPyuyi Oct 19, 2026
c690372
feat(dotbridge): expose scoped job metadata lists and align HTTP budgets
VeraPyuyi Oct 19, 2026
563d238
docs: link the optional Dot task bridge and acceptance limits
VeraPyuyi Oct 19, 2026
e437da6
test(dotbridge): verify exact budget, write-after-pause and list erro…
VeraPyuyi Oct 19, 2026
1b1bfdf
test(dotqueue): race two child processes for the last job slot
VeraPyuyi Oct 19, 2026
d437577
docs: Magpie dot pairing identity implementation report
VeraPyuyi Oct 19, 2026
2642ef7
Expose bridge capacity and job summaries for operators
VeraPyuyi Oct 19, 2026
fe5fffa
Gate capacity publication on final authorized status snapshot
VeraPyuyi Oct 19, 2026
70d2740
docs: record pairing identity review corrections and race evidence
VeraPyuyi Oct 19, 2026
9705d8f
fix(dotevents): retry transient Windows state replacement denials
VeraPyuyi Oct 19, 2026
a2882f6
test: synchronize capacity fixtures and retain child diagnostics
VeraPyuyi Oct 19, 2026
3549c5f
docs: qualify loaded event diagnostic log coverage
VeraPyuyi Oct 19, 2026
34ba3c5
fix(desktop): reject stale Windows parent PID relationships
VeraPyuyi Oct 19, 2026
9db5f33
test(dotbridge): install lifecycle callbacks before worker startup
VeraPyuyi Oct 19, 2026
5d16f98
fix(mcp-events): publish official catalog and flat subscribe result
VeraPyuyi Oct 20, 2026
537a271
fix(mcp-events): keep catalog discoverable before subscription
VeraPyuyi Oct 20, 2026
5b7d883
fix(mcp-events): classify subscription errors and format event occurr…
VeraPyuyi Oct 20, 2026
ba58a24
fix(events): expose SDK errors and private subscription diagnostics
VeraPyuyi Oct 20, 2026
c7055e4
fix(events): identify callback URL rejection rules safely
VeraPyuyi Oct 20, 2026
69ca107
fix(events): resolve callback hosts over DoH for fake-ip networks
VeraPyuyi Oct 20, 2026
019b4ee
fix(bridge): preserve verified capability across trusted subscription…
VeraPyuyi Oct 21, 2026
df4af32
fix(bridge): report verified legacy subscription leases after upgrade
VeraPyuyi Oct 21, 2026
8072411
test: compare ACL principals as SIDs and use physical temp roots
VeraPyuyi Oct 21, 2026
c7734a5
test: use physical temp roots for macOS bridge and queue fixtures
VeraPyuyi Oct 21, 2026
76b9ec8
test: use physical temp roots for event store fixtures
VeraPyuyi Oct 21, 2026
94ef582
test: use physical temp roots in mcp and event fixtures, widen takeov…
VeraPyuyi Oct 21, 2026
0601dd0
fix(queue): bound lock waits for race-slow transactions
VeraPyuyi Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@

有人使用电脑时,只允许经过执行器明确支持的后台动作,并避开正在使用的目标窗口。前台操作默认要求连续空闲 60 秒;人工接管、锁屏或活动检测失效时停止后续输入。后台能力因应用而异。

## 可选 Dot 任务桥接(开发分支)

[Dot 任务桥接](docs/DOT_BRIDGE.md)允许本机客户端提交持久任务,由连接 ChatGPT 的 Dot 通过 MCP Events 收到通知,再领取并回写结果。回答与后台任务分别验证;Magpie 集成使用独立配对凭据。该功能尚未发布,也尚未完成真实 ChatGPT/Dot 往返验收。它与下面的目标端 Codex 委派使用不同的执行路径。

## 目标端 Codex 委派

实现路线:源 Dot → 目标 DotControl → 目标 Codex → 官方 Computer Use/本机 SSH。
Expand Down
239 changes: 239 additions & 0 deletions cmd/dotcontrol/dot_bridge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/dotcontrol/dotcontrol/internal/config"
"github.com/dotcontrol/dotcontrol/internal/dotbridge"
"github.com/dotcontrol/dotcontrol/internal/localapi"
"github.com/dotcontrol/dotcontrol/internal/vault"
)

func bridgeCLI(ctx context.Context, dir string, c config.Config, v vault.Store, args []string) error {
if len(args) == 0 {
return errors.New("bridge command required")
}
switch args[0] {
case "enable":
if len(args) != 1 {
return errors.New("usage: bridge enable")
}
if !c.DotBridgeEnabled {
// Reload installed configuration instead of persisting a stale CLI snapshot.
current, err := config.Load(dir)
if err != nil {
return err
}
current.DotBridgeEnabled = true
if err := config.Save(dir, current); err != nil {
return err
}
fmt.Println(`{"configured":true,"restart_required":true,"next":"Restart the existing runtime, then run dotcontrol bridge enable"}`)
return nil
}
return bridgePost(ctx, c, v, "/admin/bridge/enable", nil)
case "disable":
return bridgePost(ctx, c, v, "/admin/bridge/disable", nil)
case "status":
return bridgePost(ctx, c, v, "/admin/bridge/status", nil)
case "pair":
return bridgePair(ctx, c, v)
case "revoke-client":
if len(args) < 2 {
return errors.New("client_id required")
}
return bridgePost(ctx, c, v, "/admin/bridge/revoke-client", map[string]string{"client_id": args[1]})
case "probe":
if len(args) < 2 {
return errors.New("client_id required")
}
mode := "answer"
if len(args) != 2 {
if len(args) != 4 || args[2] != "--mode" || (args[3] != "answer" && args[3] != "task") {
return errors.New("usage: bridge probe <client_id> [--mode answer|task]")
}
mode = args[3]
}
return bridgePost(ctx, c, v, "/admin/bridge/probe", map[string]string{"client_id": args[1], "mode": mode})
case "jobs":
if len(args) < 2 {
return errors.New("client_id required")
}
return bridgeClientGet(ctx, dir, c.Listen, args[1], "/dot-bridge/v1/jobs")
case "get":
if len(args) < 3 {
return errors.New("client_id and job_id required")
}
return bridgeClientGet(ctx, dir, c.Listen, args[1], "/dot-bridge/v1/jobs/"+args[2])
case "cancel":
if len(args) < 3 {
return errors.New("client_id and job_id required")
}
return bridgeClientPost(ctx, dir, c.Listen, args[1], "/dot-bridge/v1/jobs/"+args[2]+"/cancel", nil)
default:
return errors.New("unknown_bridge_command")
}
}

func bridgePair(ctx context.Context, c config.Config, v vault.Store) error {
data, err := bridgePostRaw(ctx, c, v, "/admin/bridge/pair", map[string]any{})
if err != nil {
return err
}
var full map[string]json.RawMessage
if json.Unmarshal(data, &full) != nil {
return errors.New("invalid_agent_response")
}
out := make(map[string]string, 3)
for _, key := range []string{"pairing_file", "client_id", "queue_id"} {
if raw, ok := full[key]; ok {
var value string
if json.Unmarshal(raw, &value) == nil && value != "" {
out[key] = value
}
}
}
if out["pairing_file"] == "" {
return errors.New("invalid_agent_response")
}
b, err := json.Marshal(out)
if err != nil {
return err
}
fmt.Println(string(b))
return nil
}

func bridgePost(ctx context.Context, c config.Config, v vault.Store, path string, body any) error {
data, err := bridgePostRaw(ctx, c, v, path, body)
if err != nil {
return err
}
fmt.Println(string(data))
return nil
}

func bridgePostRaw(ctx context.Context, c config.Config, v vault.Store, path string, body any) (json.RawMessage, error) {
if err := validateBridgeListen(c.Listen); err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
token, err := v.Get("admin-token")
if err != nil {
return nil, errors.New("local_credential_missing")
}
return localapi.Client{Address: c.Listen, Token: token}.Post(ctx, path, body)
}

func bridgeClientGet(ctx context.Context, dir, listen, clientID, path string) error {
if err := validateBridgeListen(listen); err != nil {
return err
}
token, err := loadPairingToken(dir, clientID)
if err != nil {
return err
}
return bridgeClientRequest(ctx, listen, token, http.MethodGet, path, nil)
}

func bridgeClientPost(ctx context.Context, dir, listen, clientID, path string, body any) error {
if err := validateBridgeListen(listen); err != nil {
return err
}
token, err := loadPairingToken(dir, clientID)
if err != nil {
return err
}
return bridgeClientRequest(ctx, listen, token, http.MethodPost, path, body)
}

func bridgeClientRequest(ctx context.Context, listen, token, method, path string, body any) error {
if err := validateBridgeListen(listen); err != nil {
return err
}
var reader io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return err
}
reader = strings.NewReader(string(b))
}
req, err := http.NewRequestWithContext(ctx, method, "http://"+listen+path, reader)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
transport := &http.Transport{Proxy: nil, DisableKeepAlives: true}
defer transport.CloseIdleConnections()
client := &http.Client{Transport: transport, Timeout: 10 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return errors.New("redirect_refused") }}
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
data, err := readBridgeResponseBody(res.Body)
if err != nil {
return err
}
fmt.Println(string(data))
if res.StatusCode >= 400 {
return errors.New("bridge_request_failed")
}
return nil
}

func loadPairingToken(dir, clientID string) (string, error) {
file, err := dotbridge.ReadPairingFile(filepath.Join(dir, "dot-bridge"), clientID)
if err != nil {
return "", err
}
return file.Token, nil
}

func readBridgeResponseBody(r io.Reader) ([]byte, error) {
data, err := io.ReadAll(io.LimitReader(r, dotbridge.MaxWireResponseBytes+1))
if err != nil {
return nil, err
}
if len(data) > dotbridge.MaxWireResponseBytes {
return nil, errors.New("bridge_response_too_large")
}
return data, nil
}

func validateBridgeListen(listen string) error {
host, port, err := net.SplitHostPort(listen)
if err != nil {
return errors.New("invalid_local_address")
}
ip, err := netip.ParseAddr(host)
n, portErr := strconv.Atoi(port)
if err != nil || !ip.IsLoopback() || ip.Zone() != "" || portErr != nil || n < 1 || n > 65535 {
return errors.New("invalid_local_address")
}
return nil
}

// Optional bridge failures preserve the existing runtime and its default catalog.
func openRuntimeDotBridge(ctx context.Context, dir string, c config.Config, v vault.Store, paused, revoked func() bool) (*dotbridge.Service, error) {
if e := dotbridge.EnsureStateKey(v); e != nil {
return nil, e
}
return dotbridge.Open(ctx, dotbridge.Config{Root: filepath.Join(dir, "dot-bridge"), Listen: c.Listen, Vault: v, MCPQueue: c.DeviceID, Paused: paused, Revoked: revoked})
}
Loading
Loading