Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ TUNNEL_TOKEN=replace-with-cloudflare-tunnel-token
# 可选:浏览器自动化镜像。
# AGENTDOCK_IMAGE=ghcr.io/uvwt/agentdock:browser-latest
# AGENTDOCK_BROWSER_ENABLED=true

# AGENTS.md 原生自动上下文,默认开启。false 只禁用自动发现,不禁用显式 instructions 文件。
# 这些变量需由实际 Core 启动环境或容器 environment/env_file 传入。
# AGENTDOCK_AGENTS_AUTOLOAD=true
# 可选:覆盖 ${AGENTDOCK_HOME}/AGENTS.md 的全局规则来源,必须为 Core 可访问的绝对路径。
# AGENTDOCK_INSTRUCTIONS_FILE=/path/to/global/AGENTS.md
14 changes: 8 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -610,12 +610,13 @@ jobs:

"$install_dir/bin/agentdock" --help >/dev/null 2>&1

test -f "$data_dir/.agentdock/skill-store/bundled-skills.json"
test -f "$data_dir/.agentdock/skills/.system/.agentdock-system-skills.marker"
for skill in skill-authoring skill-installation; do
state="$data_dir/.agentdock/skill-store/state/$skill.json"
state="$data_dir/.agentdock/skills/.state/$skill.json"
version="$(jq -r .active_version "$state")"
test -n "$version"
test -f "$data_dir/.agentdock/skill-store/installed/$skill/$version/SKILL.md"
test "$(jq -r .system "$state")" = "true"
test -f "$data_dir/.agentdock/skills/.system/$skill/SKILL.md"
done

systemd_dir="$RUNNER_TEMP/systemd"
Expand Down Expand Up @@ -726,11 +727,12 @@ jobs:
AGENTDOCK_INSTALL_DIR="$RUNNER_TEMP/bin" \
sh "$installer" --version "$tag"
"$RUNNER_TEMP/bin/agentdock" --help >/dev/null 2>&1
test -f "$RUNNER_TEMP/home/.agentdock/skill-store/bundled-skills.json"
test -f "$RUNNER_TEMP/home/.agentdock/skills/.system/.agentdock-system-skills.marker"
for skill in skill-authoring skill-installation; do
state="$RUNNER_TEMP/home/.agentdock/skill-store/state/$skill.json"
state="$RUNNER_TEMP/home/.agentdock/skills/.state/$skill.json"
version="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["active_version"])' "$state")"
test -f "$RUNNER_TEMP/home/.agentdock/skill-store/installed/$skill/$version/SKILL.md"
test "$(python3 -c 'import json,sys; print(str(bool(json.load(open(sys.argv[1])).get("system"))).lower())' "$state")" = "true"
test -f "$RUNNER_TEMP/home/.agentdock/skills/.system/$skill/SKILL.md"
done
HOME="$RUNNER_TEMP/home" \
AGENTDOCK_INSTALLER_BASE_URL="$release_base" \
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/windows-installer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ jobs:
$env:GOOS = 'windows'
$env:GOARCH = 'amd64'
go build -trimpath -o $runtimeTestAgentDockBinary .\cmd\agentdock
if ($LASTEXITCODE -ne 0) { throw 'Runtime test Core build failed.' }
$runtimeTestNativeLauncher = Join-Path $env:RUNNER_TEMP 'agentdock-tray-shim.exe'
go build -trimpath -ldflags '-s -w -H=windowsgui' -o $runtimeTestNativeLauncher .\cmd\agentdock-shim
if ($LASTEXITCODE -ne 0) { throw 'Runtime test native GUI launcher build failed.' }
& .\scripts\test\test-windows-runtime-launch-diagnostics.ps1 `
-LauncherPath .\scripts\install\launch-windows-process.ps1 `
-AgentDockBinary $runtimeTestAgentDockBinary
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ English | [简体中文](./README.zh-CN.md)

# AgentDock MCP

