Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

260 lines
8.7 KiB
Go

package runtimeprep
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
const cursorPluginDirEnv = "TUTTI_CURSOR_PLUGIN_DIR"
const cursorPromptContextFileEnv = "TUTTI_CURSOR_PROMPT_CONTEXT_FILE"
const cursorWindowsFileSearchPolicy = `## Windows file-search boundary
- Cursor ACP's built-in Glob and Grep do not reliably execute drive-qualified absolute Windows paths such as C:\... or C:/.... Do not pass those paths as path or target_directory.
- When the target is inside the current session workspace, use a workspace-relative path with Glob or Grep; use . for the workspace root. Treat the session cwd as the workspace root.
- When the target is outside the workspace or a relative conversion is uncertain, use the native terminal with rg or PowerShell and the exact absolute Windows path instead of Glob or Grep.
- If Glob or Grep reports a Windows path or rg error, retry through the relative-path or native-terminal route. Report only files returned by that retry.`
const (
cursorBackgroundTaskGuardCommand = `"${CURSOR_PLUGIN_ROOT}/hooks/guard-background-task.sh"`
cursorBackgroundTaskGuardDeniedMessage = "Tutti's Cursor ACP integration does not support background Task execution. Retry this Task in the foreground without run_in_background=true."
)
// The background Task guard remains dormant. RTK uses a separate Shell-only
// preToolUse hook supported by current Cursor Agent plugin discovery; the Task
// guard must not be treated as protection against detached background Tasks.
const cursorBackgroundTaskGuardScript = `#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
resolve_cursor_node() {
local invoked="${CURSOR_INVOKED_AS:-}"
local candidate=""
local resolved=""
for name in "$invoked" cursor-agent agent; do
if [[ -z "$name" ]]; then
continue
fi
candidate="$(command -v "$name" 2>/dev/null || true)"
if [[ -n "$candidate" ]]; then
break
fi
done
if [[ -z "$candidate" ]]; then
return 1
fi
if command -v realpath >/dev/null 2>&1; then
resolved="$(realpath "$candidate")"
else
resolved="$candidate"
if [[ -L "$candidate" ]]; then
local target
target="$(readlink "$candidate")"
if [[ "$target" = /* ]]; then
resolved="$target"
else
resolved="$(cd "$(dirname "$candidate")" && pwd)/$target"
fi
fi
fi
local node_bin
node_bin="$(dirname "$resolved")/node"
if [[ ! -x "$node_bin" ]]; then
return 1
fi
printf '%s\n' "$node_bin"
}
if node_bin="$(resolve_cursor_node)"; then
payload="$(cat)"
if output="$(printf '%s' "$payload" | "$node_bin" "$script_dir/guard-background-task.mjs")"; then
printf '%s\n' "$output"
exit 0
fi
fi
# Fail closed when Cursor's bundled Node runtime cannot be located. If this
# dormant hook is enabled in a future compatible ACP runtime, it must never
# silently allow a matched Task to bypass the guard.
printf '%s\n' "{\"permission\":\"deny\",\"user_message\":\"` + cursorBackgroundTaskGuardDeniedMessage + `\"}"
`
const cursorBackgroundTaskGuardJavaScript = `const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
const deny = () => ({
permission: "deny",
user_message: "` + cursorBackgroundTaskGuardDeniedMessage + `",
});
try {
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
const input = payload?.tool_input ?? payload?.toolInput;
if (input === null || typeof input !== "object" || Array.isArray(input)) {
process.stdout.write(JSON.stringify(deny()));
} else {
const background = input.run_in_background ?? input.runInBackground;
process.stdout.write(JSON.stringify(background === true || background === "true" ? deny() : {}));
}
} catch {
process.stdout.write(JSON.stringify(deny()));
}
`
type CursorPreparer struct{}
func (CursorPreparer) Provider() string {
return "cursor"
}
func (CursorPreparer) Prepare(_ context.Context, input ProviderPrepareInput) (ProviderPrepareResult, error) {
pluginDir := filepath.Join(input.RuntimeRoot, "cursor-plugin", "tutti-cli")
if err := installCursorTuttiPlugin(pluginDir, input.PrepareInput); err != nil {
return ProviderPrepareResult{}, err
}
if input.Manifest != nil {
input.Manifest.RecordManagedFile(pluginDir, "provider-plugin", true)
}
return ProviderPrepareResult{
Cwd: input.Cwd,
Env: []string{
cursorPluginDirEnv + "=" + pluginDir,
cursorPromptContextFileEnv + "=" + filepath.Join(pluginDir, "tutti-context.md"),
},
}, nil
}
func installCursorTuttiPlugin(pluginDir string, input PrepareInput) error {
policy, err := tuttiCLIPolicy(input)
if err != nil {
return fmt.Errorf("render cursor prompt context: %w", err)
}
manifestDir := filepath.Join(pluginDir, ".cursor-plugin")
if err := os.MkdirAll(manifestDir, 0o700); err != nil {
return fmt.Errorf("create cursor plugin manifest directory: %w", err)
}
manifest := struct {
Name string `json:"name"`
DisplayName string `json:"displayName"`
Version string `json:"version"`
Description string `json:"description"`
Author map[string]string `json:"author"`
License string `json:"license"`
Skills string `json:"skills"`
Rules []string `json:"rules"`
}{
Name: "tutti-cli",
DisplayName: "Tutti CLI",
Version: "0.1.0",
Description: "Tutti CLI skills for AgentGUI sessions.",
Author: map[string]string{
"name": "Tutti",
},
License: "UNLICENSED",
Skills: "./skills/",
Rules: []string{},
}
content, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return fmt.Errorf("encode cursor plugin manifest: %w", err)
}
if err := os.WriteFile(filepath.Join(manifestDir, "plugin.json"), append(content, '\n'), 0o600); err != nil {
return fmt.Errorf("write cursor plugin manifest: %w", err)
}
skillPaths, err := installProviderNativeSkillsSessionScoped(filepath.Join(pluginDir, "skills"), input)
if err != nil {
return fmt.Errorf("install cursor tutti skill plugin: %w", err)
}
context := renderProviderPromptContext(cursorPromptPolicy(policy), skillPaths)
if err := os.WriteFile(filepath.Join(pluginDir, "tutti-context.md"), []byte(context+"\n"), 0o600); err != nil {
return fmt.Errorf("write cursor prompt context: %w", err)
}
if input.RTKSaverMode {
if err := installCursorRTKHook(filepath.Join(pluginDir, "hooks")); err != nil {
return err
}
}
return nil
}
func installCursorRTKHook(hooksDir string) error {
if err := os.MkdirAll(hooksDir, 0o700); err != nil {
return fmt.Errorf("create cursor RTK hooks directory: %w", err)
}
document := map[string]any{
"version": 1,
"hooks": map[string]any{
"preToolUse": []any{map[string]any{
"matcher": "Shell",
"command": "rtk hook cursor",
}},
},
}
content, err := json.MarshalIndent(document, "", " ")
if err != nil {
return fmt.Errorf("encode cursor RTK hooks: %w", err)
}
if err := os.WriteFile(filepath.Join(hooksDir, "hooks.json"), append(content, '\n'), 0o600); err != nil {
return fmt.Errorf("write cursor RTK hooks: %w", err)
}
return nil
}
func cursorPromptPolicy(policy string) string {
return cursorPromptPolicyForGOOS(policy, runtime.GOOS)
}
func cursorPromptPolicyForGOOS(policy string, goos string) string {
policy = strings.TrimSpace(policy)
if goos != "windows" {
return policy
}
return strings.TrimSpace(policy + "\n\n" + cursorWindowsFileSearchPolicy)
}
func installCursorBackgroundTaskGuard(hooksDir string) error {
if err := os.MkdirAll(hooksDir, 0o700); err != nil {
return fmt.Errorf("create cursor plugin hooks directory: %w", err)
}
hooks := struct {
Version int `json:"version"`
Hooks struct {
PreToolUse []struct {
Matcher string `json:"matcher"`
Command string `json:"command"`
} `json:"preToolUse"`
} `json:"hooks"`
}{Version: 1}
hooks.Hooks.PreToolUse = []struct {
Matcher string `json:"matcher"`
Command string `json:"command"`
}{
{Matcher: "^Task$", Command: cursorBackgroundTaskGuardCommand},
}
content, err := json.MarshalIndent(hooks, "", " ")
if err != nil {
return fmt.Errorf("encode cursor plugin hooks: %w", err)
}
if err := os.WriteFile(filepath.Join(hooksDir, "hooks.json"), append(content, '\n'), 0o600); err != nil {
return fmt.Errorf("write cursor plugin hooks: %w", err)
}
if err := os.WriteFile(filepath.Join(hooksDir, "guard-background-task.sh"), []byte(cursorBackgroundTaskGuardScript), 0o700); err != nil {
return fmt.Errorf("write cursor background Task guard launcher: %w", err)
}
if err := os.WriteFile(filepath.Join(hooksDir, "guard-background-task.mjs"), []byte(cursorBackgroundTaskGuardJavaScript), 0o600); err != nil {
return fmt.Errorf("write cursor background Task guard: %w", err)
}
return nil
}