This fork's Windows release: **[AgentDock 1.0.1](https://github.com/A-m-o-r-F-a-t-i/agentdock/releases/tag/v1.0.1)** · [Fixes and verification scope](./docs/releases/v1.0.1.md). Version 1.0.0 is no longer recommended for Windows upgrades.

**Give AI agents secure, controlled access to every machine you operate.**

Open ChatGPT in your browser and manage multiple computers and servers from one conversation. Write code, change configuration, run commands, and deploy in the real environment where the work belongs—without consuming a dedicated Codex coding quota.
Expand Down Expand Up @@ -123,13 +125,15 @@ AgentDock exposes tools over MCP Streamable HTTP. The exact client syntax varies

Official and community Skill sources live in [uvwt/agentdock-skills](https://github.com/uvwt/agentdock-skills). This repository only keeps core Skills that must ship with the AgentDock runtime, including bootstrap/security Skills and the built-in `agentdock-user-guide` official user guide.

- Validate, install, uninstall, activate, and roll back Skill packages
- Validate, install, uninstall, activate, enable, disable, and roll back Skill packages
- Store active Skills in a [Codex-style visible directory tree](docs/skill-directory-layout.md) while keeping inactive versions and transaction state hidden
- Stable, development, canary, and pinned release channels
- Isolated environment variables and runtimes for each Skill
- Register, enable, disable, refresh, and remove dynamic MCP servers
- Streamable HTTP and stdio transports
- Search tools, inspect schemas, and perform controlled calls
- Configuration isolation between MCP servers
- Install [self-contained heavy plugins](docs/heavy-plugins.md) that directly carry their Skills, MCP definitions, implementation files, switches, and two-stage progressive disclosure without a plugin cache layer

### Native ACP

Expand Down
6 changes: 5 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

# AgentDock MCP

本分支 Windows 发行版:**[AgentDock 1.0.1](https://github.com/A-m-o-r-F-a-t-i/agentdock/releases/tag/v1.0.1)** · [修复内容与验证范围](./docs/releases/v1.0.1.md)。1.0.0 已撤下推荐状态,请勿继续用于覆盖安装。

**让 AI 的双手,真正触达你的每一台设备。**

打开网页版 ChatGPT,即可管理多台电脑与服务器:在真实设备上写代码、改配置、跑命令与部署,执行发生在你的机器上,不消耗Codex额度。
Expand Down Expand Up @@ -127,13 +129,15 @@ AgentDock 通过 MCP Streamable HTTP 提供工具能力。下面是一个通用

官方与社区 Skill 源码统一维护在 [uvwt/agentdock-skills](https://github.com/uvwt/agentdock-skills)。本仓库只保留必须随 AgentDock 运行时发布的核心 Skill,包括自举/安全相关 Skill,以及内置的 `agentdock-user-guide` 官方用户指南。

- Skill 包校验、安装、卸载、激活和回滚
- Skill 包校验、安装、卸载、激活、启停和回滚
- 当前激活 Skill 使用[与 Codex 一致的可见目录结构](docs/skill-directory-layout.md),非激活版本和事务状态保存在隐藏目录
- 稳定版、开发版、Canary 和固定版本通道
- Skill 独立环境变量与运行环境
- 动态 MCP Server 注册、启停、刷新和移除
- Streamable HTTP 与 stdio 传输
- 工具搜索、Schema 检查和受控调用
- MCP Server 之间的配置隔离
- 安装[自包含重插件](docs/heavy-plugins.md):插件直接携带 Skill、MCP 定义、实现文件和开关,按两层渐进式方式暴露能力,不增加插件 cache 中间层

### 原生 ACP

Expand Down
74 changes: 71 additions & 3 deletions cmd/agentdock-shim/main_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package main

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
Expand All @@ -14,19 +15,30 @@ import (

"golang.org/x/sys/windows"

"github.com/uvwt/agentdock/internal/desktopruntime"
"github.com/uvwt/agentdock/internal/fs/processlock"
processctl "github.com/uvwt/agentdock/internal/process"
"github.com/uvwt/agentdock/internal/updateengine"
)

func main() {
if len(os.Args) == 3 && os.Args[1] == "--setup-exec" {
code, err := desktopruntime.RunSetupExecutor(os.Args[2])
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
}
os.Exit(code)
}
if err := run(); err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func run() error {
if len(os.Args) == 3 && (os.Args[1] == "--setup-launch" || os.Args[1] == "--setup-worker") {
return desktopruntime.RunSetupLauncher(os.Args[2], os.Args[1] == "--setup-worker")
}
executable, err := os.Executable()
if err != nil {
return fmt.Errorf("resolve AgentDock stable entry: %w", err)
Expand All @@ -44,12 +56,12 @@ func run() error {
if err != nil {
return err
}
active, err := resolveActiveWithRecovery(root, store, layout)
tray := strings.EqualFold(filepath.Base(executable), updateengine.StableTrayShimName)
active, err := resolveActiveWithRecovery(root, store, layout, !tray && coreLaunchRequiresParentLifetime(os.Args[1:]))
if err != nil {
return err
}

tray := strings.EqualFold(filepath.Base(executable), updateengine.StableTrayShimName)
target := layout.GenerationCore(active.ActiveVersion)
if tray {
target = layout.GenerationTray(active.ActiveVersion)
Expand Down Expand Up @@ -109,12 +121,26 @@ func coreLaunchRequiresParentLifetime(args []string) bool {
strings.EqualFold(strings.TrimSpace(args[1]), "launch-core")
}

func resolveActiveWithRecovery(root string, store *updateengine.Store, layout updateengine.WindowsLayout) (updateengine.ActiveVersion, error) {
func resolveActiveWithRecovery(root string, store *updateengine.Store, layout updateengine.WindowsLayout, allowInstallerHost ...bool) (updateengine.ActiveVersion, error) {
active, err := store.ReadActive()
if err != nil {
return updateengine.ActiveVersion{}, fmt.Errorf("read AgentDock active version: %w", err)
}

// An elevated Core task must enter through the stable, job-owning shim while
// Installer Engine is synchronously verifying its trial. Only that service
// host entry may use a matching live install journal; ordinary commands and
// abandoned trials retain the fail-closed behavior below.
if active.State == updateengine.StateTrial && len(allowInstallerHost) > 0 && allowInstallerHost[0] {
live, err := liveInstallerTrial(root, active)
if err != nil {
return updateengine.ActiveVersion{}, err
}
if live {
return active, nil
}
}

transaction, transactionErr := store.ReadTransaction()
if transactionErr != nil {
if active.State == updateengine.StateTrial {
Expand Down Expand Up @@ -197,3 +223,45 @@ func trayRequiresWait(args []string) bool {
}
return false
}

// liveInstallerTrial never commits, repairs, or discards installation state.
func liveInstallerTrial(root string, active updateengine.ActiveVersion) (bool, error) {
data, err := os.ReadFile(filepath.Join(root, "install", "transaction.json"))
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("read installer trial: %w", err)
}
var transaction struct {
SchemaVersion int `json:"schema_version"`
TransactionID string `json:"transaction_id"`
Action string `json:"action"`
State updateengine.State `json:"state"`
Phase string `json:"phase"`
TargetVersion string `json:"target_version"`
InstallRoot string `json:"install_root"`
RuntimeRoot string `json:"runtime_root"`
}
if err := json.Unmarshal(data, &transaction); err != nil {
return false, fmt.Errorf("decode installer trial: %w", err)
}
if transaction.SchemaVersion != 1 || transaction.TransactionID == "" ||
transaction.TransactionID != active.TransactionID ||
(transaction.Action != "install" && transaction.Action != "repair") ||
transaction.State != updateengine.StateTrial || active.State != updateengine.StateTrial ||
(transaction.Phase != "start" && transaction.Phase != "health") ||
updateengine.NormalizeVersion(transaction.TargetVersion) != updateengine.NormalizeVersion(active.ActiveVersion) ||
!strings.EqualFold(filepath.Clean(transaction.InstallRoot), filepath.Clean(root)) ||
!strings.EqualFold(filepath.Clean(transaction.RuntimeRoot), filepath.Clean(root)) {
return false, nil
}
lock, acquired, err := processlock.TryAcquire(filepath.Join(root, "install", "transaction.lock"))
if err != nil {
return false, fmt.Errorf("probe installer trial lock: %w", err)
}
if acquired {
return false, lock.Release()
}
return true, nil
}
86 changes: 85 additions & 1 deletion cmd/agentdock-shim/main_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

package main

import "testing"
import (
"encoding/json"
"github.com/uvwt/agentdock/internal/fs/processlock"
"github.com/uvwt/agentdock/internal/updateengine"
"os"
"path/filepath"
"testing"
)

func TestTrayRequiresWaitOnlyDetachesNormalBackgroundLaunches(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -49,3 +56,80 @@ func TestCoreLaunchRequiresParentLifetimeOnlyForServiceHost(t *testing.T) {
})
}
}

func TestInstallerTrialRequiresLiveMatchingOwner(t *testing.T) {
root := t.TempDir()
directory := filepath.Join(root, "install")
if err := os.MkdirAll(directory, 0700); err != nil {
t.Fatal(err)
}
active := updateengine.ActiveVersion{SchemaVersion: 1, State: updateengine.StateTrial, TransactionID: "install-test", ActiveVersion: "v0.8.4"}
valid := func() map[string]any {
return map[string]any{"schema_version": 1, "action": "install", "transaction_id": "install-test", "state": "trial", "phase": "start", "target_version": "v0.8.4", "install_root": root, "runtime_root": root}
}
write := func(value map[string]any) {
t.Helper()
data, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(directory, "transaction.json"), data, 0600); err != nil {
t.Fatal(err)
}
}
write(valid())
if live, err := liveInstallerTrial(root, active); err != nil || live {
t.Fatalf("unowned trial: live=%v err=%v", live, err)
}
lock, acquired, err := processlock.TryAcquire(filepath.Join(directory, "transaction.lock"))
if err != nil || !acquired {
t.Fatalf("hold fixture lock: %v", err)
}
defer lock.Release()
for _, test := range []struct {
name, key string
value any
want bool
}{
{name: "matching start", want: true},
{name: "matching health", key: "phase", value: "health", want: true},
{name: "wrong id", key: "transaction_id", value: "other"},
{name: "wrong target", key: "target_version", value: "v0.8.3"},
{name: "unsupported schema", key: "schema_version", value: 2},
{name: "failed", key: "state", value: "failed"},
{name: "uninstall", key: "action", value: "uninstall"},
{name: "adapter commit wait", key: "phase", value: "commit"},
{name: "wrong install root", key: "install_root", value: filepath.Join(root, "other")},
{name: "wrong runtime root", key: "runtime_root", value: filepath.Join(root, "other")},
} {
t.Run(test.name, func(t *testing.T) {
value := valid()
if test.key != "" {
value[test.key] = test.value
}
write(value)
live, err := liveInstallerTrial(root, active)
if err != nil || live != test.want {
t.Fatalf("live=%v want=%v err=%v", live, test.want, err)
}
})
}
write(valid())
store, err := updateengine.NewStore(root)
if err != nil {
t.Fatal(err)
}
if err := store.WriteActive(active); err != nil {
t.Fatal(err)
}
layout, err := updateengine.NewWindowsLayout(root)
if err != nil {
t.Fatal(err)
}
if _, err := resolveActiveWithRecovery(root, store, layout); err == nil {
t.Fatal("ordinary shim command must not enter an installer trial")
}
if got, err := resolveActiveWithRecovery(root, store, layout, true); err != nil || got.TransactionID != active.TransactionID {
t.Fatalf("live service host: %+v %v", got, err)
}
}
7 changes: 7 additions & 0 deletions cmd/agentdock/command_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ func runInstallCommand(ctx context.Context, args []string, stdout, stderr io.Wri
if len(args) > 0 && args[0] == "commit" {
return runInstallCommit(ctx, args[1:], stdout, stderr)
}
if len(args) > 0 && args[0] == "restore-files" {
return runInstallRestoreFiles(ctx, args[1:], stdout, stderr)
}
if len(args) > 0 && args[0] == "prepare-windows-legacy" {
return runInstallPrepareWindowsLegacy(ctx, args[1:], stdout, stderr)
}
Expand Down Expand Up @@ -58,6 +61,7 @@ func runInstallCommand(ctx context.Context, args []string, stdout, stderr io.Wri
flags.StringVar(&request.BinaryPath, "binary", "", "已就位的 agentdock 二进制")
flags.StringVar(&request.SkillBundle, "skill-bundle", "", "核心 Skill Bundle 目录")
flags.StringVar(&request.Version, "version", "", "目标版本")
flags.StringVar(&request.TransactionID, "transaction-id", "", "OS adapter 预分配的32位十六进制事务 ID")
flags.StringVar(&request.Channel, "channel", "official", "安装通道")
// host/port 默认必须是“未指定”,不能写成 127.0.0.1:8765。
// 否则 update/repair 省略这两个标志时会把用户已有监听地址覆盖掉。
Expand Down Expand Up @@ -231,6 +235,7 @@ func runInstallAbandon(ctx context.Context, args []string, stdout, stderr io.Wri
flags.SetOutput(stderr)
var request installer.Request
request.Action = installer.ActionAbandon
flags.BoolVar(&request.RequireHealth, "require-health", false, "确认原版本健康后才结束回滚")
flags.StringVar(&request.InstallRoot, "install-root", "", "安装根目录")
flags.StringVar(&request.RuntimeRoot, "runtime-root", "", "运行配置目录")
flags.StringVar(&request.TransactionID, "transaction-id", "", "要撤销的 install 事务 ID")
Expand All @@ -254,6 +259,8 @@ func runInstallCommit(ctx context.Context, args []string, stdout, stderr io.Writ
flags.SetOutput(stderr)
var request installer.Request
request.Action = installer.ActionCommit
flags.BoolVar(&request.RequireHealth, "require-health", false, "提交前确认目标版本健康")
flags.BoolVar(&request.KeepJournal, "keep-journal", false, "为外层安装激活阶段保留回滚文件")
flags.StringVar(&request.InstallRoot, "install-root", "", "安装根目录")
flags.StringVar(&request.RuntimeRoot, "runtime-root", "", "运行配置目录")
flags.StringVar(&request.TransactionID, "transaction-id", "", "要提交的 install 事务 ID")
Expand Down
33 changes: 33 additions & 0 deletions cmd/agentdock/command_install_restore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"context"
"encoding/json"
"errors"
"flag"
"io"
"strings"

"github.com/uvwt/agentdock/internal/installer"
)

func runInstallRestoreFiles(ctx context.Context, args []string, stdout, stderr io.Writer) error {
flags := flag.NewFlagSet("agentdock install restore-files", flag.ContinueOnError)
flags.SetOutput(stderr)
request := installer.Request{Action: installer.ActionRestoreFiles}
flags.StringVar(&request.InstallRoot, "install-root", "", "安装根目录")
flags.StringVar(&request.RuntimeRoot, "runtime-root", "", "运行配置目录")
flags.StringVar(&request.TransactionID, "transaction-id", "", "本次事务 ID,必填")
flags.StringVar(&request.PayloadDir, "payload-dir", "", "已验证的新载荷,用于停止运行时")
if err := flags.Parse(args); err != nil {
return err
}
if flags.NArg() != 0 || strings.TrimSpace(request.TransactionID) == "" {
return errors.New("restore-files requires --install-root and --transaction-id")
}
result, err := (installer.Engine{}).Run(ctx, request)
if err != nil {
return err
}
return json.NewEncoder(stdout).Encode(result)
}
Loading