diff --git a/.gitignore b/.gitignore index 8ddfece5..fcd6eada 100644 --- a/.gitignore +++ b/.gitignore @@ -56,8 +56,11 @@ agent/test/MULTI_TURN_DESIGN.md agent/test/UPGRADE_PLAN.md introduction/* !sandbox/docker/build.sh +!sandbox/docker/vnc/*.sh +!sandbox/docker/desktop/config/*.sh sandbox/docker/yao-bridge-* sandbox/docker/claude-proxy-* sandbox/docker/claude/claude-proxy-* sandbox/proxy/claude-proxy-linux-* release/* +sandbox/TODO-VNC.md diff --git a/agent/context/jsapi_sandbox.go b/agent/context/jsapi_sandbox.go index 8439d691..9ad1edc6 100644 --- a/agent/context/jsapi_sandbox.go +++ b/agent/context/jsapi_sandbox.go @@ -4,6 +4,7 @@ import ( "context" "github.com/yaoapp/gou/runtime/v8/bridge" + openapiSandbox "github.com/yaoapp/yao/openapi/sandbox" infraSandbox "github.com/yaoapp/yao/sandbox" "rogchap.com/v8go" ) @@ -22,6 +23,12 @@ type SandboxExecutor interface { // Workspace info GetWorkDir() string + + // Sandbox identification + GetSandboxID() string + + // VNC access (returns empty string if not available) + GetVNCUrl() string } // SetSandboxExecutor sets the sandbox executor for this context @@ -54,6 +61,8 @@ func (ctx *Context) newSandboxObject(iso *v8go.Isolate) *v8go.ObjectTemplate { sandboxObj.Set("WriteFile", ctx.sandboxWriteFileMethod(iso)) sandboxObj.Set("ListDir", ctx.sandboxListDirMethod(iso)) sandboxObj.Set("Exec", ctx.sandboxExecMethod(iso)) + sandboxObj.Set("GetVNCUrl", ctx.sandboxGetVNCUrlMethod(iso)) + sandboxObj.Set("GetSandboxID", ctx.sandboxGetSandboxIDMethod(iso)) return sandboxObj } @@ -72,6 +81,19 @@ func (ctx *Context) createSandboxInstance(v8ctx *v8go.Context) *v8go.Value { // Set workdir as a property sandboxTemplate.Set("workdir", ctx.sandboxExecutor.GetWorkDir()) + // Set sandbox_id as a property + sandboxID := ctx.sandboxExecutor.GetSandboxID() + sandboxTemplate.Set("sandbox_id", sandboxID) + + // Set vnc_url as a property (empty string if not available) + // GetVNCUrl returns sandbox ID if VNC is supported, empty otherwise + vncSandboxID := ctx.sandboxExecutor.GetVNCUrl() + if vncSandboxID != "" { + sandboxTemplate.Set("vnc_url", openapiSandbox.GetVNCClientURL(vncSandboxID)) + } else { + sandboxTemplate.Set("vnc_url", "") + } + instance, err := sandboxTemplate.NewInstance(v8ctx) if err != nil { return nil @@ -233,3 +255,48 @@ func (ctx *Context) sandboxExecMethod(iso *v8go.Isolate) *v8go.FunctionTemplate return jsVal }) } + +// sandboxGetVNCUrlMethod implements ctx.sandbox.GetVNCUrl() +func (ctx *Context) sandboxGetVNCUrlMethod(iso *v8go.Isolate) *v8go.FunctionTemplate { + return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { + v8ctx := info.Context() + + if ctx.sandboxExecutor == nil { + return bridge.JsException(v8ctx, "sandbox executor not available") + } + + // GetVNCUrl returns sandbox ID if VNC is supported, empty otherwise + vncSandboxID := ctx.sandboxExecutor.GetVNCUrl() + vncUrl := "" + if vncSandboxID != "" { + vncUrl = openapiSandbox.GetVNCClientURL(vncSandboxID) + } + + jsVal, err := v8go.NewValue(iso, vncUrl) + if err != nil { + return bridge.JsException(v8ctx, err.Error()) + } + + return jsVal + }) +} + +// sandboxGetSandboxIDMethod implements ctx.sandbox.GetSandboxID() +func (ctx *Context) sandboxGetSandboxIDMethod(iso *v8go.Isolate) *v8go.FunctionTemplate { + return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { + v8ctx := info.Context() + + if ctx.sandboxExecutor == nil { + return bridge.JsException(v8ctx, "sandbox executor not available") + } + + sandboxID := ctx.sandboxExecutor.GetSandboxID() + + jsVal, err := v8go.NewValue(iso, sandboxID) + if err != nil { + return bridge.JsException(v8ctx, err.Error()) + } + + return jsVal + }) +} diff --git a/agent/context/jsapi_sandbox_test.go b/agent/context/jsapi_sandbox_test.go index 414347f0..b040b413 100644 --- a/agent/context/jsapi_sandbox_test.go +++ b/agent/context/jsapi_sandbox_test.go @@ -88,6 +88,17 @@ func (e *realSandboxExecutor) GetWorkDir() string { return e.workDir } +func (e *realSandboxExecutor) GetSandboxID() string { + // Extract sandbox ID from container name (format: yao-sandbox-{userID}-{chatID}) + // For tests, just return a mock ID + return "test-user-test-chat" +} + +func (e *realSandboxExecutor) GetVNCUrl() string { + // Tests don't use VNC, return empty + return "" +} + // TestJsSandboxNotAvailable tests ctx.sandbox when not configured func TestJsSandboxNotAvailable(t *testing.T) { test.Prepare(t, config.Conf) diff --git a/agent/sandbox/claude/executor.go b/agent/sandbox/claude/executor.go index 158bbfd4..8a2c6087 100644 --- a/agent/sandbox/claude/executor.go +++ b/agent/sandbox/claude/executor.go @@ -79,7 +79,12 @@ func NewExecutor(manager *infraSandbox.Manager, opts interface{}) (*Executor, er // Create or get container // Note: IPC session is created by manager.createContainer, socket is already bind mounted ctx := context.Background() - container, err := manager.GetOrCreate(ctx, execOpts.UserID, execOpts.ChatID) + createOpts := infraSandbox.CreateOptions{ + UserID: execOpts.UserID, + ChatID: execOpts.ChatID, + Image: execOpts.Image, + } + container, err := manager.GetOrCreate(ctx, execOpts.UserID, execOpts.ChatID, createOpts) if err != nil { return nil, fmt.Errorf("failed to create container: %w", err) } @@ -556,11 +561,21 @@ func (e *Executor) parseStream(ctx *agentContext.Context, reader io.Reader, hand switch eventType { case "content_block_start": - // Check if this is a tool_use block starting - // Format: {"event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"...","name":"Write","input":{}}}} + // Handle new content blocks + // Format: {"event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use"|"text",...}}} if contentBlock, ok := event["content_block"].(map[string]interface{}); ok { blockType, _ := contentBlock["type"].(string) - if blockType == "tool_use" { + switch blockType { + case "text": + // New text block starting - add paragraph separator if we already have content + // This ensures proper separation between text blocks across tool-use rounds + if textContent.Len() > 0 { + textContent.WriteString("\n\n") + if handler != nil && messageStarted { + handler(message.ChunkText, []byte("\n\n")) + } + } + case "tool_use": toolName, _ := contentBlock["name"].(string) blockIndex := 0 if idx, ok := event["index"].(float64); ok { @@ -1073,6 +1088,36 @@ func (e *Executor) GetWorkDir() string { return e.workDir } +// GetSandboxID returns the sandbox ID (userID-chatID) +func (e *Executor) GetSandboxID() string { + if e.opts == nil { + return "" + } + return fmt.Sprintf("%s-%s", e.opts.UserID, e.opts.ChatID) +} + +// GetVNCUrl returns the VNC preview URL path +// Returns empty string if VNC is not enabled for this sandbox image +func (e *Executor) GetVNCUrl() string { + if e.opts == nil { + return "" + } + + // Check if the image supports VNC (playwright or desktop variants) + imageName := e.opts.Image + if imageName == "" { + return "" + } + + // VNC is only available for playwright and desktop images + if !strings.Contains(imageName, "playwright") && !strings.Contains(imageName, "desktop") { + return "" + } + + // Return only the sandbox ID, the full URL is constructed by openapi/sandbox.GetVNCClientURL() + return e.GetSandboxID() +} + // Close releases the executor resources and removes the container // Note: IPC session is managed by sandbox.Manager.Remove() func (e *Executor) Close() error { diff --git a/agent/sandbox/types.go b/agent/sandbox/types.go index ea17f710..209944a2 100644 --- a/agent/sandbox/types.go +++ b/agent/sandbox/types.go @@ -32,6 +32,13 @@ type Executor interface { // GetWorkDir returns the container workspace directory GetWorkDir() string + // GetSandboxID returns the sandbox ID (userID-chatID) + GetSandboxID() string + + // GetVNCUrl returns the VNC preview URL path (e.g., /api/__yao/vnc/{sandboxID}/) + // Returns empty string if VNC is not enabled for this sandbox image + GetVNCUrl() string + // Close releases container resources Close() error } diff --git a/data/bindata.go b/data/bindata.go index 4cb2fcd7..2f408c36 100644 --- a/data/bindata.go +++ b/data/bindata.go @@ -495,7 +495,7 @@ func cuiSetupIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/setup/index.html", size: 10, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "cui/setup/index.html", size: 10, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -515,7 +515,7 @@ func cuiV09IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v0.9/index.html", size: 13, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "cui/v0.9/index.html", size: 13, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -535,7 +535,7 @@ func cuiV10IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/index.html", size: 49, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "cui/v1.0/index.html", size: 49, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -555,7 +555,7 @@ func cuiV10Layouts__indexAsyncJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "cui/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -575,7 +575,7 @@ func cuiV10UmiJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "cui/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -595,7 +595,7 @@ func initEnv() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.env", size: 6599, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.env", size: 6599, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -615,7 +615,7 @@ func initVscodeSettingsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/settings.json", size: 4666, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/settings.json", size: 4666, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -635,7 +635,7 @@ func initVscodeTypesRuntimeConsoleDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/console.d.ts", size: 221, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/console.d.ts", size: 221, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -655,7 +655,7 @@ func initVscodeTypesRuntimeExceptionDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/exception.d.ts", size: 738, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/exception.d.ts", size: 738, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -675,7 +675,7 @@ func initVscodeTypesRuntimeFsDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/fs.d.ts", size: 8554, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/fs.d.ts", size: 8554, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -695,7 +695,7 @@ func initVscodeTypesRuntimeGlobalDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/global.d.ts", size: 1759, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/global.d.ts", size: 1759, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -715,7 +715,7 @@ func initVscodeTypesRuntimeHttpDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/http.d.ts", size: 6179, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/http.d.ts", size: 6179, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -735,7 +735,7 @@ func initVscodeTypesRuntimeIoDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/io.d.ts", size: 587, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/io.d.ts", size: 587, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -755,7 +755,7 @@ func initVscodeTypesRuntimeLogDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/log.d.ts", size: 1692, mode: os.FileMode(493), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/log.d.ts", size: 1692, mode: os.FileMode(493), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -775,7 +775,7 @@ func initVscodeTypesRuntimeNeoDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/neo.d.ts", size: 3750, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/neo.d.ts", size: 3750, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -795,7 +795,7 @@ func initVscodeTypesRuntimeProcessFsDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/fs.d.ts", size: 11133, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/fs.d.ts", size: 11133, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -815,7 +815,7 @@ func initVscodeTypesRuntimeProcessHttpDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/http.d.ts", size: 5653, mode: os.FileMode(493), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/http.d.ts", size: 5653, mode: os.FileMode(493), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -835,7 +835,7 @@ func initVscodeTypesRuntimeProcessModelDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/model.d.ts", size: 6656, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/model.d.ts", size: 6656, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -855,7 +855,7 @@ func initVscodeTypesRuntimeProcessDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process.d.ts", size: 23165, mode: os.FileMode(493), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process.d.ts", size: 23165, mode: os.FileMode(493), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -875,7 +875,7 @@ func initVscodeTypesRuntimeQueryDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/query.d.ts", size: 6124, mode: os.FileMode(493), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/query.d.ts", size: 6124, mode: os.FileMode(493), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -895,7 +895,7 @@ func initVscodeTypesRuntimeStoreDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/store.d.ts", size: 2251, mode: os.FileMode(493), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/store.d.ts", size: 2251, mode: os.FileMode(493), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -915,7 +915,7 @@ func initVscodeTypesRuntimeSuiDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/sui.d.ts", size: 1713, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/sui.d.ts", size: 1713, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -935,7 +935,7 @@ func initVscodeTypesRuntimeTimeDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/time.d.ts", size: 711, mode: os.FileMode(493), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/time.d.ts", size: 711, mode: os.FileMode(493), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -955,7 +955,7 @@ func initVscodeTypesRuntimeDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime.d.ts", size: 424, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime.d.ts", size: 424, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -975,7 +975,7 @@ func initVscodeTypesSuiDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/sui.d.ts", size: 8931, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/.vscode/types/sui.d.ts", size: 8931, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -995,7 +995,7 @@ func initAgentAgentYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/agent.yml", size: 1583, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/agent.yml", size: 1583, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1015,7 +1015,7 @@ func initAgentLocalesEnUsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/locales/en-us.yml", size: 151, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/locales/en-us.yml", size: 151, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1035,7 +1035,7 @@ func initAgentLocalesZhCnYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/locales/zh-cn.yml", size: 135, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/locales/zh-cn.yml", size: 135, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1055,7 +1055,7 @@ func initAgentPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/prompts.yml", size: 713, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/prompts.yml", size: 713, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1075,7 +1075,7 @@ func initAgentSearchYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/search.yml", size: 330, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/search.yml", size: 330, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1095,7 +1095,7 @@ func initAgentTemplate__assetsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1115,7 +1115,7 @@ func initAgentTemplate__assetsBrandsAppleSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/brands/apple.svg", size: 650, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/brands/apple.svg", size: 650, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1135,7 +1135,7 @@ func initAgentTemplate__assetsBrandsGithubSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/brands/github.svg", size: 822, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/brands/github.svg", size: 822, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1155,7 +1155,7 @@ func initAgentTemplate__assetsBrandsGoogleSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/brands/google.svg", size: 457, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/brands/google.svg", size: 457, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1175,7 +1175,7 @@ func initAgentTemplate__assetsBrandsMicrosoftSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/brands/microsoft.svg", size: 206, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/brands/microsoft.svg", size: 206, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1195,7 +1195,7 @@ func initAgentTemplate__assetsCssVarsCss() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/css/vars.css", size: 7296, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/css/vars.css", size: 7296, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1215,7 +1215,7 @@ func initAgentTemplate__assetsImagesAssistantsExpensePng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/images/assistants/expense.png", size: 1434910, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/images/assistants/expense.png", size: 1434910, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1235,7 +1235,7 @@ func initAgentTemplate__assetsImagesAssistantsTasksSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/images/assistants/tasks.svg", size: 1686, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/images/assistants/tasks.svg", size: 1686, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1255,7 +1255,7 @@ func initAgentTemplate__assetsImagesIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/images/icons/app.png", size: 18302, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/images/icons/app.png", size: 18302, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1275,7 +1275,7 @@ func initAgentTemplate__assetsImagesLogosLogo_colorSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/images/logos/logo_color.svg", size: 2608, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/images/logos/logo_color.svg", size: 2608, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1295,7 +1295,7 @@ func initAgentTemplate__assetsImagesLogosWordmarkSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/images/logos/wordmark.svg", size: 8648, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/images/logos/wordmark.svg", size: 8648, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1315,7 +1315,7 @@ func initAgentTemplate__assetsJsEcharts543MinJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/js/echarts-5.4.3.min.js", size: 1024740, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/js/echarts-5.4.3.min.js", size: 1024740, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1335,7 +1335,7 @@ func initAgentTemplate__assetsJsHighlightMinJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/js/highlight.min.js", size: 65157, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/js/highlight.min.js", size: 65157, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1355,7 +1355,7 @@ func initAgentTemplate__assetsJsRemarkableMinJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/js/remarkable.min.js", size: 122397, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/js/remarkable.min.js", size: 122397, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1375,7 +1375,7 @@ func initAgentTemplate__assetsJsYaoAgentDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/js/yao-agent.d.ts", size: 2082, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/js/yao-agent.d.ts", size: 2082, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1395,7 +1395,7 @@ func initAgentTemplate__assetsJsYaoAgentJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__assets/js/yao-agent.js", size: 15828, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__assets/js/yao-agent.js", size: 15828, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1415,7 +1415,7 @@ func initAgentTemplate__dataJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__data.json", size: 538, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__data.json", size: 538, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1435,7 +1435,7 @@ func initAgentTemplate__documentHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/__document.html", size: 4391, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/__document.html", size: 4391, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1455,7 +1455,7 @@ func initAgentTemplatePackageJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/package.json", size: 356, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/package.json", size: 356, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1475,7 +1475,7 @@ func initAgentTemplatePages401401Css() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/pages/401/401.css", size: 700, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/pages/401/401.css", size: 700, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1495,7 +1495,7 @@ func initAgentTemplatePages401401Html() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/pages/401/401.html", size: 531, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/pages/401/401.html", size: 531, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1515,7 +1515,7 @@ func initAgentTemplatePages401401Json() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/pages/401/401.json", size: 54, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/pages/401/401.json", size: 54, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1535,7 +1535,7 @@ func initAgentTemplatePages401401Ts() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/pages/401/401.ts", size: 214, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/pages/401/401.ts", size: 214, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1555,7 +1555,7 @@ func initAgentTemplatePages401__localesEnUsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/pages/401/__locales/en-us.yml", size: 145, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/pages/401/__locales/en-us.yml", size: 145, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1575,7 +1575,7 @@ func initAgentTemplatePages401__localesZhCnYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/pages/401/__locales/zh-cn.yml", size: 139, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/pages/401/__locales/zh-cn.yml", size: 139, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1595,7 +1595,7 @@ func initAgentTemplatePages404404Css() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/pages/404/404.css", size: 1877, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/pages/404/404.css", size: 1877, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1615,7 +1615,7 @@ func initAgentTemplatePages404404Html() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/pages/404/404.html", size: 896, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/pages/404/404.html", size: 896, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1635,7 +1635,7 @@ func initAgentTemplatePages404404Json() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/pages/404/404.json", size: 32, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/pages/404/404.json", size: 32, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1655,7 +1655,7 @@ func initAgentTemplatePages404404Ts() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/pages/404/404.ts", size: 450, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/pages/404/404.ts", size: 450, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1675,7 +1675,7 @@ func initAgentTemplatePages404__localesEnUsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/pages/404/__locales/en-us.yml", size: 243, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/pages/404/__locales/en-us.yml", size: 243, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1695,7 +1695,7 @@ func initAgentTemplatePages404__localesZhCnYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/agent/template/pages/404/__locales/zh-cn.yml", size: 232, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/agent/template/pages/404/__locales/zh-cn.yml", size: 232, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1715,7 +1715,7 @@ func initAppYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/app.yao", size: 1642, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/app.yao", size: 1642, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1735,7 +1735,7 @@ func initAssistantsLlmsLocalesEnUsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/llms/locales/en-us.yml", size: 235, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/llms/locales/en-us.yml", size: 235, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1755,7 +1755,7 @@ func initAssistantsLlmsLocalesZhCnYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/llms/locales/zh-cn.yml", size: 231, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/llms/locales/zh-cn.yml", size: 231, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1775,7 +1775,7 @@ func initAssistantsLlmsPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/llms/package.yao", size: 546, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/llms/package.yao", size: 546, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1795,7 +1795,7 @@ func initAssistantsLlmsPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/llms/prompts.yml", size: 127, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/llms/prompts.yml", size: 127, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1815,7 +1815,7 @@ func initAssistantsMessagesLocalesEnUsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/messages/locales/en-us.yml", size: 383, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/messages/locales/en-us.yml", size: 383, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1835,7 +1835,7 @@ func initAssistantsMessagesLocalesZhCnYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/messages/locales/zh-cn.yml", size: 371, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/messages/locales/zh-cn.yml", size: 371, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1855,7 +1855,7 @@ func initAssistantsMessagesPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/messages/package.yao", size: 562, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/messages/package.yao", size: 562, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1875,7 +1875,7 @@ func initAssistantsMessagesSrcActionTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/messages/src/action.ts", size: 4752, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/messages/src/action.ts", size: 4752, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1895,7 +1895,7 @@ func initAssistantsMessagesSrcBasicTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/messages/src/basic.ts", size: 3310, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/messages/src/basic.ts", size: 3310, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1915,7 +1915,7 @@ func initAssistantsMessagesSrcCodeTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/messages/src/code.ts", size: 2463, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/messages/src/code.ts", size: 2463, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1935,7 +1935,7 @@ func initAssistantsMessagesSrcErrorTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/messages/src/error.ts", size: 6281, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/messages/src/error.ts", size: 6281, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1955,7 +1955,7 @@ func initAssistantsMessagesSrcIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/messages/src/index.ts", size: 3326, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/messages/src/index.ts", size: 3326, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1975,7 +1975,7 @@ func initAssistantsMessagesSrcMarkdownTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/messages/src/markdown.ts", size: 7045, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/messages/src/markdown.ts", size: 7045, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1995,7 +1995,7 @@ func initAssistantsYaoLocalesEnUsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/yao/locales/en-us.yml", size: 353, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/yao/locales/en-us.yml", size: 353, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2015,7 +2015,7 @@ func initAssistantsYaoLocalesZhCnYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/yao/locales/zh-cn.yml", size: 363, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/yao/locales/zh-cn.yml", size: 363, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2035,7 +2035,7 @@ func initAssistantsYaoPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/yao/package.yao", size: 582, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/yao/package.yao", size: 582, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2055,7 +2055,7 @@ func initAssistantsYaoPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/assistants/yao/prompts.yml", size: 648, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/assistants/yao/prompts.yml", size: 648, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2075,7 +2075,7 @@ func initConnectorsAnthropicClaudeOpus4_5ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/anthropic/claude-opus-4_5.conn.yao", size: 385, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/anthropic/claude-opus-4_5.conn.yao", size: 385, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2095,7 +2095,7 @@ func initConnectorsAnthropicClaudeSonnet4_5ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/anthropic/claude-sonnet-4_5.conn.yao", size: 389, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/anthropic/claude-sonnet-4_5.conn.yao", size: 389, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2115,7 +2115,7 @@ func initConnectorsAzureGpt5_2ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/azure/gpt-5_2.conn.yao", size: 377, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/azure/gpt-5_2.conn.yao", size: 377, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2135,7 +2135,7 @@ func initConnectorsDeepseekDeepseekChatConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/deepseek/deepseek-chat.conn.yao", size: 377, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/deepseek/deepseek-chat.conn.yao", size: 377, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2155,7 +2155,7 @@ func initConnectorsDeepseekDeepseekReasonerConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/deepseek/deepseek-reasoner.conn.yao", size: 385, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/deepseek/deepseek-reasoner.conn.yao", size: 385, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2175,7 +2175,7 @@ func initConnectorsFireworksLlama4MaverickConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/fireworks/llama-4-maverick.conn.yao", size: 449, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/fireworks/llama-4-maverick.conn.yao", size: 449, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2195,7 +2195,7 @@ func initConnectorsGoogleGemini2_5ProConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/google/gemini-2_5-pro.conn.yao", size: 406, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/google/gemini-2_5-pro.conn.yao", size: 406, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2215,7 +2215,7 @@ func initConnectorsGoogleGemini3FlashConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/google/gemini-3-flash.conn.yao", size: 416, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/google/gemini-3-flash.conn.yao", size: 416, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2235,7 +2235,7 @@ func initConnectorsGroqLlama4MaverickConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/groq/llama-4-maverick.conn.yao", size: 420, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/groq/llama-4-maverick.conn.yao", size: 420, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2255,7 +2255,7 @@ func initConnectorsMetaLlama4MaverickConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/meta/llama-4-maverick.conn.yao", size: 400, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/meta/llama-4-maverick.conn.yao", size: 400, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2275,7 +2275,7 @@ func initConnectorsMistralMistralLarge3ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/mistral/mistral-large-3.conn.yao", size: 381, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/mistral/mistral-large-3.conn.yao", size: 381, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2295,7 +2295,7 @@ func initConnectorsOllamaDeepseekR1ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/ollama/deepseek-r1.conn.yao", size: 356, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/ollama/deepseek-r1.conn.yao", size: 356, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2315,7 +2315,7 @@ func initConnectorsOllamaGemma3ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/ollama/gemma3.conn.yao", size: 350, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/ollama/gemma3.conn.yao", size: 350, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2335,7 +2335,7 @@ func initConnectorsOllamaLlama3_3ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/ollama/llama3_3.conn.yao", size: 355, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/ollama/llama3_3.conn.yao", size: 355, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2355,7 +2355,7 @@ func initConnectorsOllamaQwen2_5CoderConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/ollama/qwen2_5-coder.conn.yao", size: 361, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/ollama/qwen2_5-coder.conn.yao", size: 361, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2375,7 +2375,7 @@ func initConnectorsOllamaQwen3ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/ollama/qwen3.conn.yao", size: 345, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/ollama/qwen3.conn.yao", size: 345, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2395,7 +2395,7 @@ func initConnectorsOpenaiGpt4oMiniConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/openai/gpt-4o-mini.conn.yao", size: 371, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/openai/gpt-4o-mini.conn.yao", size: 371, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2415,7 +2415,7 @@ func initConnectorsOpenaiGpt4oConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/openai/gpt-4o.conn.yao", size: 360, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/openai/gpt-4o.conn.yao", size: 360, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2435,7 +2435,7 @@ func initConnectorsOpenaiGpt5_2ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/openai/gpt-5_2.conn.yao", size: 362, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/openai/gpt-5_2.conn.yao", size: 362, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2455,7 +2455,7 @@ func initConnectorsOpenaiO3ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/openai/o3.conn.yao", size: 354, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/openai/o3.conn.yao", size: 354, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2475,7 +2475,7 @@ func initConnectorsOpenaiTextEmbedding3LargeConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/openai/text-embedding-3-large.conn.yao", size: 394, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/openai/text-embedding-3-large.conn.yao", size: 394, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2495,7 +2495,7 @@ func initConnectorsOpenrouterAutoConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/openrouter/auto.conn.yao", size: 395, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/openrouter/auto.conn.yao", size: 395, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2515,7 +2515,7 @@ func initConnectorsOpenrouterClaudeOpus4_5ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/openrouter/claude-opus-4_5.conn.yao", size: 409, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/openrouter/claude-opus-4_5.conn.yao", size: 409, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2535,7 +2535,7 @@ func initConnectorsOpenrouterNovaPremierConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/openrouter/nova-premier.conn.yao", size: 410, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/openrouter/nova-premier.conn.yao", size: 410, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2555,7 +2555,7 @@ func initConnectorsSiliconflowDeepseekV3ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/siliconflow/deepseek-v3.conn.yao", size: 405, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/siliconflow/deepseek-v3.conn.yao", size: 405, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2575,7 +2575,7 @@ func initConnectorsSiliconflowQwen2_572bConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/siliconflow/qwen-2_5-72b.conn.yao", size: 408, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/siliconflow/qwen-2_5-72b.conn.yao", size: 408, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2595,7 +2595,7 @@ func initConnectorsTogetherDeepseekR1ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/together/deepseek-r1.conn.yao", size: 396, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/together/deepseek-r1.conn.yao", size: 396, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2615,7 +2615,7 @@ func initConnectorsTogetherLlama4MaverickConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/together/llama-4-maverick.conn.yao", size: 429, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/together/llama-4-maverick.conn.yao", size: 429, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2635,7 +2635,7 @@ func initConnectorsVolcengineDeepseekR1ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/volcengine/deepseek-r1.conn.yao", size: 408, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/volcengine/deepseek-r1.conn.yao", size: 408, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2655,7 +2655,7 @@ func initConnectorsVolcengineDeepseekV3ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/volcengine/deepseek-v3.conn.yao", size: 408, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/volcengine/deepseek-v3.conn.yao", size: 408, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2675,7 +2675,7 @@ func initConnectorsVolcengineDoubao1_5ProConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/volcengine/doubao-1_5-pro.conn.yao", size: 412, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/volcengine/doubao-1_5-pro.conn.yao", size: 412, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2695,7 +2695,7 @@ func initConnectorsVolcengineGlm4PlusConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/volcengine/glm-4-plus.conn.yao", size: 399, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/volcengine/glm-4-plus.conn.yao", size: 399, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2715,7 +2715,7 @@ func initConnectorsVolcengineQwenVlMaxConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/volcengine/qwen-vl-max.conn.yao", size: 403, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/volcengine/qwen-vl-max.conn.yao", size: 403, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2735,7 +2735,7 @@ func initConnectorsXaiGrok4ConnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/connectors/xai/grok-4.conn.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/connectors/xai/grok-4.conn.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2755,7 +2755,7 @@ func initDataReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/README.md", size: 41, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/README.md", size: 41, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2775,7 +2775,7 @@ func initDataTemplatesDefault__assetsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2795,7 +2795,7 @@ func initDataTemplatesDefault__assetsBrandsAppleSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/brands/apple.svg", size: 650, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/brands/apple.svg", size: 650, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2815,7 +2815,7 @@ func initDataTemplatesDefault__assetsBrandsGithubSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/brands/github.svg", size: 822, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/brands/github.svg", size: 822, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2835,7 +2835,7 @@ func initDataTemplatesDefault__assetsBrandsGoogleSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/brands/google.svg", size: 457, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/brands/google.svg", size: 457, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2855,7 +2855,7 @@ func initDataTemplatesDefault__assetsBrandsMicrosoftSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/brands/microsoft.svg", size: 206, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/brands/microsoft.svg", size: 206, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2875,7 +2875,7 @@ func initDataTemplatesDefault__assetsImagesIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2895,7 +2895,7 @@ func initDataTemplatesDefault__assetsImagesLogosLogo_colorSvg() (*asset, error) return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2915,7 +2915,7 @@ func initDataTemplatesDefault__assetsImagesLogosWordmarkSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2935,7 +2935,7 @@ func initDataTemplatesDefault__dataJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__data.json", size: 30, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__data.json", size: 30, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2955,7 +2955,7 @@ func initDataTemplatesDefault__documentHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__document.html", size: 492, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__document.html", size: 492, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2975,7 +2975,7 @@ func initDataTemplatesDefaultIndexIndexCss() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.css", size: 2896, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.css", size: 2896, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2995,7 +2995,7 @@ func initDataTemplatesDefaultIndexIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.html", size: 2361, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.html", size: 2361, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3015,7 +3015,7 @@ func initDataTemplatesDefaultIndexIndexJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.json", size: 31, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.json", size: 31, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3035,7 +3035,7 @@ func initDbReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/db/README.md", size: 84, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/db/README.md", size: 84, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3055,7 +3055,7 @@ func initIconsAppIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/icons/app.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3075,7 +3075,7 @@ func initIconsAppIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/icons/app.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3095,7 +3095,7 @@ func initIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3115,7 +3115,7 @@ func initLogsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/logs/README.md", size: 28, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/logs/README.md", size: 28, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3135,7 +3135,7 @@ func initMessengersChannelsYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/messengers/channels.yao", size: 474, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/messengers/channels.yao", size: 474, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3155,7 +3155,7 @@ func initMessengersProvidersPrimaryMailgunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/messengers/providers/primary.mailgun.yao", size: 356, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/messengers/providers/primary.mailgun.yao", size: 356, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3175,7 +3175,7 @@ func initMessengersProvidersUnifiedTwilioYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/messengers/providers/unified.twilio.yao", size: 508, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/messengers/providers/unified.twilio.yao", size: 508, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3195,7 +3195,7 @@ func initMessengersTemplatesEnInvite_memberMailHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/messengers/templates/en/invite_member.mail.html", size: 483, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/messengers/templates/en/invite_member.mail.html", size: 483, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3215,7 +3215,7 @@ func initMessengersTemplatesEnInvite_memberSmsTxt() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/messengers/templates/en/invite_member.sms.txt", size: 114, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/messengers/templates/en/invite_member.sms.txt", size: 114, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3235,7 +3235,7 @@ func initMessengersTemplatesEnVerify_emailMailHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/messengers/templates/en/verify_email.mail.html", size: 397, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/messengers/templates/en/verify_email.mail.html", size: 397, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3255,7 +3255,7 @@ func initMessengersTemplatesEnVerify_mobileSmsTxt() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/messengers/templates/en/verify_mobile.sms.txt", size: 124, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/messengers/templates/en/verify_mobile.sms.txt", size: 124, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3275,7 +3275,7 @@ func initMessengersTemplatesZhCnInvite_mailHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/messengers/templates/zh-cn/invite_mail.html", size: 373, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/messengers/templates/zh-cn/invite_mail.html", size: 373, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3295,7 +3295,7 @@ func initMessengersTemplatesZhCnInvite_memberMailHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/messengers/templates/zh-cn/invite_member.mail.html", size: 441, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/messengers/templates/zh-cn/invite_member.mail.html", size: 441, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3315,7 +3315,7 @@ func initMessengersTemplatesZhCnInvite_smsTxt() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/messengers/templates/zh-cn/invite_sms.txt", size: 122, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/messengers/templates/zh-cn/invite_sms.txt", size: 122, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3335,7 +3335,7 @@ func initMessengersTemplatesZhCnVerify_emailMailHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/messengers/templates/zh-cn/verify_email.mail.html", size: 347, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/messengers/templates/zh-cn/verify_email.mail.html", size: 347, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3355,7 +3355,7 @@ func initMessengersTemplatesZhCnVerify_mobileSmsTxt() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/messengers/templates/zh-cn/verify_mobile.sms.txt", size: 114, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/messengers/templates/zh-cn/verify_mobile.sms.txt", size: 114, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3375,7 +3375,7 @@ func initModelsMenuModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/models/menu.mod.yao", size: 3246, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/models/menu.mod.yao", size: 3246, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3395,7 +3395,7 @@ func initOpenapiCertsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/certs/README.md", size: 10174, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/certs/README.md", size: 10174, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3415,7 +3415,7 @@ func initOpenapiCertsMtlsClientCaKeyTestingOnlyDoNotUseInProductionPem() (*asset return nil, err } - info := bindataFileInfo{name: "init/openapi/certs/mtls-client-ca-key-TESTING-ONLY-DO-NOT-USE-IN-PRODUCTION.pem", size: 3268, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/certs/mtls-client-ca-key-TESTING-ONLY-DO-NOT-USE-IN-PRODUCTION.pem", size: 3268, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3435,7 +3435,7 @@ func initOpenapiCertsMtlsClientCaPem() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/certs/mtls-client-ca.pem", size: 2029, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/certs/mtls-client-ca.pem", size: 2029, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3455,7 +3455,7 @@ func initOpenapiCertsSigningCertPem() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/certs/signing-cert.pem", size: 2090, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/certs/signing-cert.pem", size: 2090, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3475,7 +3475,7 @@ func initOpenapiCertsSigningKeyPem() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/certs/signing-key.pem", size: 3272, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/certs/signing-key.pem", size: 3272, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3495,7 +3495,7 @@ func initOpenapiFeaturesAliasYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/features/alias.yml", size: 800, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/features/alias.yml", size: 800, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3515,7 +3515,7 @@ func initOpenapiFeaturesFeaturesYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/features/features.yml", size: 1202, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/features/features.yml", size: 1202, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3535,7 +3535,7 @@ func initOpenapiFeaturesUserProfileYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/features/user/profile.yml", size: 96, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/features/user/profile.yml", size: 96, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3555,12 +3555,12 @@ func initOpenapiFeaturesUserTeamYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/features/user/team.yml", size: 286, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/features/user/team.yml", size: 286, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _initOpenapiOpenapiYao = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x59\x5b\x6f\x1b\xbb\x11\x7e\xcf\xaf\x18\xe8\x29\x06\x24\xeb\x92\xd8\x27\x36\xd0\x07\x1d\x3b\xa7\x75\x73\x52\x0b\xb6\xd3\xe2\x20\x08\x16\x14\x77\xd6\x4b\x7b\x97\xdc\x43\x72\x25\xab\x41\xfe\x7b\x41\x72\x2f\xdc\x9b\xbc\x05\x9a\x3e\xc5\xe1\xce\x7c\x1f\x39\x33\x9c\x0b\xf5\xfd\x0d\xc0\x64\x4b\x14\xe6\x32\x99\x5c\xc2\x64\xbe\x5b\x4e\xa6\x30\x9f\xc3\xaf\x44\x21\x7c\xb9\xfb\x1d\x32\xa2\x63\x88\x84\x84\xdb\x0c\xf9\x7a\x73\x03\xc8\xc3\x4c\x30\xae\x95\x51\x55\x5a\x48\x34\x8a\x41\x70\x20\xe2\x54\x90\x5c\xc7\xa7\x6e\xd1\xc2\x5c\x63\x44\xf2\x44\x83\x5d\x72\x30\xeb\x5c\xc7\x10\x12\x4d\x8c\x3e\x25\x34\xee\xe8\xbb\xc5\x86\xbe\x5d\xea\xd1\xcf\xa4\xd8\xb1\x10\xa5\x9a\x5c\x82\x39\x0b\xc0\x24\x57\x28\x6b\x44\xfb\x3f\x0b\xf5\x45\xa1\x84\x52\xfe\x12\xd6\xfc\x00\x7f\x10\x01\xa9\x08\x31\x81\x3d\xd3\x31\x48\xfc\x33\x67\x12\x43\x88\x18\x26\xa1\x82\xb7\xa1\x63\xbf\x84\x1a\xeb\xc4\x91\xd0\x84\x21\xd7\x9d\x8d\xbb\x55\x4b\x77\x65\xff\x76\x07\xaf\xd9\xee\x5b\x76\x70\x1a\xf0\xe9\x9f\xf6\x44\x1d\x46\x1f\x16\x72\x85\x0a\x7e\x25\xe1\xa3\xdb\xc5\x8f\xa9\x31\x80\x95\xa8\x0f\xcf\x94\xca\x51\x06\x85\x37\x63\xad\x33\x75\x39\x9f\x27\x82\x92\x24\x16\x4a\x5f\x9e\x2d\x2e\x2e\xdc\xfe\xee\x8a\xc3\x5e\x16\x3b\xd1\xe2\x19\x39\x38\x7d\xeb\xf8\x7a\x93\x0a\xe5\x0e\xa5\x23\x50\xec\x91\x33\xfe\x58\x31\xd6\x4b\x01\x45\xa9\x03\x13\x2e\x86\xba\x58\x9c\x99\xc5\xd3\x0c\xd3\x36\xe9\x86\x58\x83\x27\x44\xb3\x1d\x82\x16\xf0\xfd\x8f\xf5\x6d\x70\x77\x7b\xfb\xf0\x63\x2e\x32\xe4\x24\x63\x73\xa3\xab\xe6\x10\x32\x89\x54\x0b\x79\x68\x13\x3e\xe3\xa1\xc3\xf7\x8c\x87\x9f\x4b\xa7\xd4\x5e\xc8\xd0\x50\x3a\x8e\xdb\x4c\x33\xc1\x49\x62\x38\xdc\x37\x6b\x39\xe4\x54\x1e\x32\x8d\x21\x64\x92\xed\x88\x46\x78\xc6\x83\xe7\x5f\x4c\x33\x7d\x38\x69\x53\x90\xe4\x51\x48\xa6\xe3\xd4\xe0\xdf\xdd\xaf\xce\xce\xdb\x24\x0f\xd6\x4d\x85\x3c\x54\xf2\x1e\xb2\x55\xab\x90\x77\x28\x59\xc4\x28\x31\xfa\xd6\x43\xe6\xa6\x7c\xfd\xd6\x42\x5d\x87\x21\x73\x7f\x83\x91\x71\x1a\xa8\xec\x49\x5c\x60\xf8\x38\x83\xc7\x48\x75\xa2\x02\x17\xad\x01\x25\xcd\x80\x30\xdf\x66\xee\xdb\x8c\x92\xda\x47\xf5\x26\xae\xd6\x3e\xb9\x4b\x3c\xa3\x5c\xd6\xa0\x47\x4e\xb6\x09\x1a\x07\x69\x99\x63\x8b\xe2\xa3\xfd\x08\x69\xae\x73\x92\xc0\xc3\xef\xf7\x60\xae\x0f\x72\xdd\x3d\x59\x44\x12\x85\xd5\xc9\xec\x51\xa4\xd0\xce\x8e\x35\x87\x95\xea\x27\x21\xb9\x16\x29\xd1\x8c\x36\x4e\x55\x62\x8c\x64\x62\x5c\xa3\xdc\x11\x7b\x9b\x57\xef\xe3\x8e\xc9\xfa\x90\x4b\x1d\x8f\x62\xf5\x3e\x76\x04\x36\x69\x00\x4c\xac\x53\xfd\x4b\x4c\x28\x45\xa5\x02\xbb\x1e\x24\x2c\x42\xcd\x52\x9b\x98\x97\x1d\xd2\xb5\x15\x2d\xe3\x82\x24\x2c\x64\xfa\x00\x19\x4a\x26\x42\x8f\x73\x19\x9f\xf4\x82\x47\x42\xa6\xc4\xa6\xce\xa7\xbd\x3e\x8a\xed\x24\x61\x06\x4f\x7b\x3d\x17\x19\xf9\x33\x47\x0f\xff\x69\xaf\xfb\x09\xbc\xcb\x34\x78\x8d\x1a\x3c\xe3\x6f\x93\xc4\x48\xa2\x8a\x7b\xcc\xd4\xe3\x9c\x3b\x27\xfc\xaa\xa1\x2a\xe7\x74\x08\x4a\x97\x1e\x0d\x66\xd9\xa0\xa9\xa2\xa0\x4e\xdf\xab\xd3\xe5\x60\xb4\x35\xf9\x6a\xd7\x38\x6b\x1f\x3f\x51\xe5\x1e\x27\x3c\x7f\xda\x6b\x8f\xc7\x2d\xd6\x1e\xca\x75\x2c\x24\xfb\x77\x91\x88\x44\x88\xcd\x28\x5b\x74\xd2\xc1\xda\xd7\x00\xa3\x71\x2c\xd8\x16\xe9\x51\x2a\xe4\x8f\x36\x0d\xbd\x5b\xbd\xce\xe2\x84\x81\x71\xd8\x1e\x4c\x12\xac\x59\xde\xad\x2a\x92\x10\x77\x8c\x62\xcf\x41\xce\x3a\x07\xb9\xb6\xa2\xaf\x9e\xe0\x2c\xed\x07\x2f\xb7\xfe\xe1\x08\xec\xf0\x96\x3f\x54\xa0\xa6\x7b\x79\x05\xd2\xb6\x48\x3e\xa0\x09\x22\xb7\x19\x88\x12\xb1\xef\xc5\xf5\x37\xeb\xa7\xab\x33\x75\xcc\x10\x99\x48\x12\x73\xe7\x7a\x92\xd5\x99\xaa\xb0\x5d\x58\x6e\x19\x0f\xcd\x85\x1e\x95\x76\x5d\x6c\x16\x2a\xa6\x6c\x14\xdd\x53\xa3\xae\x0d\x5d\x07\x95\x67\x99\x90\x1a\xc3\x8a\x53\x1f\x32\xb4\x25\x73\x12\x66\x22\x9b\x4c\x5d\x95\x99\xb4\x0b\xe8\x7d\xa9\xd8\xe6\x37\xea\x1e\xdd\x57\x83\x32\x05\x83\xf1\xcd\xb3\xa0\xfd\x1a\x90\x3c\x64\xc8\x29\xf6\x55\xe8\xb2\x11\x76\xf0\xa5\xe4\x60\x25\x2e\x05\x02\x1b\x70\xee\x26\x98\x4e\xd7\xb6\x4a\x5a\x32\xda\xcd\xbc\x25\x64\xad\x61\x7b\x63\x98\x81\x53\x98\x9b\x5a\xfc\x82\x7e\xcc\xba\x0f\xcd\xca\xa2\x90\xe6\x92\xe9\x83\x5f\x5c\xb2\x67\x8a\x41\xd9\x5d\xf7\x27\xb3\xa2\x55\x83\xcd\xa7\xab\x8f\xad\xdc\x45\x45\x9a\x25\x8c\x34\x8f\x6b\x20\x4e\x1a\xf8\x36\x00\x69\x4c\x12\x13\xbb\x18\xa4\xa8\x63\x11\x5a\xcf\xd9\x02\x30\xec\x31\x4b\x69\x83\xb2\xd2\x06\xa7\xdd\xf0\x9c\x41\xf9\xd6\x43\xe9\x3a\x23\x94\xf5\xb5\x5a\xae\xda\x17\xab\xa6\x28\xa5\xcb\x1b\xe6\x25\x80\x55\x7d\xa7\x94\x26\x1a\x83\x8c\x48\x92\xa2\x46\xe9\xdb\xae\x2f\xf4\x4b\xe3\x59\x35\xa8\xd4\xac\x1d\xaf\xee\xef\x7e\x33\x63\x8f\x46\x7a\xb4\xef\x68\x53\xbe\x92\x9d\xef\x5b\x54\xe3\x52\x73\x87\x64\x30\x2f\xb7\xf1\x47\x25\x65\x69\xd0\x13\x96\x32\x3d\x2e\x57\x18\x79\xb0\xf2\xe6\xae\x0e\xd6\xc8\x1a\xd5\xf8\x01\x95\xed\xa1\x97\x8b\x45\x0b\xf4\x1f\x79\xba\x45\x09\x22\x82\x52\xcc\x18\x03\xf6\x8c\x87\x8d\xd4\xb9\x5c\x2c\xfa\xb0\x9d\x9c\x35\x77\xc7\xda\x77\xd5\x46\xc1\xf8\xa4\x07\x33\xed\x83\xdc\x1e\xca\x6e\x9c\xbd\x62\x89\x0c\x65\xd1\x9c\x8f\x34\xca\x56\xe6\x1a\x4d\xc3\x40\x31\xa8\xa3\x6b\x9c\xd9\xad\x2e\x58\x5d\x20\x5a\x13\xfa\x3c\x26\x40\x53\xf2\x12\x44\x84\x25\x18\x06\x44\x6b\x93\xee\x8c\x1b\xce\x5a\x14\x9f\xc9\x0b\x4b\xf3\x14\x9c\x24\x24\xe2\x91\x71\x28\xe5\xfd\x22\x53\xe1\x26\x82\x3e\x8b\x5c\x07\x61\x2e\xcb\x66\xab\xaf\x8a\xaf\x29\x15\x39\xd7\x50\x88\x43\x29\x3e\x50\xc2\x8b\xf9\xcf\xd8\xe4\x19\x0f\x7d\xe3\xe2\x27\x3c\xf8\x93\xa2\x31\xb6\x42\xae\x98\x1d\x77\x5a\xaf\x01\xcd\xe4\xee\x61\x37\x06\xc6\xf5\xc7\xfb\xd9\xea\xec\x7c\xf6\xd7\xab\xcf\x6d\xb2\x8f\x95\x8a\xd7\xe5\x1a\xf6\x41\x4a\x0f\xac\x22\x66\x59\xb0\x8f\x99\xc6\x84\x29\xdd\x57\xa4\x6e\x36\x40\xc2\x50\xa2\x52\xa8\x80\x24\x89\xd8\xdb\x82\x08\xae\x3d\x1f\x3c\x0f\xcb\x82\x6d\x42\xe8\xf3\x28\xd8\xad\x71\x00\x86\x10\x49\x91\xbe\x06\x5c\xa4\xcd\xc0\xbe\x83\x1c\xaf\x3b\x7f\x7b\x78\xd8\xdc\x5b\x8b\x90\x24\xa9\xdf\xb8\x86\x2a\x4e\xc8\x94\x09\xe5\x20\xe7\xb6\xde\x61\x50\x69\x0c\x84\xfe\xb5\x53\x00\x2e\xf8\xcc\x71\xf5\x71\x78\x11\x5f\xd6\xd3\xea\xa5\xe9\x7b\xbb\x5d\x28\xae\xb6\x69\x31\x8c\xf7\xa9\xe0\x11\x0b\xcd\x2c\x4b\x92\x6e\xe7\x55\x3c\xa3\xb9\x2b\x6e\x54\x60\x06\xbe\xc6\x3c\xcb\xb7\x09\xa3\xde\x5e\xfc\xaf\x9d\x56\xc5\x35\x66\xe5\x11\x02\xd3\x72\xd7\x15\xb7\xd8\x73\xa0\x90\x4a\xd4\xc1\x96\x28\x46\x5f\xd9\x51\x6b\x0c\x77\x50\xfe\x66\xba\x88\x9d\x3d\x3d\x4a\x52\x98\xc3\x35\x6c\xdd\x39\xc0\xb4\x6f\x8d\x89\xa7\xd3\x15\x54\xef\x95\x55\x77\x60\x51\xbb\x7d\x5c\x17\x7c\x0a\x0d\xe8\x6e\x7b\x27\x51\x65\x82\x2b\xf4\xb6\x68\x37\xf5\xfa\x1e\x4a\xcd\xee\x36\x0c\x40\x97\x49\x51\x51\x32\x88\x0c\x39\x0b\xcd\xc1\x33\x29\x22\x96\x58\x1b\x60\x4a\x58\x32\xc8\x5b\x3c\xfa\x59\x0c\x9f\xcb\x41\x4d\xa1\x00\x9a\x82\x85\xa9\xd9\xab\x52\x73\xa4\xac\x17\xaf\xa2\x37\xd7\xe3\x0a\x7a\xd3\xed\x15\xec\xf9\xfb\x7e\x58\x27\x77\x04\xfa\xfc\xfd\x10\xb4\xd7\xed\x2c\x3a\x83\x4b\x0b\xbe\x90\x85\x19\x2c\xe0\x2f\xc0\x71\x87\x12\xf0\x25\x63\xb2\xc1\xb5\xa8\xc7\x98\xf0\xc0\x49\xca\x68\x20\xf1\x91\x29\x2d\xdb\x8f\x48\xc3\xb3\x7d\xa1\x58\xde\x12\x5f\x7f\x28\x33\x15\x69\x37\x90\xe8\x1e\x32\x83\x5c\xb2\x40\xd1\x18\xd3\x22\x22\x5c\x32\x9c\xba\xd7\xe1\x4e\x10\xac\x8b\xac\x5d\xaa\xc3\x97\xbb\x1b\x28\xd4\xfd\x60\xb0\x28\x53\x30\xff\x7c\x3b\xce\x1d\x0b\xd7\x30\x7d\x9d\x54\xcf\xd0\x86\x7d\xb9\xfa\xe5\x74\x71\xba\x38\x5d\x8e\xdb\x82\x45\xf1\x37\x50\x81\x4d\xa1\x82\xea\xc4\xa2\x37\xf4\x8d\xed\xa1\xff\x9b\x71\xb1\x87\xa5\x9e\x9e\x4c\x28\x71\xc1\x3b\x8f\x28\x57\x1d\x82\x9e\x91\xcb\x28\xce\x45\xa1\x33\xaf\x7e\x98\xa8\x37\x62\x04\x9a\x85\x22\x42\xa2\x73\x89\xca\x2f\x15\xf6\xd7\x81\xd5\x72\x54\xac\xd5\x03\x57\x89\x74\x74\xdc\x42\x6e\x1b\xb8\x41\x4c\xd7\xde\x55\xe3\x9c\xa9\xaa\xce\x5c\x83\xb0\xfd\x6f\x5f\xa3\xf6\x3e\xf0\x06\x36\x54\xbc\xdd\x8b\x45\x94\x88\xfd\xb8\x5b\xe8\xde\x2d\x1a\x29\xbf\xfd\x1e\xd2\x20\x28\xaa\xe3\x0b\x8d\x89\x19\x45\xc7\x70\xb8\x9d\x97\x2a\x70\xf7\xdb\x15\x7c\x38\xbf\x78\x37\x18\x7a\x59\xae\x62\xd3\x09\x37\xca\xd0\x18\x22\xa7\xd8\x3a\x4c\x35\xb0\x18\xde\x8b\xe5\xea\x7c\x90\xb7\x4c\x65\x45\xe8\xff\x4f\x33\x9a\x21\xff\xe5\xec\x62\x39\x64\xd6\x94\x66\x41\xfd\x18\x30\x8a\xee\xf3\xd5\x06\xde\x7e\xb6\x3f\xf1\x5d\x09\xae\xf1\x45\xc3\x46\x0a\x2d\xa8\x48\x4e\xfa\x1f\x16\xda\xef\xa3\x4a\xe4\x76\xc6\xa9\x66\xd6\x71\xf1\xe8\xd4\xbc\x01\xb6\xa8\xe6\x83\x4c\xff\xef\xa7\xae\x9f\xf8\x63\x49\x98\x89\x6c\xdc\x09\xae\x37\x62\x03\x6f\xaf\x31\x15\xbc\x8a\x02\x11\x19\x17\x89\x68\x26\xa2\xd9\x46\x98\x9e\x5f\x31\xc1\x4f\x06\xd9\x9e\xf6\x3a\x60\x5c\x4b\xa1\xb2\xce\x10\x3a\x7c\xaa\xbf\xff\xeb\xa1\xfc\xc9\xd3\xd7\x3d\x7e\xa3\x25\xee\x04\x1d\x1f\xeb\x45\x36\xaa\x94\x5c\x80\x2f\x16\x17\x43\x2c\xb9\x42\xc9\x78\x24\x02\x73\xa6\xb1\xa7\x28\x9e\xe1\x4d\x92\xfd\xa2\x50\xde\xf0\x48\x54\x0d\xe3\xb1\xd9\xe2\xc7\xf4\xcd\x8f\x37\xff\x09\x00\x00\xff\xff\xeb\x2c\x90\xc1\x00\x20\x00\x00") +var _initOpenapiOpenapiYao = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x59\xdb\x6e\x1b\x39\xd2\xbe\xcf\x53\x14\x74\x15\x03\x3a\x27\xf6\xc4\x06\xfe\x0b\x8d\x9d\xf9\xc7\x9b\xc9\x46\xb0\x9c\x5d\x0c\x06\x41\x83\xea\xae\xb6\x68\xb1\xc9\x1e\x92\x2d\x59\x1b\xe4\xdd\x17\x24\xfb\xc0\x3e\xc9\xbd\xc0\xce\x5e\xc5\x61\x57\x7d\x5f\xb1\x58\xac\x03\xf5\xfd\x0d\xc0\x68\x4b\x14\x66\x92\x8d\x6e\x60\x34\x3b\x2c\x46\x63\x98\xcd\xe0\x67\xa2\x10\xbe\x3e\xfc\x06\x29\xd1\x3b\x88\x85\x84\x2f\x29\xf2\xd5\xfa\x1e\x90\x47\xa9\xa0\x5c\x2b\xa3\xaa\xb4\x90\x68\x14\x83\xe0\x44\xc4\x54\x90\x4c\xef\xa6\x6e\xd1\xc2\xdc\x61\x4c\x32\xa6\xc1\x2e\x39\x98\x55\xa6\x77\x10\x11\x4d\x8c\x7e\x48\xc2\x5d\x4b\xdf\x2d\xd6\xf4\xed\x52\x87\x7e\x2a\xc5\x81\x46\x28\xd5\xe8\x06\xcc\x5e\x00\x46\x99\x42\x59\x21\xda\xff\x59\xa8\xaf\x0a\x25\x14\xf2\x37\xb0\xe2\x27\xf8\x9d\x08\x48\x44\x84\x0c\x8e\x54\xef\x40\xe2\x9f\x19\x95\x18\x41\x4c\x91\x45\x0a\xde\x46\x8e\xfd\x06\x2a\xac\x0b\x47\x12\x32\x8a\x5c\xb7\x0c\x77\xab\x96\xee\xd6\xfe\xed\x36\x5e\xb1\x6d\x1a\x7e\x70\x1a\xf0\xe9\x1f\x76\x47\x2d\x46\x1f\x16\x32\x85\x0a\x7e\x26\xd1\x93\xb3\xe2\xc7\xd8\x38\xc0\x4a\x54\x9b\xa7\x4a\x65\x28\x83\xfc\x34\x77\x5a\xa7\xea\x66\x36\x63\x22\x24\x6c\x27\x94\xbe\xb9\x9c\x5f\x5f\x3b\xfb\x1e\xf2\xcd\xde\xe4\x96\x68\xb1\x47\x0e\x4e\xdf\x1e\x7c\x65\xa4\x42\x79\x40\xe9\x08\x14\x7d\xe2\x94\x3f\x95\x8c\xd5\x52\x10\xa2\xd4\x81\x09\x17\x43\x9d\x2f\x4e\xcc\xe2\x34\xc5\xa4\x49\xba\x26\xd6\xe1\x8c\x68\x7a\x40\xd0\x02\xbe\xff\xbe\xfa\x12\x3c\x7c\xf9\xf2\xf8\x63\x26\x52\xe4\x24\xa5\x33\xa3\xab\x66\x10\x51\x89\xa1\x16\xf2\xd4\x24\xdc\xe3\xa9\xc5\xb7\xc7\xd3\x5f\x4b\xa7\xd4\x51\xc8\xc8\x50\x3a\x8e\x2f\xa9\xa6\x82\x13\x66\x38\xdc\x37\xeb\x39\xe4\xa1\x3c\xa5\x1a\x23\x48\x25\x3d\x10\x8d\xb0\xc7\x93\x77\xbe\x98\xa4\xfa\x74\xd1\xa4\x20\xec\x49\x48\xaa\x77\x89\xc1\x7f\xd8\x2c\x2f\xaf\x9a\x24\x8f\xf6\x98\x72\x79\x28\xe5\x3d\x64\xab\x56\x22\x1f\x50\xd2\x98\x86\xc4\xe8\xdb\x13\x32\x37\xe5\x8f\x6f\x0d\xd4\x55\x14\x51\xf7\x37\x18\x19\xa7\x81\xca\xee\xc4\x05\x86\x8f\xd3\xbb\x8d\x44\x33\x15\xb8\x68\x0d\x42\x52\x0f\x08\xf3\x6d\xe2\xbe\x4d\x42\x52\x9d\x51\x65\xc4\xed\xca\x27\x77\x89\x67\xd0\x91\xd5\xe8\x91\x93\x2d\x43\x73\x40\x5a\x66\xd8\xa0\xf8\x68\x3f\x42\x92\xe9\x8c\x30\x78\xfc\x6d\x03\xe6\xfa\x20\xd7\xed\x9d\xc5\x84\x29\x2c\x77\x66\xb7\x22\x85\x76\x7e\xac\x38\xac\x54\x37\x09\xc9\xb4\x48\x88\xa6\x61\x6d\x57\x05\xc6\x40\x26\xca\x35\xca\x03\xb1\xb7\x79\xf9\x7e\xd7\x72\x59\x17\x72\xa1\xe3\x51\x2c\xdf\xef\x1c\x81\x4d\x1a\x00\x23\x7b\xa8\xfe\x25\x26\x61\x88\x4a\x05\x76\x3d\x60\x34\x46\x4d\x13\x9b\x98\x17\x2d\xd2\x95\x15\x2d\xe2\x82\x30\x1a\x51\x7d\x82\x14\x25\x15\x91\xc7\xb9\xd8\x5d\x74\x82\xc7\x42\x26\xc4\xa6\xce\xe7\xa3\x3e\x8b\xed\x24\x61\x02\xcf\x47\x3d\x13\x29\xf9\x33\x43\x0f\xff\xf9\xa8\xbb\x09\xbc\xcb\xd4\x7b\x8d\x6a\x3c\xc3\x6f\x93\xc4\x58\xa2\xda\x75\xb8\xa9\xe3\x70\x1e\x9c\xf0\xab\x8e\x2a\x0f\xa7\x45\x50\x1c\xe9\xd9\x60\x96\x35\x9a\x32\x0a\xaa\xf4\xbd\x9c\x2e\x7a\xa3\xad\xce\x57\x1d\x8d\xf3\xf6\xf9\x1d\x95\xc7\xe3\x84\x67\xcf\x47\xed\xf1\xb8\xc5\xea\x84\x32\xbd\x13\x92\xfe\x2b\x4f\x44\x22\xc2\x7a\x94\xcd\x5b\xe9\x60\xe5\x6b\x80\xd1\x38\x17\x6c\xf3\xe4\x2c\x15\xf2\x27\x9b\x86\xde\x2d\x5f\x67\x71\xc2\x40\x39\x6c\x4f\x26\x09\x56\x2c\xef\x96\x25\x49\x84\x07\x1a\x62\xc7\x46\x2e\x5b\x1b\xb9\xb3\xa2\xaf\xee\xe0\x32\xe9\x06\x2f\x4c\xff\x70\x06\xb6\xdf\xe4\x0f\x25\xa8\xe9\x5e\x5e\x81\xb4\x2d\x92\x0f\x68\x82\xc8\x19\x03\x31\x13\xc7\x4e\x5c\xdf\x58\x3f\x5d\x5d\xaa\x73\x8e\x48\x05\x63\xe6\xce\x75\x24\xab\x4b\x55\x62\xbb\xb0\xdc\x52\x1e\x99\x0b\x3d\x28\xed\xba\xd8\xcc\x55\x4c\xd9\xc8\xbb\xa7\x5a\x5d\xeb\xbb\x0e\x2a\x4b\x53\x21\x35\x46\x25\xa7\x3e\xa5\x68\x4b\xe6\x28\x4a\x45\x3a\x1a\xbb\x2a\x33\x6a\x16\xd0\x4d\xa1\xd8\xe4\x37\xea\x1e\xdd\x1f\x06\x65\x0c\x06\xe3\x9b\xe7\x41\xfb\x35\x20\x59\x44\x91\x87\xd8\x55\xa1\x8b\x46\xd8\xc1\x17\x92\xbd\x95\xb8\x10\x08\x6c\xc0\xb9\x9b\x60\x3a\x5d\xdb\x2a\x69\x49\xc3\x76\xe6\x2d\x20\x2b\x0d\xdb\x1b\xc3\x04\x9c\xc2\xcc\xd4\xe2\x17\xf4\x63\xd6\x7d\xa8\x57\x16\x85\x61\x26\xa9\x3e\xf9\xc5\x25\xdd\x87\x18\x14\xdd\x75\x77\x32\xcb\x5b\x35\x58\x7f\xba\xfd\xd8\xc8\x5d\xa1\x48\x52\x46\x49\x7d\xbb\x06\xe2\xa2\x86\x6f\x03\x30\xdc\x11\x66\x62\x17\x83\x04\xf5\x4e\x44\xf6\xe4\x6c\x01\xe8\x3f\x31\x4b\x69\x83\xb2\xd4\x06\xa7\x5d\x3b\x39\x83\xf2\xad\x83\xd2\x75\x46\x28\xab\x6b\xb5\x58\x36\x2f\x56\x45\x51\x48\x17\x37\xcc\x4b\x00\xcb\xea\x4e\x29\x4d\x34\x06\x29\x91\x24\x41\x8d\xd2\xf7\x5d\x57\xe8\x17\xce\xb3\x6a\x50\xaa\x59\x3f\xde\x6e\x1e\x7e\x31\x63\x8f\xc6\xf0\x6c\xdf\xd1\xa4\x7c\x25\x3b\x6f\x1a\x54\xc3\x52\x73\x8b\xa4\x37\x2f\x37\xf1\x07\x25\x65\x69\xd0\x19\x4d\xa8\x1e\x96\x2b\x8c\x3c\x58\x79\x73\x57\x7b\x6b\x64\x85\x6a\xce\x01\x95\xed\xa1\x17\xf3\x79\x03\xf4\xef\x59\xb2\x45\x09\x22\x86\x42\xcc\x38\x03\x8e\x94\x47\xb5\xd4\xb9\x98\xcf\xbb\xb0\x9d\x9c\x75\x77\xcb\xdb\x0f\xa5\xa1\x60\xce\xa4\x03\x33\xe9\x82\xdc\x9e\x8a\x6e\x9c\xbe\xe2\x89\x14\x65\xde\x9c\x0f\x74\xca\x56\x66\x1a\x4d\xc3\x10\x62\x50\x45\xd7\x30\xb7\x5b\x5d\xb0\xba\x40\xb4\x26\xe1\x7e\x48\x80\x26\xe4\x25\x88\x09\x65\x18\x05\x44\x6b\x93\xee\xcc\x31\x5c\x36\x28\x3e\x93\x17\x9a\x64\x09\x38\x49\x60\xe2\x89\x72\x28\xe4\xfd\x22\x53\xe2\x32\x11\xee\x45\xa6\x83\x28\x93\x45\xb3\xd5\x55\xc5\x57\x61\x28\x32\xae\x21\x17\x87\x42\xbc\xa7\x84\xe7\xf3\x9f\xf1\xc9\x1e\x4f\x5d\xe3\xe2\x27\x3c\xf9\x93\xa2\x71\xb6\x42\xae\xa8\x1d\x77\x1a\xaf\x01\xf5\xe4\xee\x61\xd7\x06\xc6\xd5\xc7\xcd\x64\x79\x79\x35\xf9\xff\xdb\xcf\x4d\xb2\x8f\xa5\x8a\xd7\xe5\x1a\xf6\x5e\x4a\x0f\xac\x24\xa6\x69\x70\xdc\x51\x8d\x8c\x2a\xdd\x55\xa4\xee\xd7\x40\xa2\x48\xa2\x52\xa8\x80\x30\x26\x8e\xb6\x20\x82\x6b\xcf\x7b\xf7\x43\xd3\x60\xcb\x48\xb8\x1f\x04\xbb\x35\x07\x80\x11\xc4\x52\x24\xaf\x01\xe7\x69\x33\xb0\xef\x20\xe7\xeb\xce\xaf\x8f\x8f\xeb\x8d\xf5\x08\x61\xac\x7a\xe3\xea\xab\x38\x11\x55\x26\x94\x83\x8c\xdb\x7a\x87\x41\xa9\xd1\x13\xfa\x77\x4e\x01\xb8\xe0\x13\xc7\xd5\xc5\xd1\x48\xc9\x0e\x3a\x14\x62\x4f\xb1\x07\xf7\xab\x42\x08\x82\x5f\x85\xd2\x13\x48\x25\xc6\xf4\x05\x08\x8f\x60\x63\x55\x21\x66\xe4\xc9\x6e\xca\x61\xb4\xb6\x33\x85\x0d\x9a\x9e\xc2\x41\x5b\x49\x63\x9c\xe9\xf9\x00\xf9\x81\x4a\xc1\x13\x34\x26\xda\xf7\x31\x63\x7b\xf9\x94\x04\xf7\x6b\x05\x8c\xee\x11\x16\xd7\xcb\xe9\xe2\xea\xc3\xf4\x65\xfa\x52\xeb\x04\xca\x37\xb2\xef\xcd\x46\x27\x4f\x4a\xa6\x39\x32\x71\x1b\x0a\x1e\xd3\xc8\x4c\xe1\x84\xb5\x7b\xc6\xfc\x01\xd0\x25\x27\xa3\x02\x13\xf0\x35\x66\x69\xb6\x65\x34\xf4\xb6\xe6\x7f\x6d\x35\x59\xae\xa5\x2c\x9c\x1f\x98\x61\xa1\xea\x15\x72\x9b\x03\x85\xa1\x44\x1d\x6c\x89\xa2\xe1\x2b\x16\x35\x1e\x10\x1c\x94\x6f\x4c\x1b\xb1\x65\xd3\x93\x24\xb9\x3b\x5c\xab\xd9\x9e\x60\x4c\xe3\x59\x9b\xd5\x5a\xfd\x4c\xf9\xd2\x5a\xf6\x35\x16\xb5\xdd\x81\xb6\xc1\xc7\x50\x83\x6e\x37\xa6\x12\x55\x2a\xb8\x42\xcf\x44\x6b\xd4\xeb\x36\x14\x9a\x6d\x33\x0c\x40\x9b\x49\x85\xa2\x60\x10\x29\x72\x1a\x99\x8d\xa7\x52\xc4\x94\x59\x1f\x60\x42\x28\xeb\xe5\xcd\x9f\x2b\x2d\x86\xcf\xe5\xa0\xc6\x90\x03\x8d\xc1\xc2\x54\xec\x65\x91\x3c\xd3\x90\xe4\xef\xb9\xf7\x77\xc3\x5a\x91\xfa\xb1\x97\xb0\x57\xef\xbb\x61\x9d\xdc\x19\xe8\xab\xf7\x7d\xd0\x5e\x9f\x36\x6f\x8d\x5c\x0d\xf8\x5c\x16\x26\x30\x87\xff\x03\x8e\x07\x94\x80\x2f\x29\x95\x35\xae\x79\x35\x80\x45\x27\x4e\x12\x1a\x06\x12\x9f\xa8\xd2\xb2\xf9\xfc\xd5\xff\x2a\x91\x2b\x16\xb7\xc4\xd7\xef\xcb\xa9\x79\xc1\x08\x24\xba\x27\xd8\x20\x93\x34\x50\xe1\x0e\x93\x3c\x22\x5c\x1a\x1f\xbb\x77\xed\x56\x10\xac\xf2\x7a\x53\xa8\xc3\xd7\x87\x7b\xc8\xd5\xfd\x60\xb0\x28\x63\x30\xff\x7c\x3b\xcf\x6d\xd2\x9c\x63\x2e\xb3\x9e\x61\x5f\x2c\x7f\x9a\xce\xa7\xf3\xe9\x62\x98\x09\x16\xc5\x37\xa0\x04\x1b\x43\x09\xd5\x8a\x45\x6f\x5c\x1d\xda\xfd\xff\x27\x83\x6e\x07\x4b\x35\xf7\x99\x50\xe2\x82\xb7\x9e\x7f\x6e\x5b\x04\x1d\xc3\xa2\x51\x9c\x89\x5c\x67\x56\xfe\xa4\x52\x19\x62\x04\xea\x23\x63\x8c\x44\x67\x12\x95\x5f\x2a\xec\xef\x1a\xcb\xc5\xa0\x58\xab\x46\xc5\x02\xe9\xec\xa0\x88\xdc\xb6\x9e\xbd\x98\xae\x31\x2d\x07\x51\xd3\x0f\x38\x77\xf5\xc2\x76\xbf\xda\x0d\xb2\xbd\xe7\xf5\xae\xaf\xed\x70\x6f\x2d\x31\x13\xc7\x61\xb7\xd0\xbd\xb8\xd4\x52\x7e\xf3\x25\xa7\x46\x90\x57\xc7\x97\x70\x47\xcc\x10\x3d\x84\xc3\x59\x5e\xa8\xc0\xc3\x2f\xb7\xf0\xe1\xea\xfa\x5d\x6f\xe8\xa5\x99\xda\x99\x1e\xbe\x56\x86\x86\x10\x39\xc5\xc6\x66\xca\x51\xcb\xf0\x5e\x2f\x96\x57\xbd\xbc\x45\x2a\xcb\x43\xff\xbf\x9a\xd1\x0c\xf9\x4f\x97\xd7\x8b\x3e\xb7\x26\x61\x1a\x54\xcf\x18\x83\xe8\x3e\xdf\xae\xe1\xed\x67\xfb\xe3\xe4\xad\xe0\x1a\x5f\x34\xac\xa5\xd0\x22\x14\xec\xa2\xfb\x49\xa4\xf9\xb2\xab\x44\x66\xa7\xb3\x72\xda\x1e\x16\x8f\x4e\xcd\x1b\xbd\xf3\x6a\xde\xcb\xf4\xbf\x7e\xa4\xfb\x0b\x7f\xe6\x89\x52\x91\x0e\xdb\xc1\xdd\x5a\xac\xe1\xed\x1d\x26\x82\x97\x51\x20\x62\x73\x44\x22\x9e\x88\x78\xb2\x16\x66\x5a\x51\x54\xf0\x8b\x5e\xb6\xe7\xa3\x0e\x28\xd7\x52\xa8\xb4\x35\x3e\xf7\xef\xea\x6f\xff\x7c\x2c\x7e\xac\xf5\x75\xcf\xdf\x68\x89\x07\x11\x0e\x8f\xf5\x3c\x1b\x95\x4a\x2e\xc0\xe7\xf3\xeb\x3e\x96\x4c\xa1\xa4\x3c\x16\x81\xd9\xd3\xd0\x5d\xe4\x3f\x20\x98\x24\xfb\x55\xa1\xbc\xe7\xb1\x28\x1b\xc6\x9e\xe3\xb7\x25\xe3\xc7\xf8\xcd\x8f\x37\xff\x0e\x00\x00\xff\xff\xbb\x95\xdd\xac\xba\x20\x00\x00") func initOpenapiOpenapiYaoBytes() ([]byte, error) { return bindataRead( @@ -3575,7 +3575,7 @@ func initOpenapiOpenapiYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/openapi.yao", size: 8192, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/openapi.yao", size: 8378, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3595,7 +3595,7 @@ func initOpenapiScopes__yaoYaoYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/__yao/yao.yml", size: 747, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/__yao/yao.yml", size: 747, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3615,7 +3615,7 @@ func initOpenapiScopesAgentAssistantsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/agent/assistants.yml", size: 2054, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/agent/assistants.yml", size: 2054, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3635,7 +3635,7 @@ func initOpenapiScopesAgentRobotsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/agent/robots.yml", size: 4625, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/agent/robots.yml", size: 4625, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3655,7 +3655,7 @@ func initOpenapiScopesAliasYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/alias.yml", size: 17481, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/alias.yml", size: 17481, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3675,7 +3675,7 @@ func initOpenapiScopesApiApiYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/api/api.yml", size: 727, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/api/api.yml", size: 727, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3695,7 +3695,7 @@ func initOpenapiScopesAppMenuYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/app/menu.yml", size: 476, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/app/menu.yml", size: 476, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3715,7 +3715,7 @@ func initOpenapiScopesChatCompletionsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/chat/completions.yml", size: 2001, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/chat/completions.yml", size: 2001, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3735,7 +3735,7 @@ func initOpenapiScopesChatModelsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/chat/models.yml", size: 589, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/chat/models.yml", size: 589, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3755,7 +3755,7 @@ func initOpenapiScopesChatReferencesYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/chat/references.yml", size: 581, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/chat/references.yml", size: 581, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3775,7 +3775,7 @@ func initOpenapiScopesChatSessionsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/chat/sessions.yml", size: 1603, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/chat/sessions.yml", size: 1603, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3795,7 +3795,7 @@ func initOpenapiScopesDslDslsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/dsl/dsls.yml", size: 2911, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/dsl/dsls.yml", size: 2911, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3815,7 +3815,7 @@ func initOpenapiScopesFileFilesYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/file/files.yml", size: 1486, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/file/files.yml", size: 1486, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3835,7 +3835,7 @@ func initOpenapiScopesJobCategoriesYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/job/categories.yml", size: 249, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/job/categories.yml", size: 249, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3855,7 +3855,7 @@ func initOpenapiScopesJobExecutionsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/job/executions.yml", size: 1217, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/job/executions.yml", size: 1217, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3875,7 +3875,7 @@ func initOpenapiScopesJobJobsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/job/jobs.yml", size: 937, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/job/jobs.yml", size: 937, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3895,7 +3895,7 @@ func initOpenapiScopesJobLogsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/job/logs.yml", size: 564, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/job/logs.yml", size: 564, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3915,7 +3915,7 @@ func initOpenapiScopesJobStatsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/job/stats.yml", size: 419, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/job/stats.yml", size: 419, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3935,7 +3935,7 @@ func initOpenapiScopesKbBackupsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/kb/backups.yml", size: 713, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/kb/backups.yml", size: 713, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3955,7 +3955,7 @@ func initOpenapiScopesKbCollectionsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/kb/collections.yml", size: 1725, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/kb/collections.yml", size: 1725, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3975,7 +3975,7 @@ func initOpenapiScopesKbDocumentsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/kb/documents.yml", size: 2235, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/kb/documents.yml", size: 2235, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3995,7 +3995,7 @@ func initOpenapiScopesKbGraphsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/kb/graphs.yml", size: 1847, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/kb/graphs.yml", size: 1847, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4015,7 +4015,7 @@ func initOpenapiScopesKbHitsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/kb/hits.yml", size: 1766, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/kb/hits.yml", size: 1766, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4035,7 +4035,7 @@ func initOpenapiScopesKbProvidersYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/kb/providers.yml", size: 351, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/kb/providers.yml", size: 351, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4055,7 +4055,7 @@ func initOpenapiScopesKbSearchYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/kb/search.yml", size: 535, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/kb/search.yml", size: 535, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4075,7 +4075,7 @@ func initOpenapiScopesKbSegmentsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/kb/segments.yml", size: 2580, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/kb/segments.yml", size: 2580, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4095,7 +4095,7 @@ func initOpenapiScopesKbVotesYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/kb/votes.yml", size: 1805, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/kb/votes.yml", size: 1805, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4115,7 +4115,7 @@ func initOpenapiScopesLlmProvidersYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/llm/providers.yml", size: 268, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/llm/providers.yml", size: 268, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4135,7 +4135,7 @@ func initOpenapiScopesMcpServersYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/mcp/servers.yml", size: 242, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/mcp/servers.yml", size: 242, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4155,7 +4155,7 @@ func initOpenapiScopesMessengerChannelsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/messenger/channels.yml", size: 244, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/messenger/channels.yml", size: 244, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4175,7 +4175,7 @@ func initOpenapiScopesMessengerProvidersYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/messenger/providers.yml", size: 306, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/messenger/providers.yml", size: 306, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4195,7 +4195,7 @@ func initOpenapiScopesMessengerWebhooksYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/messenger/webhooks.yml", size: 512, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/messenger/webhooks.yml", size: 512, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4215,7 +4215,7 @@ func initOpenapiScopesScopesYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/scopes.yml", size: 527, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/scopes.yml", size: 527, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4235,7 +4235,7 @@ func initOpenapiScopesTraceTracesYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/trace/traces.yml", size: 1456, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/trace/traces.yml", size: 1456, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4255,7 +4255,7 @@ func initOpenapiScopesUserEntryYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/user/entry.yml", size: 794, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/user/entry.yml", size: 794, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4275,7 +4275,7 @@ func initOpenapiScopesUserFeaturesYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/user/features.yml", size: 231, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/user/features.yml", size: 231, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4295,7 +4295,7 @@ func initOpenapiScopesUserInvitationsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/user/invitations.yml", size: 1458, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/user/invitations.yml", size: 1458, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4315,7 +4315,7 @@ func initOpenapiScopesUserMembersYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/user/members.yml", size: 1873, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/user/members.yml", size: 1873, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4335,7 +4335,7 @@ func initOpenapiScopesUserProfileYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/user/profile.yml", size: 333, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/user/profile.yml", size: 333, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4355,7 +4355,7 @@ func initOpenapiScopesUserTeamsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/scopes/user/teams.yml", size: 1724, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/scopes/user/teams.yml", size: 1724, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4375,7 +4375,7 @@ func initOpenapiUserClientYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/user/client.yao", size: 624, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/user/client.yao", size: 624, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4395,7 +4395,7 @@ func initOpenapiUserEntryEnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/user/entry/en.yao", size: 2438, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/user/entry/en.yao", size: 2438, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4415,7 +4415,7 @@ func initOpenapiUserEntryZhCnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/user/entry/zh-cn.yao", size: 2322, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/user/entry/zh-cn.yao", size: 2322, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4435,7 +4435,7 @@ func initOpenapiUserProvidersAppleYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/user/providers/apple.yao", size: 946, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/user/providers/apple.yao", size: 946, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4455,7 +4455,7 @@ func initOpenapiUserProvidersGithubYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/user/providers/github.yao", size: 428, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/user/providers/github.yao", size: 428, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4475,7 +4475,7 @@ func initOpenapiUserProvidersGoogleYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/user/providers/google.yao", size: 434, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/user/providers/google.yao", size: 434, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4495,7 +4495,7 @@ func initOpenapiUserProvidersMicrosoftYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/user/providers/microsoft.yao", size: 487, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/user/providers/microsoft.yao", size: 487, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4515,7 +4515,7 @@ func initOpenapiUserTeamEnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/user/team/en.yao", size: 2662, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/user/team/en.yao", size: 2662, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4535,7 +4535,7 @@ func initOpenapiUserTeamZhCnYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/openapi/user/team/zh-cn.yao", size: 2576, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/openapi/user/team/zh-cn.yao", size: 2576, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4555,7 +4555,7 @@ func initScriptsMenuTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/menu.ts", size: 15362, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/scripts/menu.ts", size: 15362, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4575,7 +4575,7 @@ func initScriptsSetupTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/setup.ts", size: 11914, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/scripts/setup.ts", size: 11914, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4595,7 +4595,7 @@ func initSeedsInvitation_codesCsv() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/seeds/invitation_codes.csv", size: 643, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/seeds/invitation_codes.csv", size: 643, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4615,7 +4615,7 @@ func initSeedsMenusCsv() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/seeds/menus.csv", size: 976, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/seeds/menus.csv", size: 976, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4635,7 +4635,7 @@ func initSeedsRolesCsv() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/seeds/roles.csv", size: 2799, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/seeds/roles.csv", size: 2799, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4655,7 +4655,7 @@ func initSeedsTypesCsv() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/seeds/types.csv", size: 3357, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/seeds/types.csv", size: 3357, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4675,7 +4675,7 @@ func initServicesReademeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/services/READEME.md", size: 18, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/services/READEME.md", size: 18, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4695,7 +4695,7 @@ func initSuisWebSuiYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/suis/web.sui.yao", size: 675, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/suis/web.sui.yao", size: 675, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4715,7 +4715,7 @@ func initTsconfigJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/tsconfig.json", size: 178, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "init/tsconfig.json", size: 178, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4735,7 +4735,7 @@ func libsuiIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/index.ts", size: 13047, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "libsui/index.ts", size: 13047, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4755,7 +4755,7 @@ func libsuiOpenapiTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/openapi.ts", size: 22959, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "libsui/openapi.ts", size: 22959, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4775,7 +4775,7 @@ func libsuiUtilsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/utils.ts", size: 5959, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "libsui/utils.ts", size: 5959, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4795,7 +4795,7 @@ func libsuiYaoTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/yao.ts", size: 4338, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "libsui/yao.ts", size: 4338, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4815,7 +4815,7 @@ func publicIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "public/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "public/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4835,7 +4835,7 @@ func uiIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4855,7 +4855,7 @@ func yaoAssistantsEntityPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/entity/package.yao", size: 212, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/entity/package.yao", size: 212, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4875,7 +4875,7 @@ func yaoAssistantsEntityPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/entity/prompts.yml", size: 930, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/entity/prompts.yml", size: 930, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4895,7 +4895,7 @@ func yaoAssistantsKeywordPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/keyword/package.yao", size: 200, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/keyword/package.yao", size: 200, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4915,7 +4915,7 @@ func yaoAssistantsKeywordPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/keyword/prompts.yml", size: 990, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/keyword/prompts.yml", size: 990, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4935,7 +4935,7 @@ func yaoAssistantsKeywordSrcIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/keyword/src/index.ts", size: 4104, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/keyword/src/index.ts", size: 4104, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4955,7 +4955,7 @@ func yaoAssistantsNeedsearchPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/needsearch/package.yao", size: 207, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/needsearch/package.yao", size: 207, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4975,7 +4975,7 @@ func yaoAssistantsNeedsearchPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/needsearch/prompts.yml", size: 3092, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/needsearch/prompts.yml", size: 3092, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -4995,7 +4995,7 @@ func yaoAssistantsNeedsearchSrcIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/needsearch/src/index.ts", size: 2767, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/needsearch/src/index.ts", size: 2767, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5015,7 +5015,7 @@ func yaoAssistantsPromptPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/prompt/package.yao", size: 186, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/prompt/package.yao", size: 186, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5035,7 +5035,7 @@ func yaoAssistantsPromptPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/prompt/prompts.yml", size: 621, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/prompt/prompts.yml", size: 621, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5055,7 +5055,7 @@ func yaoAssistantsQuerydslPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/package.yao", size: 196, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/package.yao", size: 196, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5075,7 +5075,7 @@ func yaoAssistantsQuerydslPromptsAggregationYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/aggregation.yml", size: 6982, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/aggregation.yml", size: 6982, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5095,7 +5095,7 @@ func yaoAssistantsQuerydslPromptsComplexYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/complex.yml", size: 7352, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/complex.yml", size: 7352, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5115,7 +5115,7 @@ func yaoAssistantsQuerydslPromptsFilterYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/filter.yml", size: 7087, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/filter.yml", size: 7087, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5135,7 +5135,7 @@ func yaoAssistantsQuerydslPromptsJoinYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/join.yml", size: 8167, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/join.yml", size: 8167, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5155,7 +5155,7 @@ func yaoAssistantsQuerydslPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/prompts.yml", size: 5836, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/prompts.yml", size: 5836, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5175,7 +5175,7 @@ func yaoAssistantsQuerydslSrcIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/src/index.ts", size: 1873, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/src/index.ts", size: 1873, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5195,7 +5195,7 @@ func yaoAssistantsRobot_promptPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/robot_prompt/package.yao", size: 204, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/robot_prompt/package.yao", size: 204, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5215,7 +5215,7 @@ func yaoAssistantsRobot_promptPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/robot_prompt/prompts.yml", size: 2606, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/robot_prompt/prompts.yml", size: 2606, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5235,7 +5235,7 @@ func yaoAssistantsTitlePackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/title/package.yao", size: 178, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/title/package.yao", size: 178, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5255,7 +5255,7 @@ func yaoAssistantsTitlePromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/title/prompts.yml", size: 958, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/assistants/title/prompts.yml", size: 958, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5275,7 +5275,7 @@ func yaoDataIcons404Png() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5295,7 +5295,7 @@ func yaoDataIconsIconIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5315,7 +5315,7 @@ func yaoDataIconsIconIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5335,7 +5335,7 @@ func yaoDataIconsIconPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5355,7 +5355,7 @@ func yaoDataIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5375,7 +5375,7 @@ func yaoDataKbProvidersChunkingSemanticEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/en.json", size: 5543, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/en.json", size: 5543, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5395,7 +5395,7 @@ func yaoDataKbProvidersChunkingSemanticZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/zh-cn.json", size: 5446, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/zh-cn.json", size: 5446, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5415,7 +5415,7 @@ func yaoDataKbProvidersChunkingStructuredEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/en.json", size: 2423, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/en.json", size: 2423, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5435,7 +5435,7 @@ func yaoDataKbProvidersChunkingStructuredZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/zh-cn.json", size: 2321, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/zh-cn.json", size: 2321, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5455,7 +5455,7 @@ func yaoDataKbProvidersConverterMcpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/en.json", size: 4235, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/en.json", size: 4235, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5475,7 +5475,7 @@ func yaoDataKbProvidersConverterMcpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/zh-cn.json", size: 4060, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/zh-cn.json", size: 4060, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5495,7 +5495,7 @@ func yaoDataKbProvidersConverterOcrEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/en.json", size: 6631, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/en.json", size: 6631, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5515,7 +5515,7 @@ func yaoDataKbProvidersConverterOcrZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/zh-cn.json", size: 6501, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/zh-cn.json", size: 6501, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5535,7 +5535,7 @@ func yaoDataKbProvidersConverterOfficeEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/en.json", size: 5476, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/en.json", size: 5476, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5555,7 +5555,7 @@ func yaoDataKbProvidersConverterOfficeZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/zh-cn.json", size: 5356, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/zh-cn.json", size: 5356, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5575,7 +5575,7 @@ func yaoDataKbProvidersConverterUtf8EnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/en.json", size: 292, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/en.json", size: 292, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5595,7 +5595,7 @@ func yaoDataKbProvidersConverterUtf8ZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/zh-cn.json", size: 281, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/zh-cn.json", size: 281, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5615,7 +5615,7 @@ func yaoDataKbProvidersConverterVideoEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/en.json", size: 6411, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/en.json", size: 6411, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5635,7 +5635,7 @@ func yaoDataKbProvidersConverterVideoZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/zh-cn.json", size: 6297, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/zh-cn.json", size: 6297, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5655,7 +5655,7 @@ func yaoDataKbProvidersConverterVisionEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/en.json", size: 4085, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/en.json", size: 4085, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5675,7 +5675,7 @@ func yaoDataKbProvidersConverterVisionZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/zh-cn.json", size: 3949, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/zh-cn.json", size: 3949, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5695,7 +5695,7 @@ func yaoDataKbProvidersConverterWhisperEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/en.json", size: 4449, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/en.json", size: 4449, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5715,7 +5715,7 @@ func yaoDataKbProvidersConverterWhisperZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/zh-cn.json", size: 4312, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/zh-cn.json", size: 4312, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5735,7 +5735,7 @@ func yaoDataKbProvidersEmbeddingFastembedEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/en.json", size: 6865, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/en.json", size: 6865, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5755,7 +5755,7 @@ func yaoDataKbProvidersEmbeddingFastembedZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/zh-cn.json", size: 6685, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/zh-cn.json", size: 6685, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5775,7 +5775,7 @@ func yaoDataKbProvidersEmbeddingOpenaiEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/en.json", size: 5636, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/en.json", size: 5636, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5795,7 +5795,7 @@ func yaoDataKbProvidersEmbeddingOpenaiZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/zh-cn.json", size: 5463, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/zh-cn.json", size: 5463, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5815,7 +5815,7 @@ func yaoDataKbProvidersExtractionOpenaiEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/en.json", size: 9110, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/en.json", size: 9110, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5835,7 +5835,7 @@ func yaoDataKbProvidersExtractionOpenaiZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/zh-cn.json", size: 8827, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/zh-cn.json", size: 8827, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5855,7 +5855,7 @@ func yaoDataKbProvidersFetcherHttpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/en.json", size: 5885, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/en.json", size: 5885, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5875,7 +5875,7 @@ func yaoDataKbProvidersFetcherHttpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/zh-cn.json", size: 5925, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/zh-cn.json", size: 5925, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5895,7 +5895,7 @@ func yaoDataKbProvidersFetcherMcpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/en.json", size: 6819, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/en.json", size: 6819, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5915,7 +5915,7 @@ func yaoDataKbProvidersFetcherMcpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/zh-cn.json", size: 6611, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/zh-cn.json", size: 6611, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5935,7 +5935,7 @@ func yaoFieldsModelTransJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14938, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14938, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5955,7 +5955,7 @@ func yaoLangsEnUsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5975,7 +5975,7 @@ func yaoLangsZhCnGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -5995,7 +5995,7 @@ func yaoLangsZhCnLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6015,7 +6015,7 @@ func yaoLangsZhCnLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6035,7 +6035,7 @@ func yaoLangsZhHkGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6055,7 +6055,7 @@ func yaoLangsZhHkLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6075,7 +6075,7 @@ func yaoLangsZhHkLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6095,7 +6095,7 @@ func yaoModelsAgentAssistantModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/assistant.mod.yao", size: 6945, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/agent/assistant.mod.yao", size: 6945, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6115,7 +6115,7 @@ func yaoModelsAgentChatModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/chat.mod.yao", size: 3093, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/agent/chat.mod.yao", size: 3093, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6135,7 +6135,7 @@ func yaoModelsAgentExecutionModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/execution.mod.yao", size: 5579, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/agent/execution.mod.yao", size: 5579, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6155,7 +6155,7 @@ func yaoModelsAgentMessageModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/message.mod.yao", size: 3712, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/agent/message.mod.yao", size: 3712, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6175,7 +6175,7 @@ func yaoModelsAgentResumeModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/resume.mod.yao", size: 3896, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/agent/resume.mod.yao", size: 3896, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6195,7 +6195,7 @@ func yaoModelsAgentSearchModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/search.mod.yao", size: 3103, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/agent/search.mod.yao", size: 3103, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6215,7 +6215,7 @@ func yaoModelsAttachmentModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/attachment.mod.yao", size: 4687, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/attachment.mod.yao", size: 4687, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6235,7 +6235,7 @@ func yaoModelsAuditModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/audit.mod.yao", size: 5588, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/audit.mod.yao", size: 5588, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6255,7 +6255,7 @@ func yaoModelsConfigModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/config.mod.yao", size: 1649, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/config.mod.yao", size: 1649, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6275,7 +6275,7 @@ func yaoModelsDslModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/dsl.mod.yao", size: 3826, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/dsl.mod.yao", size: 3826, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6295,7 +6295,7 @@ func yaoModelsInvitationModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/invitation.mod.yao", size: 6693, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/invitation.mod.yao", size: 6693, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6315,7 +6315,7 @@ func yaoModelsJobCategoryModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/category.mod.yao", size: 2041, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/job/category.mod.yao", size: 2041, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6335,7 +6335,7 @@ func yaoModelsJobExecutionModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/execution.mod.yao", size: 7201, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/job/execution.mod.yao", size: 7201, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6355,7 +6355,7 @@ func yaoModelsJobJobModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/job.mod.yao", size: 6429, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/job/job.mod.yao", size: 6429, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6375,7 +6375,7 @@ func yaoModelsJobLogModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/log.mod.yao", size: 4711, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/job/log.mod.yao", size: 4711, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6395,7 +6395,7 @@ func yaoModelsKbCollectionModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/kb/collection.mod.yao", size: 5390, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/kb/collection.mod.yao", size: 5390, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6415,7 +6415,7 @@ func yaoModelsKbDocumentModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/kb/document.mod.yao", size: 9906, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/kb/document.mod.yao", size: 9906, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6435,7 +6435,7 @@ func yaoModelsMemberModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/member.mod.yao", size: 14798, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/member.mod.yao", size: 14798, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6455,7 +6455,7 @@ func yaoModelsRoleModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/role.mod.yao", size: 6434, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/role.mod.yao", size: 6434, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6475,7 +6475,7 @@ func yaoModelsTeamModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/team.mod.yao", size: 15823, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/team.mod.yao", size: 15823, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6495,7 +6495,7 @@ func yaoModelsUserOauth_accountModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user/oauth_account.mod.yao", size: 6928, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/user/oauth_account.mod.yao", size: 6928, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6515,7 +6515,7 @@ func yaoModelsUserTypeModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user/type.mod.yao", size: 7502, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/user/type.mod.yao", size: 7502, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6535,7 +6535,7 @@ func yaoModelsUserModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user.mod.yao", size: 12335, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/models/user.mod.yao", size: 12335, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6555,7 +6555,7 @@ func yaoReleaseAppYaz() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/release/app.yaz", size: 181682, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/release/app.yaz", size: 181682, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6575,7 +6575,7 @@ func yaoStoresAgentCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/stores/agent/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6595,7 +6595,7 @@ func yaoStoresAgentMemoryChatXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/memory/chat.xun.yao", size: 497, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/stores/agent/memory/chat.xun.yao", size: 497, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6615,7 +6615,7 @@ func yaoStoresAgentMemoryContextXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/memory/context.xun.yao", size: 507, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/stores/agent/memory/context.xun.yao", size: 507, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6635,7 +6635,7 @@ func yaoStoresAgentMemoryTeamXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/memory/team.xun.yao", size: 483, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/stores/agent/memory/team.xun.yao", size: 483, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6655,7 +6655,7 @@ func yaoStoresAgentMemoryUserXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/memory/user.xun.yao", size: 489, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/stores/agent/memory/user.xun.yao", size: 489, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6675,7 +6675,7 @@ func yaoStoresCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/cache.lru.yao", size: 285, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/stores/cache.lru.yao", size: 285, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6695,7 +6695,7 @@ func yaoStoresKbCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/kb/cache.lru.yao", size: 304, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/stores/kb/cache.lru.yao", size: 304, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6715,7 +6715,7 @@ func yaoStoresKbStoreXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/kb/store.xun.yao", size: 373, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/stores/kb/store.xun.yao", size: 373, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6735,7 +6735,7 @@ func yaoStoresOauthCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6755,7 +6755,7 @@ func yaoStoresOauthClientXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/client.xun.yao", size: 377, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/client.xun.yao", size: 377, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6775,7 +6775,7 @@ func yaoStoresOauthStoreXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/store.xun.yao", size: 401, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/store.xun.yao", size: 401, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6795,7 +6795,7 @@ func yaoStoresStoreXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/store.xun.yao", size: 369, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/stores/store.xun.yao", size: 369, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -6815,7 +6815,7 @@ func yaoUploadersAttachmentLocalYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/uploaders/attachment.local.yao", size: 1163, mode: os.FileMode(420), modTime: time.Unix(1770202828, 0)} + info := bindataFileInfo{name: "yao/uploaders/attachment.local.yao", size: 1163, mode: os.FileMode(420), modTime: time.Unix(1770207364, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/openapi/config.go b/openapi/config.go index 9925ef37..b0dff37a 100644 --- a/openapi/config.go +++ b/openapi/config.go @@ -93,6 +93,7 @@ func (config *Config) MarshalJSON() ([]byte, error) { IPBlacklist: config.OAuth.Security.IPBlacklist, RequireHTTPS: config.OAuth.Security.RequireHTTPS, DisableUnsecureEndpoints: config.OAuth.Security.DisableUnsecureEndpoints, + SecureCookie: config.OAuth.Security.SecureCookie, }, Client: TempClientConfig{ DefaultClientType: config.OAuth.Client.DefaultClientType, @@ -215,6 +216,7 @@ func (config *Config) UnmarshalJSON(data []byte) error { IPBlacklist: tempConfig.OAuth.Security.IPBlacklist, RequireHTTPS: tempConfig.OAuth.Security.RequireHTTPS, DisableUnsecureEndpoints: tempConfig.OAuth.Security.DisableUnsecureEndpoints, + SecureCookie: tempConfig.OAuth.Security.SecureCookie, } if tempConfig.OAuth.Security.StateParameterLifetime != "" { if duration, err := parseDuration(tempConfig.OAuth.Security.StateParameterLifetime); err == nil { diff --git a/openapi/oauth/guard.go b/openapi/oauth/guard.go index c9695790..c29449a8 100644 --- a/openapi/oauth/guard.go +++ b/openapi/oauth/guard.go @@ -109,7 +109,8 @@ func (s *Service) tryAutoRefreshToken(c *gin.Context, _ *types.TokenClaims) { func (s *Service) getAccessToken(c *gin.Context) string { token := c.GetHeader("Authorization") if token == "" { - cookie, err := c.Cookie("__Host-access_token") + cookieName := response.GetCookieName("access_token") + cookie, err := c.Cookie(cookieName) if err != nil { return "" } @@ -176,7 +177,8 @@ func (s *Service) GetAccessToken(c *gin.Context) string { func (s *Service) getRefreshToken(c *gin.Context) string { token := c.GetHeader("Authorization") if token == "" { - cookie, err := c.Cookie("__Host-refresh_token") + cookieName := response.GetCookieName("refresh_token") + cookie, err := c.Cookie(cookieName) if err != nil { return "" } @@ -200,7 +202,8 @@ func (s *Service) getSessionID(c *gin.Context) string { } // 1. Try to get Session ID from cookies first - if sid, err := c.Cookie("__Host-session_id"); err == nil && sid != "" { + cookieName := response.GetCookieName("session_id") + if sid, err := c.Cookie(cookieName); err == nil && sid != "" { return sid } diff --git a/openapi/oauth/oauth.go b/openapi/oauth/oauth.go index e67156ea..cbc9626a 100644 --- a/openapi/oauth/oauth.go +++ b/openapi/oauth/oauth.go @@ -179,6 +179,14 @@ func (s *Service) GetStore() store.Store { return s.store } +// GetSecurityConfig returns the security configuration for the service +func (s *Service) GetSecurityConfig() types.SecurityConfig { + if s.config == nil { + return types.SecurityConfig{} + } + return s.config.Security +} + // setConfigDefaults sets default values for configuration func setConfigDefaults(config *Config) error { // Certificate defaults diff --git a/openapi/oauth/types/types.go b/openapi/oauth/types/types.go index 730cee42..d7477b1d 100644 --- a/openapi/oauth/types/types.go +++ b/openapi/oauth/types/types.go @@ -575,6 +575,9 @@ type SecurityConfig struct { IPBlacklist []string `json:"ip_blacklist,omitempty"` // Optional: IP addresses blocked from access (default: []) RequireHTTPS bool `json:"require_https"` // Optional: Require HTTPS for all endpoints (default: true) DisableUnsecureEndpoints bool `json:"disable_unsecure_endpoints"` // Optional: Disable non-HTTPS endpoints (default: false) + + // Cookie security settings + SecureCookie *bool `json:"secure_cookie,omitempty"` // Optional: Use __Host- prefix and Secure flag for cookies (default: true). Set to false for non-HTTPS dev environments with non-localhost IPs. } // TokenClaims represents decoded token claims for both JWT and opaque tokens diff --git a/openapi/openapi.go b/openapi/openapi.go index a7d6ff0c..32c0e64e 100644 --- a/openapi/openapi.go +++ b/openapi/openapi.go @@ -21,6 +21,8 @@ import ( "github.com/yaoapp/yao/openapi/oauth" "github.com/yaoapp/yao/openapi/oauth/acl" "github.com/yaoapp/yao/openapi/oauth/types" + "github.com/yaoapp/yao/openapi/response" + "github.com/yaoapp/yao/openapi/sandbox" "github.com/yaoapp/yao/openapi/team" openapiTrace "github.com/yaoapp/yao/openapi/trace" "github.com/yaoapp/yao/openapi/user" @@ -63,6 +65,10 @@ func Load(appConfig config.Config) (*OpenAPI, error) { return nil, err } + // Set the secure cookie configuration for the response package + // This determines whether to use __Host- prefix and Secure flag for cookies + response.SetSecureCookieEnabled(oauthConfig.Security.SecureCookie) + // Load user configurations err = user.Load(appConfig) if err != nil { @@ -154,6 +160,10 @@ func (openapi *OpenAPI) Attach(router *gin.Engine) { // App handlers (menu, etc.) app.Attach(group.Group("/app"), openapi.OAuth) + // Sandbox handlers (VNC proxy for visual browser automation) + sandbox.SetPathPrefix(baseURL) + sandbox.Attach(group.Group("/sandbox"), openapi.OAuth) + // Custom handlers (Defined by developer) } diff --git a/openapi/response/response.go b/openapi/response/response.go index 23e97c0c..3286716d 100644 --- a/openapi/response/response.go +++ b/openapi/response/response.go @@ -8,6 +8,32 @@ import ( "github.com/yaoapp/yao/openapi/oauth/types" ) +// secureCookieEnabled is the global setting for secure cookie behavior +// Default is nil (meaning true/enabled). Set to false to disable __Host- prefix and Secure flag. +// This is set during OAuth initialization based on the secure_cookie config. +var secureCookieEnabled *bool + +// SetSecureCookieEnabled sets the global secure cookie setting +// This should be called during OAuth initialization +func SetSecureCookieEnabled(enabled *bool) { + secureCookieEnabled = enabled +} + +// IsSecureCookieEnabled returns whether secure cookie is enabled +// Returns true if secureCookieEnabled is nil or true +func IsSecureCookieEnabled() bool { + return secureCookieEnabled == nil || *secureCookieEnabled +} + +// GetCookieName returns the correct cookie name based on secure cookie setting +// If secure cookie is enabled, it returns "__Host-" + name, otherwise just name +func GetCookieName(name string) string { + if IsSecureCookieEnabled() { + return "__Host-" + name + } + return name +} + // Type aliases for OAuth types to simplify usage type ( // Core response types @@ -202,13 +228,14 @@ type SecureCookieOptions struct { } // NewSecureCookieOptions creates a new SecureCookieOptions with secure defaults +// The UseHostPrefix is determined by the secure_cookie configuration in openapi.yao func NewSecureCookieOptions() *SecureCookieOptions { return &SecureCookieOptions{ - MaxAge: 0, // Session cookie by default - Path: "/", // Root path - Domain: "", // Current domain - SameSite: "Lax", // Default SameSite policy - UseHostPrefix: true, // Use most secure __Host- prefix + MaxAge: 0, // Session cookie by default + Path: "/", // Root path + Domain: "", // Current domain + SameSite: "Lax", // Default SameSite policy + UseHostPrefix: IsSecureCookieEnabled(), // Determined by secure_cookie config } } @@ -284,12 +311,15 @@ func SendSecureCookieWithOptions(c *gin.Context, key string, value string, optio cookiePath := options.Path cookieDomain := options.Domain - if options.UseHostPrefix { + // Use the global secure cookie setting + useSecureCookie := IsSecureCookieEnabled() + + if options.UseHostPrefix && useSecureCookie { // __Host- prefix: Requires Secure flag, no Domain attribute, Path=/ cookieName = "__Host-" + key cookiePath = "/" // Must be "/" for __Host- prefix cookieDomain = "" // Must be empty for __Host- prefix - } else if options.UseSecurePrefix { + } else if options.UseSecurePrefix && useSecureCookie { // __Secure- prefix: Requires Secure flag, allows Domain and Path cookieName = "__Secure-" + key } @@ -319,7 +349,7 @@ func SendSecureCookieWithOptions(c *gin.Context, key string, value string, optio effectiveMaxAge, // maxAge (calculated from Expires if needed) cookiePath, // path cookieDomain, // domain - true, // secure (HTTPS only) - required for security prefixes + useSecureCookie, // secure (HTTPS only) - based on secure_cookie config true, // httpOnly (prevent XSS access) ) diff --git a/openapi/sandbox/sandbox.go b/openapi/sandbox/sandbox.go new file mode 100644 index 00000000..f0d6e731 --- /dev/null +++ b/openapi/sandbox/sandbox.go @@ -0,0 +1,119 @@ +package sandbox + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/yaoapp/yao/openapi/oauth/types" + "github.com/yaoapp/yao/sandbox/vncproxy" +) + +var vncProxy *vncproxy.Proxy + +// Attach attaches sandbox handlers to the router group +// Routes: +// - GET /sandbox/:id/vnc - Get VNC status +// - GET /sandbox/:id/vnc/client - Get noVNC client page +// - GET /sandbox/:id/vnc/ws - WebSocket proxy to container VNC +func Attach(group *gin.RouterGroup, oauth types.OAuth) { + // Initialize VNC proxy lazily on first request + // This avoids startup errors if Docker is not available + + // VNC status endpoint + group.GET("/:id/vnc", oauth.Guard, handleVNCStatus) + + // VNC client page + group.GET("/:id/vnc/client", oauth.Guard, handleVNCClient) + + // VNC WebSocket proxy + group.GET("/:id/vnc/ws", oauth.Guard, handleVNCWebSocket) +} + +// ensureProxy ensures the VNC proxy is initialized +func ensureProxy() error { + if vncProxy != nil { + return nil + } + + var err error + vncProxy, err = vncproxy.NewProxy(nil) + return err +} + +// handleVNCStatus returns VNC status for a sandbox container +func handleVNCStatus(c *gin.Context) { + if err := ensureProxy(); err != nil { + c.JSON(http.StatusServiceUnavailable, gin.H{ + "error": "VNC service not available", + }) + return + } + + // Rewrite path to match vncproxy expected format + sandboxID := c.Param("id") + c.Request.URL.Path = "/v1/sandbox/" + sandboxID + "/vnc" + + vncProxy.HandleVNCStatus(c.Writer, c.Request) +} + +// handleVNCClient serves the noVNC client page +func handleVNCClient(c *gin.Context) { + if err := ensureProxy(); err != nil { + c.JSON(http.StatusServiceUnavailable, gin.H{ + "error": "VNC service not available", + }) + return + } + + // Rewrite path to match vncproxy expected format + sandboxID := c.Param("id") + c.Request.URL.Path = "/v1/sandbox/" + sandboxID + "/vnc/client" + + vncProxy.HandleVNCClient(c.Writer, c.Request) +} + +// handleVNCWebSocket proxies WebSocket to container VNC +func handleVNCWebSocket(c *gin.Context) { + if err := ensureProxy(); err != nil { + c.JSON(http.StatusServiceUnavailable, gin.H{ + "error": "VNC service not available", + }) + return + } + + // Rewrite path to match vncproxy expected format + sandboxID := c.Param("id") + c.Request.URL.Path = "/v1/sandbox/" + sandboxID + "/vnc/ws" + + vncProxy.HandleVNCWebSocket(c.Writer, c.Request) +} + +// Close closes the VNC proxy and releases resources +func Close() error { + if vncProxy != nil { + return vncProxy.Close() + } + return nil +} + +// pathPrefix stores the router path prefix for sandbox endpoints +var pathPrefix string = "/v1/sandbox" + +// SetPathPrefix sets the path prefix for sandbox URLs +// Called during router setup with the actual OpenAPI base URL +func SetPathPrefix(prefix string) { + pathPrefix = strings.TrimSuffix(prefix, "/") + "/sandbox" +} + +// GetVNCClientURL returns the API VNC client page URL +// sandboxID is the sandbox identifier (userID-chatID) +// Returns the URL path like "/v1/sandbox/{id}/vnc/client" +// Note: For CUI navigation, use "$dashboard/sandbox/{id}" directly with sandbox_id +func GetVNCClientURL(sandboxID string) string { + if sandboxID == "" { + return "" + } + return fmt.Sprintf("%s/%s/vnc/client", pathPrefix, sandboxID) +} diff --git a/openapi/types.go b/openapi/types.go index a30f1c63..44568eaf 100644 --- a/openapi/types.go +++ b/openapi/types.go @@ -92,6 +92,7 @@ type TempSecurityConfig struct { IPBlacklist []string `json:"ip_blacklist,omitempty"` RequireHTTPS bool `json:"require_https"` DisableUnsecureEndpoints bool `json:"disable_unsecure_endpoints"` + SecureCookie *bool `json:"secure_cookie,omitempty"` } // TempClientConfig represents client configuration with string duration fields diff --git a/openapi/user/entry.go b/openapi/user/entry.go index 2e49886c..4c289c6f 100644 --- a/openapi/user/entry.go +++ b/openapi/user/entry.go @@ -45,6 +45,9 @@ func getEntryConfig(c *gin.Context) { // Create public config without sensitive data (deep copy to avoid modifying global config) publicConfig := createPublicEntryConfig(config) + // Add secure_cookie setting from OAuth config + publicConfig.SecureCookie = response.IsSecureCookieEnabled() + // Return the entry configuration response.RespondWithSuccess(c, response.StatusOK, publicConfig) } diff --git a/openapi/user/types.go b/openapi/user/types.go index ae88e03f..30c6d57c 100644 --- a/openapi/user/types.go +++ b/openapi/user/types.go @@ -112,6 +112,7 @@ type EntryConfig struct { InviteRequired bool `json:"invite_required,omitempty"` // From register config Invite *InvitePageConfig `json:"invite,omitempty"` // Invite code page configuration ThirdParty *ThirdParty `json:"third_party,omitempty"` + SecureCookie bool `json:"secure_cookie"` // Whether secure cookie is enabled (for frontend JWT verification) } // MessengerConfig represents the messenger configuration for user registration diff --git a/sandbox/DESIGN-PLAYWRIGHT-VNC.md b/sandbox/DESIGN-PLAYWRIGHT-VNC.md new file mode 100644 index 00000000..46a28dfc --- /dev/null +++ b/sandbox/DESIGN-PLAYWRIGHT-VNC.md @@ -0,0 +1,1498 @@ +# Sandbox VNC Integration Design Document + +## Overview + +This document describes the design for integrating VNC remote desktop access into the Yao Sandbox system. This enables users to **observe Claude's operations in real-time** through a web-based VNC client, providing full transparency and building trust. + +The design provides **multiple sandbox image variants** with VNC support. Users can choose the appropriate image type when configuring their assistants based on their needs. + +## Goals + +1. **Transparency**: Let users see exactly what Claude is doing in the sandbox in real-time +2. **Multiple Image Options**: Provide different sandbox images for different use cases +3. **User Choice**: Allow users to select sandbox image type when building assistants +4. **Web-Based Access**: Use noVNC for browser-based VNC access (no client installation required) +5. **Unified Entry Point**: Single proxy endpoint to access any container's VNC session +6. **Security**: Proper authentication and isolation between users +7. **Minimal Core Changes**: Leverage existing sandbox infrastructure with minimal modifications + +## Non-Goals + +1. Persistent VNC sessions across container restarts +2. Multi-user access to the same VNC session +3. Audio support + +## Architecture + +### High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ User Browser │ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ Yao Web UI │ │ +│ │ │ │ +│ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │ +│ │ │ 💬 Chat Window │ │ 📺 VNC Preview (iframe) │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ User: Help me... │ │ Real-time view of Claude's │ │ │ +│ │ │ │ │ operations in sandbox │ │ │ +│ │ │ Claude: Working... │ │ │ │ │ +│ │ └─────────────────────┘ └─────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + │ WebSocket (VNC) + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Yao Server (Host) │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ VNC Proxy Service │ │ +│ │ (sandbox/vncproxy) │ │ +│ │ │ │ +│ │ Endpoints: │ │ +│ │ ├── GET /v1/sandbox/{id}/vnc → VNC status │ │ +│ │ ├── GET /v1/sandbox/{id}/vnc/client → noVNC client │ │ +│ │ └── GET /v1/sandbox/{id}/vnc/ws → WebSocket │ │ +│ │ │ │ +│ │ Internal Flow: │ │ +│ │ 1. Authenticate request (JWT/session) │ │ +│ │ 2. Resolve container name: yao-sandbox-{id} │ │ +│ │ 3. Get container IP from Docker API │ │ +│ │ 4. Proxy WebSocket to container_ip:6080 │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ Docker Bridge Network │ +│ │ │ +│ ┌──────────────────────────────────────┼──────────────────────────────┐ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌──────────────────┐ ┌──────────────────────────┐ ┌──────────────────┐ │ +│ │ sandbox-claude │ │ sandbox-claude-browser │ │ sandbox-claude- │ │ +│ │ (No VNC) │ │ (Browser + VNC) │ │ desktop (Full) │ │ +│ │ │ │ │ │ │ │ +│ │ • Claude CLI │ │ • Claude CLI │ │ • Claude CLI │ │ +│ │ • Node.js │ │ • Node.js │ │ • Node.js │ │ +│ │ • Python │ │ • Python │ │ • Python │ │ +│ │ │ │ • Playwright + Browsers │ │ • XFCE Desktop │ │ +│ │ │ │ • Xvfb + VNC │ │ • File Manager │ │ +│ │ │ │ • Fluxbox (minimal WM) │ │ • Terminal │ │ +│ │ │ │ │ │ • Xvfb + VNC │ │ +│ └──────────────────┘ └──────────────────────────┘ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Image Variants + +| Image | VNC | Use Case | Size | Memory | +|-------|-----|----------|------|--------| +| `sandbox-claude` | ❌ | Code execution, scripts, CLI tasks | ~700MB | 2GB | +| `sandbox-claude-browser` | ✅ | Browser automation, web scraping | ~1.8GB | 4GB | +| `sandbox-claude-desktop` | ✅ | Full visibility, any GUI app | ~2.5GB | 4GB | + +### User Selection Flow + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Assistant Configuration UI │ +│ │ +│ Assistant Name: [My Web Scraper ] │ +│ │ +│ Sandbox Environment: │ +│ ┌─────────────────────────────────────────────────────────────────┐│ +│ │ ○ Standard (sandbox-claude) ││ +│ │ Code execution, no GUI. Lightweight and fast. ││ +│ │ ││ +│ │ ○ Browser (sandbox-claude-browser) ⭐ ││ +│ │ Playwright browser automation with VNC preview. ││ +│ │ See browser operations in real-time. ││ +│ │ ││ +│ │ ● Desktop (sandbox-claude-desktop) ││ +│ │ Full Ubuntu desktop with VNC preview. ││ +│ │ See ALL operations: terminal, files, browser, etc. ││ +│ └─────────────────────────────────────────────────────────────────┘│ +│ │ +│ [ Save Assistant ] │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Components + +### 1. Docker Images + +**Location**: `sandbox/docker/` + +Three image variants sharing the same VNC infrastructure: + +``` +ubuntu:24.04 + └── sandbox-base:latest (~200MB) + └── sandbox-claude:latest (~700MB) # No VNC + ├── sandbox-claude-browser:latest (~1.8GB) # VNC + Browser + └── sandbox-claude-desktop:latest (~2.5GB) # VNC + Full Desktop +``` + +#### 1.1 sandbox-claude-browser (Browser + VNC) + +For browser automation tasks with real-time visibility. + +**Includes**: +- Everything from `sandbox-claude` +- Xvfb (virtual display) +- x11vnc + noVNC +- Fluxbox (minimal window manager) +- Playwright + Chromium/Firefox + +#### 1.2 sandbox-claude-desktop (Full Desktop + VNC) + +For maximum transparency - users can see everything Claude does. + +**Includes**: +- Everything from `sandbox-claude` +- Xvfb (virtual display) +- x11vnc + noVNC +- XFCE desktop environment +- Thunar file manager +- xfce4-terminal +- Playwright + browsers (optional) + +### 2. VNC Proxy Service + +**Location**: `sandbox/vncproxy/` + +A unified Go service that provides VNC access to all VNC-enabled containers. + +**Key Features**: +- Single entry point for all containers +- WebSocket proxy to container VNC +- Container IP resolution via Docker API +- Authentication and authorization +- Works with any VNC-enabled image + +**Key Interfaces**: + +```go +// VNCProxy handles VNC connections to sandbox containers +type VNCProxy struct { + docker *client.Client + manager *sandbox.Manager + config *Config +} + +// Config for VNC proxy +type Config struct { + // Container VNC port (fixed, internal) + ContainerVNCPort int // default: 5900 + + // Container noVNC/websockify port (fixed, internal) + ContainerNoVNCPort int // default: 6080 + + // Connection timeout + Timeout time.Duration +} + +// ServeHTTP handles HTTP requests +func (p *VNCProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) + +// GetVNCURL returns the VNC URL for a container +func (p *VNCProxy) GetVNCURL(sandboxID string) (string, error) + +// GetContainerIP returns the internal IP of a container +func (p *VNCProxy) GetContainerIP(containerName string) (string, error) +``` + +### 3. Manager Extensions (Optional) + +**Location**: `sandbox/manager.go` + +**Note**: These extensions are optional. The core sandbox functionality works without changes because: +- Image is specified in assistant config, passed to existing `GetOrCreate()` +- VNC status is determined by checking container env vars at runtime + +Optional helper types for convenience: + +```go +// ImageType represents the sandbox image variant (optional, for reference) +type ImageType string + +const ( + ImageTypeClaude ImageType = "claude" // No VNC + ImageTypeBrowser ImageType = "browser" // Browser + VNC + ImageTypeDesktop ImageType = "desktop" // Full desktop + VNC +) + +// ImageConfig holds configuration for each image type (optional, for reference) +var ImageConfigs = map[ImageType]struct { + Image string + VNCEnabled bool + Memory string + CPU float64 +}{ + ImageTypeClaude: {"yaoapp/sandbox-claude:latest", false, "2g", 1.0}, + ImageTypeBrowser: {"yaoapp/sandbox-claude-browser:latest", true, "4g", 2.0}, + ImageTypeDesktop: {"yaoapp/sandbox-claude-desktop:latest", true, "4g", 2.0}, +} +``` + +VNC access is determined at runtime by VNC Proxy checking container env vars - no Manager changes needed. + +### 4. API Endpoints + +All endpoints under `/v1/sandbox/`. Each sandbox has its own unique ID (generated by the caller/business layer). + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/v1/sandbox/{id}` | POST | Create container (with image) | +| `/v1/sandbox/{id}` | GET | Get container status | +| `/v1/sandbox/{id}` | DELETE | Stop/remove container | +| `/v1/sandbox/{id}/vnc` | GET | Get VNC access info | +| `/v1/sandbox/{id}/vnc/client` | GET | Serve noVNC HTML client (supports `?viewonly=true`) | +| `/v1/sandbox/{id}/vnc/ws` | GET | WebSocket proxy to container VNC | + +**Sandbox ID**: +- Generated by the caller (business layer) +- Format: any unique string (e.g., UUID, `{userID}-{chatID}`, `{assistantID}-{sessionID}`) +- Container name: `yao-sandbox-{id}` + +**Create Container Request**: + +```json +// POST /v1/sandbox/abc123-def456 +{ + "image": "yaoapp/sandbox-claude-desktop:latest" // Optional, defaults based on config +} +``` + +**VNC Access Response**: + +```json +// GET /v1/sandbox/abc123-def456/vnc +// VNC ready: +{ + "available": true, + "status": "ready", + "sandbox_id": "abc123-def456", + "container": "yao-sandbox-abc123-def456", + "client_url": "/v1/sandbox/abc123-def456/vnc/client", + "websocket_url": "/v1/sandbox/abc123-def456/vnc/ws" +} + +// VNC starting (container running but VNC services not ready yet): +{ + "available": false, + "status": "starting", + "sandbox_id": "abc123-def456", + "container": "yao-sandbox-abc123-def456", + "message": "VNC services are starting..." +} + +// VNC not supported (sandbox-claude image): +{ + "available": false, + "status": "not_supported", + "sandbox_id": "abc123-def456", + "container": "yao-sandbox-abc123-def456", + "message": "VNC not available for this container type" +} + +// Container not found/running: +{ + "available": false, + "status": "unavailable", + "sandbox_id": "abc123-def456", + "message": "Container not available" +} +``` + +**Full API Structure**: + +``` +/v1/sandbox/ +├── {id} +│ ├── POST # Create container +│ ├── GET # Get container status +│ ├── DELETE # Stop/remove container +│ ├── /exec # Execute command +│ ├── /files # File operations +│ └── /vnc # VNC access (if available) +│ ├── GET # VNC status & URLs +│ ├── /client # noVNC HTML client +│ └── /ws # WebSocket proxy +``` + +**Business Layer Integration Example**: + +```go +// Agent executor generates sandbox ID +sandboxID := fmt.Sprintf("%s-%s", userID, chatID) + +// Or use UUID for more isolation +sandboxID := uuid.New().String() + +// Or per-assistant session +sandboxID := fmt.Sprintf("%s-%s", assistantID, sessionID) +``` + +### 5. Assistant Configuration (Developer Side) + +Developers configure sandbox image type in the assistant's `package.yao` file: + +```yaml +# assistants/my-assistant/package.yao +name: My Web Assistant +description: Web scraping assistant with browser preview + +sandbox: + command: claude + image: "yaoapp/sandbox-claude-desktop:latest" # Choose image variant + max_memory: "4g" + max_cpu: 2.0 +``` + +**Available Images**: +- `yaoapp/sandbox-claude:latest` - No VNC, lightweight +- `yaoapp/sandbox-claude-browser:latest` - Browser + VNC +- `yaoapp/sandbox-claude-desktop:latest` - Full desktop + VNC + +**Note**: No changes required to `agent/sandbox/` code. The existing `Image` field in `SandboxConfig` already supports custom images. + +### 6. CUI Integration (User Side) + +Users interact with VNC preview through CUI's action system. The VNC preview opens as a **sidebar iframe** via the `navigate` action. + +#### 6.1 Roles and Responsibilities + +| Role | Action | Interface | +|------|--------|-----------| +| **Developer** | Configure `sandbox.image` in `package.yao` | YAML config file | +| **User** | View VNC preview during chat | CUI chat interface | + +#### 6.2 No CUI Page Needed + +The CUI `navigate` action already supports loading any URL via iframe in the sidebar. The `/v1/sandbox/{id}/vnc/client` API returns a complete HTML page with noVNC, so we can use it directly. + +**Navigate action route types** (from `cui/packages/cui/chatbox/messages/Action/actions/navigate.ts`): +- `$dashboard/xxx` → CUI Dashboard pages +- `/xxx` → Loaded via iframe in sidebar +- `http(s)://xxx` → External URLs via iframe + +Since `/v1/sandbox/{id}/vnc/client` starts with `/`, it will be loaded in an iframe automatically. + +#### 6.3 Opening VNC Preview via Action + +When the sandbox starts and VNC is available, Claude can return a `navigate` action to open the preview: + +```json +{ + "type": "action", + "actions": [{ + "name": "navigate", + "payload": { + "route": "/v1/sandbox/abc123-def456/vnc/client", + "title": "实时预览", + "icon": "material-desktop_windows" + } + }] +} +``` + +Or as a clickable button in the chat: + +```json +{ + "type": "action", + "actions": [{ + "name": "button", + "payload": { + "text": "📺 查看实时预览", + "action": { + "name": "navigate", + "payload": { + "route": "/v1/sandbox/abc123-def456/vnc/client", + "title": "实时预览", + "icon": "material-desktop_windows" + } + } + } + }] +} +``` + +#### 6.4 User Experience Flow + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Step 1: User starts chat with sandbox-enabled assistant │ +│ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ 💬 Chat │ │ +│ │ │ │ +│ │ User: 帮我爬取这个网站的数据 │ │ +│ │ │ │ +│ │ Claude: 好的,我正在启动浏览器环境... │ │ +│ │ [📺 查看实时预览] ← Action button │ │ +│ │ │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + │ User clicks button + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Step 2: VNC preview opens in sidebar (iframe loads /vnc/client API) │ +│ │ +│ ┌──────────────────────────┐ ┌────────────────────────────────────┐ │ +│ │ 💬 Chat │ │ 📺 实时预览 [×] │ │ +│ │ │ │ ┌────────────────────────────────┐│ │ +│ │ User: 帮我爬取... │ │ │ ││ │ +│ │ │ │ │ noVNC (from API response) ││ │ +│ │ Claude: 正在打开 │ │ │ ││ │ +│ │ 浏览器,访问目标网站... │ │ │ User can see Claude ││ │ +│ │ │ │ │ operating the browser ││ │ +│ │ [📺 查看实时预览] │ │ │ ││ │ +│ │ │ │ └────────────────────────────────┘│ │ +│ └──────────────────────────┘ └────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +#### 6.5 How Claude Knows to Show VNC Button + +The VNC preview button is triggered by the **Agent executor**, not by Claude itself. When the sandbox starts with a VNC-enabled image, the executor can inject a system message or action. + +**Option A: Agent Executor Injects Action** (Recommended) + +In `agent/sandbox/claude/executor.go`, when sandbox starts with VNC: + +```go +func (e *Executor) Stream(...) { + // After sandbox container is ready + if e.isVNCEnabled() { + // Send VNC preview action to frontend + handler(message.StreamEvent{ + Type: "action", + Data: map[string]interface{}{ + "actions": []map[string]interface{}{{ + "name": "button", + "payload": map[string]interface{}{ + "text": "📺 查看实时预览", + "action": map[string]interface{}{ + "name": "navigate", + "payload": map[string]interface{}{ + "route": fmt.Sprintf("/v1/sandbox/%s/vnc/client", sandboxID), + "title": "实时预览", + }, + }, + }, + }}, + }, + }) + } + // ... continue with Claude execution +} +``` + +**Option B: System Prompt Hint** + +Add to system prompt when VNC is enabled: +``` +当你在沙盒中执行可视化任务时(如浏览器操作),可以告知用户点击"查看实时预览"按钮观看操作过程。 +``` + +#### 6.6 VNC Interaction Modes + +The VNC preview supports two modes controlled by the `viewonly` query parameter: + +| Mode | URL | Description | +|------|-----|-------------| +| **Interactive** (default) | `/vnc/client` | User can use keyboard and mouse | +| **View-only** | `/vnc/client?viewonly=true` | User can only watch | + +**Use Cases**: + +| Scenario | Mode | Example | +|----------|------|---------| +| Watch Claude browse web | View-only | `?viewonly=true` | +| User needs to login | Interactive | (default) | +| User needs to solve CAPTCHA | Interactive | (default) | +| Sensitive operation | View-only | `?viewonly=true` | + +**Action Examples**: + +```json +// View-only mode (just watching) +{ + "name": "navigate", + "payload": { + "route": "/v1/sandbox/abc123/vnc/client?viewonly=true", + "title": "实时预览" + } +} + +// Interactive mode (user needs to login) +{ + "name": "navigate", + "payload": { + "route": "/v1/sandbox/abc123/vnc/client", + "title": "请在此登录" + } +} +``` + +**How Claude Waits for User Input**: + +When user interaction is needed (e.g., login), Claude can: + +1. **Wait for user confirmation** (simple): + ``` + Claude: 请在 VNC 窗口中登录,完成后告诉我 + User: 登录好了 + Claude: 好的,继续执行... + ``` + +2. **Auto-detect via script** (advanced): + ```python + # Wait for login success indicator + page.wait_for_selector("#user-avatar", timeout=300000) # 5 min timeout + print("Login detected, continuing...") + ``` + +#### 6.7 CUI Changes + +**No CUI changes required.** The existing `navigate` action + `app/openSidebar` event already handles loading the VNC client API response in an iframe. + +## Implementation Details + +### Dockerfile.browser (browser/Dockerfile) + +```dockerfile +ARG REGISTRY=yaoapp +FROM ${REGISTRY}/sandbox-claude:latest + +USER root + +# Install X11, VNC, and minimal window manager +RUN apt-get update && apt-get install -y --no-install-recommends \ + xvfb \ + x11vnc \ + fluxbox \ + novnc \ + python3-websockify \ + fonts-liberation \ + fonts-noto-cjk \ + x11-utils \ + xdotool \ + && rm -rf /var/lib/apt/lists/* + +# Install Playwright system dependencies (requires root) +RUN npx playwright install-deps chromium firefox + +# Install Playwright and browsers as sandbox user +USER sandbox +RUN npm install -g playwright && \ + npx playwright install chromium firefox + +USER root + +# VNC startup script +COPY start-vnc.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/start-vnc.sh + +# Update entrypoint to start VNC (includes original claude entrypoint logic) +COPY entrypoint-vnc.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Environment +ENV DISPLAY=:99 +ENV VNC_PORT=5900 +ENV NOVNC_PORT=6080 +ENV RESOLUTION=1920x1080x24 +ENV SANDBOX_VNC_ENABLED=true + +EXPOSE 5900 6080 + +USER sandbox +WORKDIR /workspace + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["sleep", "infinity"] +``` + +### Dockerfile.desktop + +```dockerfile +ARG REGISTRY=yaoapp +FROM ${REGISTRY}/sandbox-claude:latest + +USER root + +# Install X11, VNC, and XFCE desktop +RUN apt-get update && apt-get install -y --no-install-recommends \ + xvfb \ + x11vnc \ + novnc \ + python3-websockify \ + # XFCE Desktop + xfce4 \ + xfce4-terminal \ + thunar \ + # Fonts + fonts-liberation \ + fonts-noto-cjk \ + # Utilities + x11-utils \ + xdotool \ + && apt-get remove -y xfce4-screensaver xscreensaver || true \ + && rm -rf /var/lib/apt/lists/* + +# Optional: Install Playwright system dependencies (requires root) +RUN npx playwright install-deps chromium || true + +# Optional: Install Playwright for browser automation +USER sandbox +RUN npm install -g playwright && \ + npx playwright install chromium || true + +USER root + +# VNC startup script +COPY start-vnc.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/start-vnc.sh + +# Update entrypoint (includes original claude entrypoint logic) +COPY entrypoint-vnc.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Environment +ENV DISPLAY=:99 +ENV VNC_PORT=5900 +ENV NOVNC_PORT=6080 +ENV RESOLUTION=1920x1080x24 +ENV SANDBOX_VNC_ENABLED=true +ENV SANDBOX_DESKTOP=xfce + +EXPOSE 5900 6080 + +USER sandbox +WORKDIR /workspace + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["sleep", "infinity"] +``` + +### start-vnc.sh (Shared) + +```bash +#!/bin/bash +set -e + +DISPLAY_NUM="${DISPLAY_NUM:-99}" +RESOLUTION="${RESOLUTION:-1920x1080x24}" +VNC_PORT="${VNC_PORT:-5900}" +NOVNC_PORT="${NOVNC_PORT:-6080}" +VNC_PASSWORD="${VNC_PASSWORD:-}" +DESKTOP="${SANDBOX_DESKTOP:-fluxbox}" + +export DISPLAY=:${DISPLAY_NUM} + +# Start Xvfb (virtual framebuffer) +echo "Starting Xvfb on display :${DISPLAY_NUM}..." +Xvfb :${DISPLAY_NUM} -screen 0 ${RESOLUTION} & +XVFB_PID=$! +sleep 1 + +if ! kill -0 $XVFB_PID 2>/dev/null; then + echo "ERROR: Xvfb failed to start" + exit 1 +fi + +# Start window manager / desktop +echo "Starting ${DESKTOP}..." +case "$DESKTOP" in + xfce|xfce4) + startxfce4 & + ;; + *) + fluxbox & + ;; +esac + +# Start VNC server +echo "Starting x11vnc on port ${VNC_PORT}..." +VNC_ARGS="-display :${DISPLAY_NUM} -forever -shared -rfbport ${VNC_PORT} -noxdamage" +if [ -n "$VNC_PASSWORD" ]; then + mkdir -p ~/.vnc + x11vnc -storepasswd "$VNC_PASSWORD" ~/.vnc/passwd + VNC_ARGS="$VNC_ARGS -rfbauth ~/.vnc/passwd" +else + VNC_ARGS="$VNC_ARGS -nopw" +fi +x11vnc $VNC_ARGS & + +# Start noVNC (websockify) +echo "Starting noVNC on port ${NOVNC_PORT}..." +websockify --web=/usr/share/novnc/ ${NOVNC_PORT} localhost:${VNC_PORT} & + +echo "VNC services started successfully" +echo " - Desktop: ${DESKTOP}" +echo " - VNC port: ${VNC_PORT}" +echo " - noVNC port: ${NOVNC_PORT}" + +# Note: Don't wait here - let the entrypoint continue +# Background processes will keep running +``` + +### entrypoint-vnc.sh + +```bash +#!/bin/bash +# Container entrypoint for VNC-enabled images +# This extends the original sandbox-claude entrypoint with VNC support + +# ============================================ +# VNC Services Startup +# ============================================ +if [ "$SANDBOX_VNC_ENABLED" = "true" ]; then + echo "Starting VNC services..." + /usr/local/bin/start-vnc.sh & + sleep 2 +fi + +# ============================================ +# Original sandbox-claude entrypoint logic +# (copied from sandbox-claude Dockerfile) +# ============================================ +WORKSPACE="${WORKSPACE:-/workspace}" +PORT="${CLAUDE_PROXY_PORT:-3456}" +ENV_FILE="/tmp/claude-proxy-env" + +# If proxy env vars are set AND proxy is not running, start it +# This supports docker run -e CLAUDE_PROXY_BACKEND=... usage +if [ -n "$CLAUDE_PROXY_BACKEND" ] && [ -n "$CLAUDE_PROXY_API_KEY" ] && [ -n "$CLAUDE_PROXY_MODEL" ]; then + if ! curl -s "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then + /usr/local/bin/start-claude-proxy + fi + + # Write env vars to a file that can be sourced + if curl -s "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then + echo "export ANTHROPIC_BASE_URL=http://127.0.0.1:${PORT}" > "$ENV_FILE" + echo "export ANTHROPIC_API_KEY=dummy" >> "$ENV_FILE" + chmod 644 "$ENV_FILE" + fi +fi + +# Execute the command passed to docker run +exec "$@" +``` + +### VNC Proxy Implementation + +```go +// sandbox/vncproxy/proxy.go +package vncproxy + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "strings" + "sync" + "time" + + "github.com/docker/docker/client" + "github.com/gorilla/websocket" +) + +type Proxy struct { + docker *client.Client + config *Config + + // IP cache with TTL support + ipCache map[string]ipCacheEntry + ipCacheMu sync.RWMutex +} + +type Config struct { + ContainerVNCPort int // default: 5900 + ContainerNoVNCPort int // default: 6080 + Timeout time.Duration // default: 30s +} + +func New(docker *client.Client, config *Config) *Proxy { + if config.ContainerVNCPort == 0 { + config.ContainerVNCPort = 5900 + } + if config.ContainerNoVNCPort == 0 { + config.ContainerNoVNCPort = 6080 + } + if config.Timeout == 0 { + config.Timeout = 30 * time.Second + } + + return &Proxy{ + docker: docker, + config: config, + ipCache: make(map[string]ipCacheEntry), + } +} + +// HandleVNCStatus returns VNC status for a container +// GET /v1/sandbox/{id}/vnc +func (p *Proxy) HandleVNCStatus(w http.ResponseWriter, r *http.Request) { + sandboxID := extractSandboxID(r) + containerName := fmt.Sprintf("yao-sandbox-%s", sandboxID) + + response := map[string]interface{}{ + "sandbox_id": sandboxID, + "container": containerName, + } + + // Check if container exists and is running + ip, err := p.getContainerIP(r.Context(), containerName) + if err != nil { + response["available"] = false + response["status"] = "unavailable" + response["message"] = "Container not available" + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // Check if VNC is enabled for this container + if !p.checkVNCEnabled(r.Context(), containerName) { + response["available"] = false + response["status"] = "not_supported" + response["message"] = "VNC not available for this container type" + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // Check if VNC services are ready (try to connect to websockify port) + if !p.checkVNCReady(r.Context(), ip) { + response["available"] = false + response["status"] = "starting" + response["message"] = "VNC services are starting..." + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // VNC is ready + response["available"] = true + response["status"] = "ready" + response["client_url"] = fmt.Sprintf("/v1/sandbox/%s/vnc/client", sandboxID) + response["websocket_url"] = fmt.Sprintf("/v1/sandbox/%s/vnc/ws", sandboxID) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// checkVNCReady tests if VNC services are ready by attempting TCP connection +func (p *Proxy) checkVNCReady(ctx context.Context, containerIP string) bool { + addr := fmt.Sprintf("%s:%d", containerIP, p.config.ContainerNoVNCPort) + conn, err := net.DialTimeout("tcp", addr, 2*time.Second) + if err != nil { + return false + } + conn.Close() + return true +} + +// HandleVNCClient serves the noVNC client page +// GET /v1/sandbox/{id}/vnc/client?viewonly=true|false +func (p *Proxy) HandleVNCClient(w http.ResponseWriter, r *http.Request) { + sandboxID := extractSandboxID(r) + containerName := fmt.Sprintf("yao-sandbox-%s", sandboxID) + + // Verify container exists, is running, and has VNC + _, err := p.getContainerIP(r.Context(), containerName) + if err != nil { + http.Error(w, "Container not available", http.StatusNotFound) + return + } + + if !p.checkVNCEnabled(r.Context(), containerName) { + http.Error(w, "VNC not available for this container", http.StatusBadRequest) + return + } + + // Get viewonly parameter (default: false = interactive) + viewOnly := r.URL.Query().Get("viewonly") == "true" + + // Serve inline noVNC HTML page with status checking + // This embeds the noVNC client directly, with retry logic for VNC startup delay + wsURL := fmt.Sprintf("/v1/sandbox/%s/vnc/ws", sandboxID) + p.serveNoVNCPage(w, sandboxID, wsURL, viewOnly) +} + +// serveNoVNCPage serves an inline HTML page that loads noVNC +// Includes status checking and retry logic for VNC startup delay +// viewOnly: if true, user can only watch; if false, user can interact with keyboard/mouse +func (p *Proxy) serveNoVNCPage(w http.ResponseWriter, sandboxID string, wsPath string, viewOnly bool) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + viewOnlyJS := "false" + if viewOnly { + viewOnlyJS = "true" + } + + html := fmt.Sprintf(` + + + Sandbox Preview + + + +
+
+
正在连接 VNC 服务...
+
+
+ + + + +`, sandboxID, wsPath, viewOnlyJS) + w.Write([]byte(html)) +} + +// HandleVNCWebSocket proxies WebSocket to container VNC +// GET /v1/sandbox/{id}/vnc/ws +func (p *Proxy) HandleVNCWebSocket(w http.ResponseWriter, r *http.Request) { + sandboxID := extractSandboxID(r) + containerName := fmt.Sprintf("yao-sandbox-%s", sandboxID) + + ip, err := p.getContainerIP(r.Context(), containerName) + if err != nil { + http.Error(w, "Container not available", http.StatusNotFound) + return + } + + if !p.checkVNCEnabled(r.Context(), containerName) { + http.Error(w, "VNC not available for this container", http.StatusBadRequest) + return + } + + // Proxy WebSocket to container's websockify port + targetURL := fmt.Sprintf("ws://%s:%d", ip, p.config.ContainerNoVNCPort) + p.proxyWebSocket(w, r, targetURL) +} + +func (p *Proxy) checkVNCEnabled(ctx context.Context, containerName string) bool { + inspect, err := p.docker.ContainerInspect(ctx, containerName) + if err != nil { + return false + } + + // Check environment variable SANDBOX_VNC_ENABLED + for _, env := range inspect.Config.Env { + if env == "SANDBOX_VNC_ENABLED=true" { + return true + } + } + return false +} + +// ipCacheEntry holds cached IP with expiration +type ipCacheEntry struct { + IP string + ExpiresAt time.Time +} + +func (p *Proxy) getContainerIP(ctx context.Context, containerName string) (string, error) { + // Check cache first (with TTL) + p.ipCacheMu.RLock() + if entry, ok := p.ipCache[containerName]; ok { + if time.Now().Before(entry.ExpiresAt) { + p.ipCacheMu.RUnlock() + return entry.IP, nil + } + } + p.ipCacheMu.RUnlock() + + // Cache miss or expired, fetch from Docker + inspect, err := p.docker.ContainerInspect(ctx, containerName) + if err != nil { + // Remove stale cache entry + p.ipCacheMu.Lock() + delete(p.ipCache, containerName) + p.ipCacheMu.Unlock() + return "", fmt.Errorf("container not found: %w", err) + } + + if !inspect.State.Running { + // Remove stale cache entry + p.ipCacheMu.Lock() + delete(p.ipCache, containerName) + p.ipCacheMu.Unlock() + return "", fmt.Errorf("container not running") + } + + ip := inspect.NetworkSettings.IPAddress + if ip == "" { + if networks := inspect.NetworkSettings.Networks; networks != nil { + if bridge, ok := networks["bridge"]; ok { + ip = bridge.IPAddress + } + } + } + + if ip == "" { + return "", fmt.Errorf("container has no IP address") + } + + // Cache with 30 second TTL + p.ipCacheMu.Lock() + p.ipCache[containerName] = ipCacheEntry{ + IP: ip, + ExpiresAt: time.Now().Add(30 * time.Second), + } + p.ipCacheMu.Unlock() + + return ip, nil +} + +// InvalidateCache removes a container from the IP cache +// Call this when container state changes (stop/restart) +func (p *Proxy) InvalidateCache(containerName string) { + p.ipCacheMu.Lock() + delete(p.ipCache, containerName) + p.ipCacheMu.Unlock() +} + +func (p *Proxy) proxyWebSocket(w http.ResponseWriter, r *http.Request, targetURL string) { + upgrader := websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, + Subprotocols: []string{"binary"}, // Required for noVNC + } + + clientConn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + defer clientConn.Close() + + dialer := websocket.Dialer{ + HandshakeTimeout: p.config.Timeout, + } + + targetConn, _, err := dialer.Dial(targetURL, nil) + if err != nil { + return + } + defer targetConn.Close() + + errChan := make(chan error, 2) + + // Client -> Target + go func() { + for { + msgType, data, err := clientConn.ReadMessage() + if err != nil { + errChan <- err + return + } + if err := targetConn.WriteMessage(msgType, data); err != nil { + errChan <- err + return + } + } + }() + + // Target -> Client + go func() { + for { + msgType, data, err := targetConn.ReadMessage() + if err != nil { + errChan <- err + return + } + if err := clientConn.WriteMessage(msgType, data); err != nil { + errChan <- err + return + } + } + }() + + <-errChan +} + +func extractSandboxID(r *http.Request) string { + // Extract from path: /v1/sandbox/{id}/vnc/... + path := r.URL.Path + path = strings.TrimPrefix(path, "/v1/sandbox/") + parts := strings.Split(path, "/") + if len(parts) >= 1 { + return parts[0] + } + return "" +} +``` + +## Security Considerations + +### 1. Authentication + +All VNC endpoints verify user authentication: + +```go +func (p *Proxy) authMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sandboxID := extractSandboxID(r) + + // Verify the requesting user owns this sandbox + // Implementation depends on how sandboxID maps to users: + // - If sandboxID = "{userID}-{chatID}", extract userID and compare with session + // - If sandboxID = UUID, lookup in database + // - Delegate to business layer authorization service + + // Example: extract userID from sandboxID pattern "{userID}-{chatID}" + // parts := strings.SplitN(sandboxID, "-", 2) + // if len(parts) >= 1 { + // ownerID := parts[0] + // sessionUserID := getSessionUserID(r) + // if sessionUserID != ownerID { + // http.Error(w, "Unauthorized", http.StatusUnauthorized) + // return + // } + // } + + // TODO: Implement authorization logic based on your sandboxID scheme + + next.ServeHTTP(w, r) + }) +} +``` + +### 2. Network Isolation + +- Containers use Docker bridge network (internal only) +- No VNC ports exposed to host +- All access through authenticated proxy +- Each user can only access their own containers + +### 3. Resource Limits by Image Type + +| Image Type | Memory | CPU | Disk | +|------------|--------|-----|------| +| claude | 2GB | 1.0 | - | +| playwright | 4GB | 2.0 | - | +| desktop | 4GB | 2.0 | - | + +## Configuration + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `YAO_SANDBOX_IMAGE` | `yaoapp/sandbox-claude:latest` | Default sandbox image | +| `YAO_SANDBOX_VNC_PORT_MAPPING` | `false` | Enable VNC port mapping to host (for Docker Desktop) | +| `YAO_VNC_PROXY_ENABLED` | `true` | Enable VNC proxy | +| `YAO_VNC_RESOLUTION` | `1920x1080x24` | VNC screen resolution | + +### Docker Desktop Support (macOS/Windows) + +Docker Desktop runs containers inside a LinuxKit VM, so container IPs (`172.17.0.x`) are not directly accessible from the host. To enable VNC access on Docker Desktop: + +```bash +# Enable VNC port mapping for local development +export YAO_SANDBOX_VNC_PORT_MAPPING=true +export YAO_SANDBOX_IMAGE="yaoapp/sandbox-claude-browser:latest" +``` + +When `YAO_SANDBOX_VNC_PORT_MAPPING=true`: +- Container ports `6080/tcp` (noVNC) and `5900/tcp` (VNC) are mapped to random available host ports +- Ports are bound to `127.0.0.1` for security +- VNC Proxy automatically detects and uses the mapped host ports + +On Linux (native Docker), this option is not needed as container IPs are directly accessible. + +## Implementation Checklist + +### Yao Backend ✅ 完成 + +- [x] `sandbox/docker/browser/Dockerfile` - Browser + VNC image +- [x] `sandbox/docker/desktop/Dockerfile` - Full desktop + VNC image +- [x] `sandbox/docker/vnc/start-vnc.sh` - Shared VNC startup script +- [x] `sandbox/docker/vnc/entrypoint-vnc.sh` - VNC entrypoint +- [x] `sandbox/vncproxy/proxy.go` - VNC WebSocket proxy +- [x] `sandbox/vncproxy/config.go` - Proxy configuration +- [x] API router integration - VNC endpoints (`openapi/sandbox/sandbox.go`) +- [x] `sandbox/docker/build.sh` - Update build script +- [x] `sandbox/config.go` - VNC port mapping configuration +- [x] `sandbox/manager.go` - Dynamic VNC port mapping for Docker Desktop + +### No Changes Needed + +- `agent/sandbox/` - existing `Image` field already supports custom images +- `cui/` - existing `navigate` action handles iframe loading via `app/openSidebar` + +## File Structure + +### Yao (Backend) + +``` +yao/sandbox/ +├── vncproxy/ # VNC Proxy Service +│ ├── proxy.go # Main proxy implementation (with port mapping detection) +│ ├── proxy_test.go # Unit tests +│ └── config.go # Configuration +├── docker/ +│ ├── base/ +│ │ └── Dockerfile.base +│ ├── claude/ +│ │ ├── Dockerfile +│ │ └── Dockerfile.full +│ ├── browser/ # Browser + VNC image +│ │ └── Dockerfile +│ ├── desktop/ # XFCE Desktop + VNC image +│ │ └── Dockerfile +│ ├── vnc/ # Shared VNC scripts +│ │ ├── start-vnc.sh +│ │ └── entrypoint-vnc.sh +│ └── build.sh # Build script for all images +├── manager.go # Container management (with VNC port mapping) +├── config.go # Configuration (VNCPortMapping option) +├── DESIGN-PLAYWRIGHT-VNC.md # This document +├── TODO-VNC.md # Implementation checklist +└── README.md # Quick start guide +``` + +### CUI (Frontend) + +``` +cui/packages/cui/ +└── ... # No changes needed +``` + +The CUI `navigate` action already supports loading URLs via iframe in sidebar. The `/v1/sandbox/{id}/vnc/client` API returns a complete HTML page that will be loaded directly. + +### Agent (No Changes) + +``` +yao/agent/ +├── sandbox/ +│ ├── types.go # Already supports custom Image +│ └── ... # No changes needed +└── ... +``` + +## Command Execution + +### Overview + +Commands execute identically across all sandbox images. The `Manager.Exec()` and `Manager.Stream()` methods remain unchanged. + +### No Manager Changes Required + +```go +// Manager.Exec() and Manager.Stream() remain unchanged +// Commands run the same way on all images +// DISPLAY=:99 is set in container env, GUI apps (browsers) use it automatically +``` + +### Behavior by Image Type + +| Image | DISPLAY | VNC Visible | Agent Gets Output | +|-------|---------|-------------|-------------------| +| sandbox-claude | ❌ | N/A | ✅ | +| sandbox-claude-browser | ✅ :99 | Browser window | ✅ | +| sandbox-claude-desktop | ✅ :99 | Browser + Desktop apps | ✅ | + +### What Users See in VNC + +| Operation | sandbox-claude-browser | sandbox-claude-desktop | +|-----------|--------------------------|------------------------| +| Browser automation | ✅ Visible | ✅ Visible | +| File operations | ❌ | ✅ (open Thunar) | +| Terminal commands | ❌ | ❌ (output to Agent) | + +**Note**: Terminal command output goes to Agent, not to VNC terminal window. This is by design - `docker exec` runs commands directly in the container, not through a terminal emulator. Users can manually open a terminal in VNC if they want to run commands interactively. + +### Why This Design + +1. **100% backward compatible**: No changes to Manager.go +2. **Agent output intact**: stdout/stderr captured normally +3. **Browser visible**: Main use case (Playwright) works perfectly +4. **Low risk**: No code changes = no bugs +5. **Future improvement**: Terminal visibility can be added later if needed + +--- + +## Appendix + +### A. Image Comparison + +| Feature | sandbox-claude | sandbox-claude-browser | sandbox-claude-desktop | +|---------|---------------|--------------------------|------------------------| +| Claude CLI | ✅ | ✅ | ✅ | +| Node.js | ✅ | ✅ | ✅ | +| Python | ✅ | ✅ | ✅ | +| VNC Access | ❌ | ✅ | ✅ | +| Playwright | ❌ | ✅ | ✅ (optional) | +| File Manager | ❌ | ❌ | ✅ | +| Terminal GUI | ❌ | ❌ | ✅ | +| Desktop | ❌ | Minimal (Fluxbox) | Full (XFCE) | +| Image Size | ~700MB | ~1.8GB | ~2.5GB | +| Memory | 2GB | 4GB | 4GB | +| **Best For** | Scripts, CLI | Browser automation | Full transparency | + +### B. User Visibility & Interaction + +What users can see and do in VNC: + +| Operation | sandbox-claude-browser | sandbox-claude-desktop | +|-----------|--------------------------|------------------------| +| Browser navigation | ✅ See | ✅ See | +| Browser clicks/typing | ✅ See | ✅ See | +| File creation | ❌ (log only) | ✅ (file manager) | +| Command execution | ❌ (output to Agent) | ❌ (output to Agent) | +| Code editing | ❌ | ✅ (if editor installed) | +| **Trust Level** | Medium | High | + +**User Interaction Modes**: + +| Mode | URL Parameter | User Can | +|------|---------------|----------| +| View-only | `?viewonly=true` | Watch only | +| Interactive | (default) | Keyboard, mouse, typing | + +**Typical Interactive Scenarios**: +- User login (accounts, passwords) +- CAPTCHA solving +- Two-factor authentication +- Manual form filling + +**Note**: Command output goes to Agent (via docker exec), not to a visible terminal in VNC. Users can manually open a terminal in `sandbox-claude-desktop` if needed. + +### C. Implementation Summary + +| Component | Location | Changes | +|-----------|----------|---------| +| Docker Images | `sandbox/docker/browser/`, `sandbox/docker/desktop/` | NEW | +| VNC Proxy | `sandbox/vncproxy/` | NEW | +| VNC API | Yao router | NEW endpoints | +| CUI | `cui/` | **No changes** (navigate action + iframe) | +| Sandbox Manager | `sandbox/manager.go` | **No changes** | +| Agent Sandbox | `agent/sandbox/` | **No changes** | + +### D. References + +- [Playwright Docker Documentation](https://playwright.dev/docs/docker) +- [noVNC GitHub](https://github.com/novnc/noVNC) +- [XFCE Documentation](https://docs.xfce.org/) +- [CUI Action System](../../cui/packages/cui/chatbox/messages/Action/actions/navigate.ts) +- [Yao Sandbox README](./README.md) +- [Yao Sandbox DESIGN](./DESIGN.md) diff --git a/sandbox/README.md b/sandbox/README.md index b2a7ca05..79c61fca 100644 --- a/sandbox/README.md +++ b/sandbox/README.md @@ -10,6 +10,7 @@ The sandbox module enables Yao to safely run external AI coding agents (like Cla - IPC communication via Unix sockets - Resource limits (CPU, memory) - Security isolation +- **VNC remote desktop** for visual transparency (optional) ## Architecture @@ -26,11 +27,20 @@ The sandbox module enables Yao to safely run external AI coding agents (like Cla │ │ │ │ │ └────────────────────────┬────────────────────────────────┘ │ │ │ │ +│ ┌────────────────────────┴────────────────────────────────┐ │ +│ │ VNC Proxy Service │ │ +│ │ │ │ +│ │ - GET /v1/sandbox/{id}/vnc → VNC status │ │ +│ │ - GET /v1/sandbox/{id}/vnc/client → noVNC page │ │ +│ │ - GET /v1/sandbox/{id}/vnc/ws → WebSocket proxy │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ │ ┌───────────────┼───────────────┐ │ │ ▼ ▼ ▼ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ -│ │ Container │ │ Container │ │ Container │ │ -│ │ (user1) │ │ (user2) │ │ (user3) │ │ +│ │ sandbox- │ │ sandbox- │ │ sandbox- │ │ +│ │ claude │ │ playwright │ │ desktop │ │ +│ │ (No VNC) │ │ (VNC) │ │ (VNC) │ │ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ │ │ │ │ │ ──────┴───────────────┴───────────────┴──── │ @@ -45,7 +55,16 @@ The sandbox module enables Yao to safely run external AI coding agents (like Cla ```bash cd sandbox/docker + +# Build base image ./build.sh claude + +# Build VNC-enabled images +./build.sh browser # Browser (Playwright) + Fluxbox + VNC +./build.sh desktop # XFCE Desktop + VNC + +# Build all images +./build.sh all ``` ### Usage @@ -84,23 +103,37 @@ data, err := manager.ReadFile(ctx, container.Name, "/workspace/test.txt") ### Environment Variables -| Variable | Default | Description | -| -------------------------- | ----------------------------------- | ------------------------- | -| `YAO_SANDBOX_IMAGE` | `yao/sandbox-claude:latest` | Docker image | -| `YAO_SANDBOX_WORKSPACE` | `{YAO_DATA_ROOT}/sandbox/workspace` | Workspace directory | -| `YAO_SANDBOX_IPC` | `{YAO_DATA_ROOT}/sandbox/ipc` | IPC socket directory | -| `YAO_SANDBOX_MAX` | `100` | Max concurrent containers | -| `YAO_SANDBOX_IDLE_TIMEOUT` | `30m` | Idle timeout | -| `YAO_SANDBOX_MEMORY` | `2g` | Memory limit | -| `YAO_SANDBOX_CPU` | `1.0` | CPU limit | +| Variable | Default | Description | +| ------------------------------ | ----------------------------------- | ---------------------------------------------- | +| `YAO_SANDBOX_IMAGE` | `yao/sandbox-claude:latest` | Docker image | +| `YAO_SANDBOX_WORKSPACE` | `{YAO_DATA_ROOT}/sandbox/workspace` | Workspace directory | +| `YAO_SANDBOX_IPC` | `{YAO_DATA_ROOT}/sandbox/ipc` | IPC socket directory | +| `YAO_SANDBOX_MAX` | `100` | Max concurrent containers | +| `YAO_SANDBOX_IDLE_TIMEOUT` | `30m` | Idle timeout | +| `YAO_SANDBOX_MEMORY` | `2g` | Memory limit | +| `YAO_SANDBOX_CPU` | `1.0` | CPU limit | +| `YAO_SANDBOX_VNC_PORT_MAPPING` | `false` | Enable VNC port mapping (for Docker Desktop) | + +### Docker Desktop (macOS/Windows) + +Docker Desktop runs containers in a LinuxKit VM, so container IPs are not directly accessible from the host. Enable VNC port mapping for local development: + +```bash +export YAO_SANDBOX_VNC_PORT_MAPPING=true +export YAO_SANDBOX_IMAGE="yaoapp/sandbox-claude-browser:latest" +``` + +When enabled, VNC ports (6080, 5900) are automatically mapped to random available host ports on `127.0.0.1`. ## Docker Images -| Image | Description | -| --------------------------- | ------------------------------------- | -| `yao/sandbox-base:latest` | Base image with git, curl, yao-bridge | -| `yao/sandbox-claude:latest` | + Claude CLI, Node.js 20, Python 3.11 | -| `yao/sandbox-claude:full` | + Go 1.23 | +| Image | VNC | Description | +| ------------------------------------------ | --- | ------------------------------------- | +| `yaoapp/sandbox-base:latest` | ❌ | Base image with git, curl, yao-bridge | +| `yaoapp/sandbox-claude:latest` | ❌ | + Claude CLI, Node.js 20, Python 3.11 | +| `yaoapp/sandbox-claude:full` | ❌ | + Go 1.23 | +| `yaoapp/sandbox-claude-browser:latest` | ✅ | + Playwright, Fluxbox, VNC (~3.4GB) | +| `yaoapp/sandbox-claude-desktop:latest` | ✅ | + XFCE Desktop, VNC (~3.1GB) | ## IPC Communication @@ -112,6 +145,25 @@ Supported methods: - `tools/list` - List available tools - `tools/call` - Execute a tool +## VNC Remote Desktop + +VNC-enabled images (playwright, desktop) provide real-time visibility into Claude's operations. + +### API Endpoints + +| Endpoint | Description | +| ------------------------------- | ---------------------------------- | +| `GET /v1/sandbox/{id}/vnc` | VNC status (ready/starting/unavailable) | +| `GET /v1/sandbox/{id}/vnc/client` | noVNC HTML client page | +| `GET /v1/sandbox/{id}/vnc/ws` | WebSocket proxy to container VNC | + +### View Modes + +- **Interactive** (default): User can use keyboard and mouse +- **View-only** (`?viewonly=true`): User can only watch + +For detailed design, see [DESIGN-PLAYWRIGHT-VNC.md](./DESIGN-PLAYWRIGHT-VNC.md). + ## Directory Structure ``` @@ -120,11 +172,18 @@ sandbox/ ├── docker/ # Dockerfiles and build script │ ├── base/ │ ├── claude/ +│ ├── browser/ # Browser (Playwright) + VNC image +│ ├── desktop/ # XFCE Desktop + VNC image +│ ├── vnc/ # Shared VNC scripts │ └── build.sh ├── ipc/ # IPC system │ ├── manager.go │ ├── session.go │ └── types.go +├── vncproxy/ # VNC proxy service +│ ├── proxy.go +│ ├── config.go +│ └── proxy_test.go ├── config.go # Configuration ├── errors.go # Error types ├── helpers.go # Helper functions @@ -135,11 +194,17 @@ sandbox/ ## Testing ```bash +# Load environment variables first +source env.local.sh + # Unit tests (no Docker required) go test -v ./sandbox/... -run "^Test.*Validation|^Test.*Generation|^Test.*Parsing" # All tests (requires Docker) go test -v ./sandbox/... + +# VNC proxy tests only +go test -v ./sandbox/vncproxy/... ``` ## Security diff --git a/sandbox/config.go b/sandbox/config.go index f6335c5e..ccb653e9 100644 --- a/sandbox/config.go +++ b/sandbox/config.go @@ -21,6 +21,9 @@ type Config struct { ContainerWorkDir string `json:"container_workdir,omitempty"` // Container working directory, default: /workspace ContainerIPCSocket string `json:"container_ipc_socket,omitempty"` // Container IPC socket path, default: /tmp/yao.sock ContainerUser string `json:"container_user,omitempty"` // Container user, default: "" (use image default). Set to "0" for root. + + // VNC port mapping (for Docker Desktop on macOS/Windows where container IPs are not directly accessible) + VNCPortMapping bool `json:"vnc_port_mapping,omitempty"` // Enable VNC port mapping to host, default: false } // DefaultConfig returns a Config with default values @@ -116,4 +119,9 @@ func (c *Config) Init(dataRoot string) { if env := os.Getenv("YAO_SANDBOX_CONTAINER_USER"); env != "" { c.ContainerUser = env } + + // VNC port mapping (for Docker Desktop on macOS/Windows) + if env := os.Getenv("YAO_SANDBOX_VNC_PORT_MAPPING"); env != "" { + c.VNCPortMapping = env == "true" || env == "1" || env == "yes" + } } diff --git a/sandbox/docker/browser/Dockerfile b/sandbox/docker/browser/Dockerfile new file mode 100644 index 00000000..eace8b69 --- /dev/null +++ b/sandbox/docker/browser/Dockerfile @@ -0,0 +1,104 @@ +# Claude sandbox with browser automation + VNC preview +# Image: sandbox-claude-browser +# Base: sandbox-claude (Ubuntu 24.04 + Node.js + Python + Claude CLI) +# Adds: Xvfb + x11vnc + noVNC + Fluxbox + Playwright/Puppeteer browsers +# +# Lightweight browser environment for web automation tasks +# Supports both amd64 and arm64 architectures + +ARG REGISTRY=yaoapp +FROM ${REGISTRY}/sandbox-claude:latest + +USER root + +# Use MIT mirror (USA) for ARM64 +RUN sed -i 's|http://ports.ubuntu.com/ubuntu-ports|http://mirrors.mit.edu/ubuntu-ports|g' /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || \ + sed -i 's|http://ports.ubuntu.com/ubuntu-ports|http://mirrors.mit.edu/ubuntu-ports|g' /etc/apt/sources.list 2>/dev/null || true + +# Install X11, VNC, and minimal window manager +RUN apt-get update && apt-get install -y --no-install-recommends \ + # Sudo for sandbox user + sudo \ + # Virtual display + xvfb \ + # VNC server + x11vnc \ + # noVNC (HTML5 VNC client) and websockify + novnc \ + python3-websockify \ + # Minimal window manager (lightweight, perfect for Playwright) + fluxbox \ + # Background/wallpaper utilities + feh \ + imagemagick \ + # Fonts (required for proper browser rendering) + fonts-liberation \ + fonts-noto-cjk \ + fonts-noto-color-emoji \ + # X11 utilities + x11-utils \ + xdotool \ + # Audio (for video playback in browsers, can be disabled) + pulseaudio \ + && rm -rf /var/lib/apt/lists/* + +# Configure passwordless sudo for sandbox user +RUN echo "sandbox ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/sandbox && \ + chmod 0440 /etc/sudoers.d/sandbox + +# Install Playwright system dependencies (requires root) +# This installs system libraries needed by Chromium/Firefox +RUN npx playwright install-deps chromium firefox || true + +# Install Playwright and browsers as sandbox user +USER sandbox + +# Install Playwright for Node.js (global) and Python +RUN npm install -g playwright && \ + pip install --user --break-system-packages playwright && \ + npx playwright install chromium firefox + +USER root + +# Create directories for branding assets +RUN mkdir -p /usr/local/share/yao + +# Copy VNC startup scripts and branding assets +# Note: Build context should be sandbox/docker/, so paths are relative to that +COPY vnc/start-vnc.sh /usr/local/bin/start-vnc.sh +COPY vnc/entrypoint-vnc.sh /usr/local/bin/entrypoint.sh +COPY browser/config/setup-fluxbox.sh /usr/local/bin/setup-fluxbox.sh +COPY browser/config/yao-logo.png /usr/local/share/yao/yao-logo.png +RUN chmod +x /usr/local/bin/start-vnc.sh /usr/local/bin/entrypoint.sh /usr/local/bin/setup-fluxbox.sh + +# Environment variables for VNC +ENV DISPLAY=:99 +ENV VNC_PORT=5900 +ENV NOVNC_PORT=6080 +ENV RESOLUTION=1920x1080x24 +ENV SANDBOX_VNC_ENABLED=true +ENV SANDBOX_DESKTOP=fluxbox + +# Node.js environment - ensure global modules are accessible +ENV NODE_PATH=/home/sandbox/.npm-global/lib/node_modules + +# Expose VNC ports (internal use only, accessed via proxy) +EXPOSE 5900 6080 + +USER sandbox +WORKDIR /workspace + +# Verify installations +RUN echo "=== Verifying installations ===" && \ + node --version && \ + npm --version && \ + python3 --version && \ + npx playwright --version && \ + python3 -c "from playwright.sync_api import sync_playwright; print('Python Playwright: OK')" && \ + which fluxbox && \ + which x11vnc && \ + which Xvfb && \ + echo "=== All installations verified ===" + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["sleep", "infinity"] diff --git a/sandbox/docker/browser/config/yao-logo.png b/sandbox/docker/browser/config/yao-logo.png new file mode 100644 index 00000000..de7e59fb Binary files /dev/null and b/sandbox/docker/browser/config/yao-logo.png differ diff --git a/sandbox/docker/build.sh b/sandbox/docker/build.sh index af99e3bd..8b727582 100755 --- a/sandbox/docker/build.sh +++ b/sandbox/docker/build.sh @@ -105,6 +105,22 @@ case $TOOL in build_multiarch "sandbox-claude" "claude/Dockerfile" "$PUSH" build_multiarch "sandbox-claude-full" "claude/Dockerfile.full" "$PUSH" ;; + claude-vnc) + echo "" + echo "=== Building Claude VNC images (Browser + Desktop) ===" + build_multiarch "sandbox-claude-browser" "browser/Dockerfile" "$PUSH" + build_multiarch "sandbox-claude-desktop" "desktop/Dockerfile" "$PUSH" + ;; + browser) + echo "" + echo "=== Building Claude Browser image ===" + build_multiarch "sandbox-claude-browser" "browser/Dockerfile" "$PUSH" + ;; + desktop) + echo "" + echo "=== Building Claude Desktop image ===" + build_multiarch "sandbox-claude-desktop" "desktop/Dockerfile" "$PUSH" + ;; cursor) echo "" echo "=== Building Cursor images ===" @@ -116,14 +132,20 @@ case $TOOL in # Claude build_multiarch "sandbox-claude" "claude/Dockerfile" "$PUSH" build_multiarch "sandbox-claude-full" "claude/Dockerfile.full" "$PUSH" + # Claude VNC variants + build_multiarch "sandbox-claude-browser" "browser/Dockerfile" "$PUSH" + build_multiarch "sandbox-claude-desktop" "desktop/Dockerfile" "$PUSH" # Cursor (uncomment when ready) # build_multiarch "sandbox-cursor" "cursor/Dockerfile" "$PUSH" ;; *) echo "Unknown tool: $TOOL" - echo "Usage: $0 [claude|cursor|all] [true|false]" + echo "Usage: $0 [claude|claude-vnc|browser|desktop|cursor|all] [true|false]" echo " $0 claude # Build Claude images locally" echo " $0 claude true # Build and push Claude images" + echo " $0 claude-vnc # Build Claude VNC images (Browser + Desktop)" + echo " $0 browser # Build Claude Browser image only" + echo " $0 desktop # Build Claude Desktop image only" echo " $0 all true # Build and push all images" exit 1 ;; @@ -142,9 +164,21 @@ if [ "$PUSH" = "true" ]; then echo " - ${REGISTRY}/sandbox-claude:latest" echo " - ${REGISTRY}/sandbox-claude-full:latest" ;; + claude-vnc) + echo " - ${REGISTRY}/sandbox-claude-browser:latest" + echo " - ${REGISTRY}/sandbox-claude-desktop:latest" + ;; + browser) + echo " - ${REGISTRY}/sandbox-claude-browser:latest" + ;; + desktop) + echo " - ${REGISTRY}/sandbox-claude-desktop:latest" + ;; all) echo " - ${REGISTRY}/sandbox-claude:latest" echo " - ${REGISTRY}/sandbox-claude-full:latest" + echo " - ${REGISTRY}/sandbox-claude-browser:latest" + echo " - ${REGISTRY}/sandbox-claude-desktop:latest" ;; esac fi diff --git a/sandbox/docker/desktop/Dockerfile b/sandbox/docker/desktop/Dockerfile new file mode 100644 index 00000000..aaa26c87 --- /dev/null +++ b/sandbox/docker/desktop/Dockerfile @@ -0,0 +1,115 @@ +# Claude sandbox with full XFCE desktop + VNC preview +# Image: sandbox-claude-desktop +# Base: sandbox-claude (Ubuntu 24.04 + Node.js + Python + Claude CLI) +# Adds: Xvfb + x11vnc + noVNC + XFCE desktop + File Manager + Terminal +# +# Supports both amd64 and arm64 architectures + +ARG REGISTRY=yaoapp +FROM ${REGISTRY}/sandbox-claude:latest + +USER root + +# Use MIT mirror (USA) for ARM64 +RUN sed -i 's|http://ports.ubuntu.com/ubuntu-ports|http://mirrors.mit.edu/ubuntu-ports|g' /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || \ + sed -i 's|http://ports.ubuntu.com/ubuntu-ports|http://mirrors.mit.edu/ubuntu-ports|g' /etc/apt/sources.list 2>/dev/null || true + +# Install X11, VNC, and XFCE desktop environment +RUN apt-get update && apt-get install -y --no-install-recommends \ + # Sudo for sandbox user + sudo \ + # Virtual display + xvfb \ + # VNC server + x11vnc \ + # noVNC (HTML5 VNC client) and websockify + novnc \ + python3-websockify \ + # D-Bus (required for XFCE) + dbus-x11 \ + # XFCE Desktop (full-featured but lightweight) + xfce4 \ + xfce4-terminal \ + thunar \ + # Fonts (required for proper rendering) + fonts-liberation \ + fonts-noto-cjk \ + fonts-noto-color-emoji \ + # X11 utilities + x11-utils \ + xdotool \ + # Audio + pulseaudio \ + # Remove screensaver (causes issues in container) + && apt-get remove -y xfce4-screensaver xscreensaver || true \ + && rm -rf /var/lib/apt/lists/* + +# Configure passwordless sudo for sandbox user +RUN echo "sandbox ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/sandbox && \ + chmod 0440 /etc/sudoers.d/sandbox + +# Create chromium wrapper script (uses Playwright's Chromium, starts maximized) +RUN echo '#!/bin/bash\nexec /home/sandbox/.cache/ms-playwright/chromium-1208/chrome-linux/chrome --no-sandbox --start-maximized "$@"' > /usr/local/bin/chromium && \ + chmod +x /usr/local/bin/chromium + +# Optional: Install Playwright system dependencies (requires root) +# Users can run browser automation in desktop mode too +RUN npx playwright install-deps chromium || true + +# Optional: Install Playwright for browser automation +USER sandbox +RUN npm install -g playwright && \ + pip install --user --break-system-packages playwright && \ + npx playwright install chromium || true + +USER root + +# Copy VNC startup scripts +# Note: Build context should be sandbox/docker/, so paths are relative to that +COPY vnc/start-vnc.sh /usr/local/bin/start-vnc.sh +COPY vnc/entrypoint-vnc.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/start-vnc.sh /usr/local/bin/entrypoint.sh + +# Copy Yao branding assets +RUN mkdir -p /usr/share/yao +COPY desktop/config/yao-logo-48.png /usr/share/yao/yao-logo-48.png +COPY desktop/config/yao-logo-128.png /usr/share/yao/yao-logo-128.png +COPY desktop/config/yao-logo-256.png /usr/share/yao/yao-logo-256.png +COPY desktop/config/panel-launcher-chromium.desktop /usr/share/yao/panel-launcher-chromium.desktop +COPY desktop/config/workspace.desktop /usr/share/yao/workspace.desktop +COPY desktop/config/setup-xfce.sh /usr/local/bin/setup-xfce.sh +RUN chmod +x /usr/local/bin/setup-xfce.sh + +# Environment variables for VNC +ENV DISPLAY=:99 +ENV VNC_PORT=5900 +ENV NOVNC_PORT=6080 +ENV RESOLUTION=1920x1080x24 +ENV SANDBOX_VNC_ENABLED=true +ENV SANDBOX_DESKTOP=xfce +# Set hostname for XFCE panel display +ENV HOSTNAME="Yao Sandbox" + +# Node.js environment - ensure global modules are accessible +ENV NODE_PATH=/home/sandbox/.npm-global/lib/node_modules + +# Expose VNC ports (internal use only, accessed via proxy) +EXPOSE 5900 6080 + +USER sandbox +WORKDIR /workspace + +# Verify installations +RUN echo "=== Verifying installations ===" && \ + node --version && \ + npm --version && \ + python3 --version && \ + which startxfce4 && \ + which thunar && \ + which xfce4-terminal && \ + which x11vnc && \ + which Xvfb && \ + echo "=== All installations verified ===" + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["sleep", "infinity"] diff --git a/sandbox/docker/desktop/config/panel-launcher-chromium.desktop b/sandbox/docker/desktop/config/panel-launcher-chromium.desktop new file mode 100644 index 00000000..f392c51e --- /dev/null +++ b/sandbox/docker/desktop/config/panel-launcher-chromium.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=Chromium +Comment=Access the Internet +GenericName=Web Browser +Exec=/usr/local/bin/chromium %U +Icon=org.xfce.webbrowser +Terminal=false +Categories=Network;WebBrowser; +MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/vnd.mozilla.xul+xml;application/rss+xml;application/rdf+xml;x-scheme-handler/http;x-scheme-handler/https; +StartupNotify=true diff --git a/sandbox/docker/desktop/config/setup-xfce.sh b/sandbox/docker/desktop/config/setup-xfce.sh new file mode 100644 index 00000000..75015a5a --- /dev/null +++ b/sandbox/docker/desktop/config/setup-xfce.sh @@ -0,0 +1,160 @@ +#!/bin/bash +# XFCE desktop configuration script +# Runs on container startup to set up Yao branding and default applications + +set -e + +XFCE_CONFIG_DIR="$HOME/.config/xfce4" +XFDESKTOP_DIR="$HOME/.config/xfce4/xfconf/xfce-perchannel-xml" +ICONS_DIR="$HOME/.local/share/icons/hicolor" +APPS_DIR="$HOME/.local/share/applications" +DESKTOP_DIR="$HOME/Desktop" + +# Create necessary directories +mkdir -p "$XFCE_CONFIG_DIR/panel" +mkdir -p "$XFDESKTOP_DIR" +mkdir -p "$ICONS_DIR/48x48/apps" +mkdir -p "$ICONS_DIR/128x128/apps" +mkdir -p "$ICONS_DIR/256x256/apps" +mkdir -p "$APPS_DIR" +mkdir -p "$DESKTOP_DIR" + +# Copy Yao logo to user icons directory +if [ -f /usr/share/yao/yao-logo-48.png ]; then + cp /usr/share/yao/yao-logo-48.png "$ICONS_DIR/48x48/apps/yao.png" + cp /usr/share/yao/yao-logo-128.png "$ICONS_DIR/128x128/apps/yao.png" + cp /usr/share/yao/yao-logo-256.png "$ICONS_DIR/256x256/apps/yao.png" + # Also copy to system location for panel icon + sudo cp /usr/share/yao/yao-logo-48.png /usr/share/pixmaps/yao.png 2>/dev/null || true + gtk-update-icon-cache "$ICONS_DIR" 2>/dev/null || true +fi + +# Copy Chromium launcher to applications +if [ -f /usr/share/yao/panel-launcher-chromium.desktop ]; then + cp /usr/share/yao/panel-launcher-chromium.desktop "$APPS_DIR/chromium-browser.desktop" +fi + +# Configure xfdesktop - hide default icons (File System, Home, Trash), keep only custom shortcuts +cat > "$XFDESKTOP_DIR/xfce4-desktop.xml" << 'XMLEOF' + + + + + + + + + + + + +XMLEOF + +# Copy workspace shortcut to desktop (named "Workspace" with folder icon) +if [ -f /usr/share/yao/workspace.desktop ]; then + cp /usr/share/yao/workspace.desktop "$DESKTOP_DIR/workspace.desktop" + chmod +x "$DESKTOP_DIR/workspace.desktop" +fi + +# Set Chromium as default browser +xdg-settings set default-web-browser chromium-browser.desktop 2>/dev/null || true + +# Configure XFCE panel - set Applications menu icon to Yao logo +# This will be applied when xfce4-panel starts +cat > "$XFDESKTOP_DIR/xfce4-panel.xml" << 'XMLEOF' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +XMLEOF + +echo "[XFCE Setup] Configuration complete" diff --git a/sandbox/docker/desktop/config/workspace.desktop b/sandbox/docker/desktop/config/workspace.desktop new file mode 100644 index 00000000..dbb5ebdb --- /dev/null +++ b/sandbox/docker/desktop/config/workspace.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=Workspace +Comment=Open Workspace folder +Icon=folder +Exec=thunar /workspace +Terminal=false +Categories=System;FileManager; diff --git a/sandbox/docker/desktop/config/yao-logo-128.png b/sandbox/docker/desktop/config/yao-logo-128.png new file mode 100644 index 00000000..d86a79ac Binary files /dev/null and b/sandbox/docker/desktop/config/yao-logo-128.png differ diff --git a/sandbox/docker/desktop/config/yao-logo-256.png b/sandbox/docker/desktop/config/yao-logo-256.png new file mode 100644 index 00000000..9ec6ffa7 Binary files /dev/null and b/sandbox/docker/desktop/config/yao-logo-256.png differ diff --git a/sandbox/docker/desktop/config/yao-logo-48.png b/sandbox/docker/desktop/config/yao-logo-48.png new file mode 100644 index 00000000..52c77970 Binary files /dev/null and b/sandbox/docker/desktop/config/yao-logo-48.png differ diff --git a/sandbox/docker/desktop/config/yao-logo.svg b/sandbox/docker/desktop/config/yao-logo.svg new file mode 100644 index 00000000..18f83e90 --- /dev/null +++ b/sandbox/docker/desktop/config/yao-logo.svg @@ -0,0 +1,16 @@ + + + Yaobots + + + + + + + + + + + + + \ No newline at end of file diff --git a/sandbox/docker/vnc/entrypoint-vnc.sh b/sandbox/docker/vnc/entrypoint-vnc.sh new file mode 100644 index 00000000..62e48f50 --- /dev/null +++ b/sandbox/docker/vnc/entrypoint-vnc.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Container entrypoint for VNC-enabled sandbox images +# This extends the original sandbox-claude entrypoint with VNC support + +# ============================================ +# VNC Services Startup +# ============================================ +if [ "$SANDBOX_VNC_ENABLED" = "true" ]; then + echo "[Entrypoint] Starting VNC services..." + /usr/local/bin/start-vnc.sh & + # Wait for VNC to initialize + sleep 3 + echo "[Entrypoint] VNC services started in background" +fi + +# ============================================ +# Original sandbox-claude entrypoint logic +# (from sandbox-claude Dockerfile) +# ============================================ +WORKSPACE="${WORKSPACE:-/workspace}" +PORT="${CLAUDE_PROXY_PORT:-3456}" +ENV_FILE="/tmp/claude-proxy-env" + +# If proxy env vars are set AND proxy is not running, start it +# This supports docker run -e CLAUDE_PROXY_BACKEND=... usage +if [ -n "$CLAUDE_PROXY_BACKEND" ] && [ -n "$CLAUDE_PROXY_API_KEY" ] && [ -n "$CLAUDE_PROXY_MODEL" ]; then + if ! curl -s "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then + /usr/local/bin/start-claude-proxy + fi + + # Write env vars to a file that can be sourced + if curl -s "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then + echo "export ANTHROPIC_BASE_URL=http://127.0.0.1:${PORT}" > "$ENV_FILE" + echo "export ANTHROPIC_API_KEY=dummy" >> "$ENV_FILE" + chmod 644 "$ENV_FILE" + fi +fi + +# Execute the command passed to docker run +exec "$@" diff --git a/sandbox/docker/vnc/start-vnc.sh b/sandbox/docker/vnc/start-vnc.sh new file mode 100644 index 00000000..873c6c8a --- /dev/null +++ b/sandbox/docker/vnc/start-vnc.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# VNC services startup script +# Shared by sandbox-claude-browser and sandbox-claude-desktop +# Starts: Xvfb (virtual display) + Window Manager + x11vnc + websockify (noVNC) + +set -e + +DISPLAY_NUM="${DISPLAY_NUM:-99}" +RESOLUTION="${RESOLUTION:-1920x1080x24}" +VNC_PORT="${VNC_PORT:-5900}" +NOVNC_PORT="${NOVNC_PORT:-6080}" +VNC_PASSWORD="${VNC_PASSWORD:-}" +DESKTOP="${SANDBOX_DESKTOP:-fluxbox}" + +export DISPLAY=:${DISPLAY_NUM} + +echo "[VNC] Starting VNC services..." +echo "[VNC] Display: :${DISPLAY_NUM}" +echo "[VNC] Resolution: ${RESOLUTION}" +echo "[VNC] Desktop: ${DESKTOP}" + +# Start Xvfb (virtual framebuffer) +echo "[VNC] Starting Xvfb..." +Xvfb :${DISPLAY_NUM} -screen 0 ${RESOLUTION} & +XVFB_PID=$! +sleep 1 + +if ! kill -0 $XVFB_PID 2>/dev/null; then + echo "[VNC] ERROR: Xvfb failed to start" + exit 1 +fi +echo "[VNC] Xvfb started (PID: $XVFB_PID)" + +# Start D-Bus session bus (required for XFCE) +if [ "$DESKTOP" = "xfce" ] || [ "$DESKTOP" = "xfce4" ]; then + echo "[VNC] Starting D-Bus session bus..." + if command -v dbus-launch &> /dev/null; then + eval $(dbus-launch --sh-syntax) + export DBUS_SESSION_BUS_ADDRESS + echo "[VNC] D-Bus started: $DBUS_SESSION_BUS_ADDRESS" + else + echo "[VNC] WARNING: dbus-launch not found, XFCE may have limited functionality" + fi +fi + +# Start window manager / desktop environment +echo "[VNC] Starting ${DESKTOP}..." +case "$DESKTOP" in + xfce|xfce4) + # Run XFCE setup script if exists (for Yao branding) + if [ -x /usr/local/bin/setup-xfce.sh ]; then + echo "[VNC] Running XFCE setup..." + /usr/local/bin/setup-xfce.sh || true + fi + # XFCE desktop environment + startxfce4 & + ;; + fluxbox) + # Run Fluxbox setup script if exists (Yao branding, disable toolbar) + if [ -x /usr/local/bin/setup-fluxbox.sh ]; then + echo "[VNC] Running Fluxbox setup..." + /usr/local/bin/setup-fluxbox.sh || true + fi + # Minimal window manager for Playwright + fluxbox & + sleep 1 + # Set wallpaper with feh if available (for Yao branding) + WALLPAPER="$HOME/.local/share/wallpapers/yao-wallpaper.png" + if [ -f "$WALLPAPER" ] && command -v feh &> /dev/null; then + echo "[VNC] Setting wallpaper..." + feh --bg-center "$WALLPAPER" || true + fi + ;; + *) + # Default to fluxbox + if [ -x /usr/local/bin/setup-fluxbox.sh ]; then + /usr/local/bin/setup-fluxbox.sh || true + fi + fluxbox & + sleep 1 + # Set wallpaper with feh if available + WALLPAPER="$HOME/.local/share/wallpapers/yao-wallpaper.png" + if [ -f "$WALLPAPER" ] && command -v feh &> /dev/null; then + feh --bg-center "$WALLPAPER" || true + fi + ;; +esac +sleep 2 + +# Start x11vnc server +echo "[VNC] Starting x11vnc on port ${VNC_PORT}..." +VNC_ARGS="-display :${DISPLAY_NUM} -forever -shared -rfbport ${VNC_PORT} -noxdamage" + +if [ -n "$VNC_PASSWORD" ]; then + mkdir -p ~/.vnc + x11vnc -storepasswd "$VNC_PASSWORD" ~/.vnc/passwd + VNC_ARGS="$VNC_ARGS -rfbauth ~/.vnc/passwd" +else + VNC_ARGS="$VNC_ARGS -nopw" +fi + +x11vnc $VNC_ARGS & +X11VNC_PID=$! +sleep 1 + +if ! kill -0 $X11VNC_PID 2>/dev/null; then + echo "[VNC] ERROR: x11vnc failed to start" + exit 1 +fi +echo "[VNC] x11vnc started (PID: $X11VNC_PID)" + +# Start websockify (noVNC WebSocket proxy) +echo "[VNC] Starting websockify on port ${NOVNC_PORT}..." +websockify --web=/usr/share/novnc/ ${NOVNC_PORT} localhost:${VNC_PORT} & +WEBSOCKIFY_PID=$! +sleep 1 + +if ! kill -0 $WEBSOCKIFY_PID 2>/dev/null; then + echo "[VNC] ERROR: websockify failed to start" + exit 1 +fi +echo "[VNC] websockify started (PID: $WEBSOCKIFY_PID)" + +echo "[VNC] ==================================" +echo "[VNC] VNC services started successfully" +echo "[VNC] Desktop: ${DESKTOP}" +echo "[VNC] VNC port: ${VNC_PORT}" +echo "[VNC] noVNC port: ${NOVNC_PORT}" +echo "[VNC] ==================================" + +# Note: Don't wait here - let the entrypoint continue +# Background processes will keep running diff --git a/sandbox/helpers.go b/sandbox/helpers.go index 013d4989..e466ae78 100644 --- a/sandbox/helpers.go +++ b/sandbox/helpers.go @@ -58,8 +58,10 @@ func parseMemory(s string) int64 { } } -// parseLS parses ls -la --time-style=+%s output to []FileInfo -func parseLS(output string) []FileInfo { +// parseLS parses ls -la output to []FileInfo +// If hasTimeStyle is true, expects GNU ls output with --time-style=+%s (Unix epoch) +// If hasTimeStyle is false, expects BusyBox/basic ls output (date string format) +func parseLS(output string, hasTimeStyle bool) []FileInfo { lines := strings.Split(strings.TrimSpace(output), "\n") var result []FileInfo @@ -69,9 +71,19 @@ func parseLS(output string) []FileInfo { continue } - // Parse ls -la output: drwxr-xr-x 2 user group 4096 1234567890 filename + // Parse ls -la output + // GNU with --time-style: drwxr-xr-x 2 user group 4096 1234567890 filename + // BusyBox/basic: drwxr-xr-x 2 user group 4096 Jan 1 12:00 filename fields := strings.Fields(line) - if len(fields) < 7 { + + var minFields int + if hasTimeStyle { + minFields = 7 // mode, links, user, group, size, timestamp, name + } else { + minFields = 9 // mode, links, user, group, size, month, day, time/year, name + } + + if len(fields) < minFields { continue } @@ -85,12 +97,21 @@ func parseLS(output string) []FileInfo { // Parse size size, _ := strconv.ParseInt(fields[4], 10, 64) - // Parse timestamp (Unix epoch) - timestamp, _ := strconv.ParseInt(fields[5], 10, 64) - modTime := time.Unix(timestamp, 0) + // Parse timestamp and get filename + var modTime time.Time + var name string - // Get filename (may contain spaces) - name := strings.Join(fields[6:], " ") + if hasTimeStyle { + // GNU ls with --time-style=+%s: timestamp is Unix epoch in fields[5] + timestamp, _ := strconv.ParseInt(fields[5], 10, 64) + modTime = time.Unix(timestamp, 0) + name = strings.Join(fields[6:], " ") + } else { + // BusyBox/basic ls: date is in fields[5:8] (e.g., "Jan 1 12:00" or "Jan 1 2024") + // Note: time.Now() is used as fallback since BusyBox date parsing is complex + modTime = time.Now() + name = strings.Join(fields[8:], " ") + } // Skip . and .. if name == "." || name == ".." { diff --git a/sandbox/helpers_test.go b/sandbox/helpers_test.go index 1ae2c41a..878d2207 100644 --- a/sandbox/helpers_test.go +++ b/sandbox/helpers_test.go @@ -65,41 +65,83 @@ func TestMapToSlice(t *testing.T) { } func TestParseLS(t *testing.T) { - output := `total 8 + // Test GNU ls output with --time-style=+%s (Unix epoch timestamp) + t.Run("GNU_ls_with_time_style", func(t *testing.T) { + output := `total 8 drwxr-xr-x 2 sandbox sandbox 4096 1700000000 dir1 -rw-r--r-- 1 sandbox sandbox 100 1700000001 file1.txt lrwxrwxrwx 1 sandbox sandbox 10 1700000002 link1 -> file1.txt ` - result := parseLS(output) + result := parseLS(output, true) - if len(result) != 3 { - t.Fatalf("expected 3 items, got %d", len(result)) - } + if len(result) != 3 { + t.Fatalf("expected 3 items, got %d", len(result)) + } - // Check dir1 - if result[0].Name != "dir1" { - t.Errorf("expected name 'dir1', got '%s'", result[0].Name) - } - if !result[0].IsDir { - t.Errorf("expected dir1 to be a directory") - } + // Check dir1 + if result[0].Name != "dir1" { + t.Errorf("expected name 'dir1', got '%s'", result[0].Name) + } + if !result[0].IsDir { + t.Errorf("expected dir1 to be a directory") + } - // Check file1.txt - if result[1].Name != "file1.txt" { - t.Errorf("expected name 'file1.txt', got '%s'", result[1].Name) - } - if result[1].Size != 100 { - t.Errorf("expected size 100, got %d", result[1].Size) - } - if result[1].IsDir { - t.Errorf("expected file1.txt to be a file, not directory") - } + // Check file1.txt + if result[1].Name != "file1.txt" { + t.Errorf("expected name 'file1.txt', got '%s'", result[1].Name) + } + if result[1].Size != 100 { + t.Errorf("expected size 100, got %d", result[1].Size) + } + if result[1].IsDir { + t.Errorf("expected file1.txt to be a file, not directory") + } - // Check link1 - if result[2].Name != "link1 -> file1.txt" { - t.Errorf("expected name 'link1 -> file1.txt', got '%s'", result[2].Name) - } + // Check link1 + if result[2].Name != "link1 -> file1.txt" { + t.Errorf("expected name 'link1 -> file1.txt', got '%s'", result[2].Name) + } + }) + + // Test BusyBox/basic ls output (Alpine-style) + t.Run("BusyBox_ls_basic", func(t *testing.T) { + output := `total 8 +drwxr-xr-x 2 sandbox sandbox 4096 Jan 1 12:00 dir1 +-rw-r--r-- 1 sandbox sandbox 100 Jan 1 12:01 file1.txt +lrwxrwxrwx 1 sandbox sandbox 10 Jan 1 12:02 link1 -> file1.txt +` + + result := parseLS(output, false) + + if len(result) != 3 { + t.Fatalf("expected 3 items, got %d", len(result)) + } + + // Check dir1 + if result[0].Name != "dir1" { + t.Errorf("expected name 'dir1', got '%s'", result[0].Name) + } + if !result[0].IsDir { + t.Errorf("expected dir1 to be a directory") + } + + // Check file1.txt + if result[1].Name != "file1.txt" { + t.Errorf("expected name 'file1.txt', got '%s'", result[1].Name) + } + if result[1].Size != 100 { + t.Errorf("expected size 100, got %d", result[1].Size) + } + if result[1].IsDir { + t.Errorf("expected file1.txt to be a file, not directory") + } + + // Check link1 (in BusyBox format, symlink target is separate field) + if result[2].Name != "link1 -> file1.txt" { + t.Errorf("expected name 'link1 -> file1.txt', got '%s'", result[2].Name) + } + }) } func TestParseStat(t *testing.T) { diff --git a/sandbox/manager.go b/sandbox/manager.go index 056b99bc..e9236aa5 100644 --- a/sandbox/manager.go +++ b/sandbox/manager.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "io" + "net" "os" "path/filepath" "strings" @@ -17,6 +18,7 @@ import ( "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" + "github.com/docker/go-connections/nat" "github.com/yaoapp/yao/sandbox/ipc" ) @@ -183,9 +185,17 @@ func (m *Manager) Close() error { } // GetOrCreate returns existing container or creates new one -func (m *Manager) GetOrCreate(ctx context.Context, userID, chatID string) (*Container, error) { +func (m *Manager) GetOrCreate(ctx context.Context, userID, chatID string, opts ...CreateOptions) (*Container, error) { name := containerName(userID, chatID) + // Extract options if provided + var createOpts CreateOptions + if len(opts) > 0 { + createOpts = opts[0] + } + createOpts.UserID = userID + createOpts.ChatID = chatID + // Check if container already exists (fast path) if c, ok := m.containers.Load(name); ok { cont := c.(*Container) @@ -241,7 +251,7 @@ func (m *Manager) GetOrCreate(ctx context.Context, userID, chatID string) (*Cont } // Create new container - cont, err := m.createContainer(ctx, userID, chatID) + cont, err := m.createContainer(ctx, createOpts) if err != nil { return nil, err } @@ -254,11 +264,19 @@ func (m *Manager) GetOrCreate(ctx context.Context, userID, chatID string) (*Cont } // createContainer creates a new Docker container -func (m *Manager) createContainer(ctx context.Context, userID, chatID string) (*Container, error) { +func (m *Manager) createContainer(ctx context.Context, opts CreateOptions) (*Container, error) { + userID := opts.UserID + chatID := opts.ChatID name := containerName(userID, chatID) + // Use image from options or fall back to config default + image := opts.Image + if image == "" { + image = m.config.Image + } + // Ensure image exists, pull if not - if err := m.ensureImage(ctx, m.config.Image); err != nil { + if err := m.ensureImage(ctx, image); err != nil { return nil, err } @@ -281,7 +299,7 @@ func (m *Manager) createContainer(ctx context.Context, userID, chatID string) (* // Container configuration containerConfig := &container.Config{ - Image: m.config.Image, + Image: image, Cmd: []string{"sleep", "infinity"}, WorkingDir: m.config.ContainerWorkDir, User: m.config.ContainerUser, // Empty string uses image default @@ -306,6 +324,24 @@ func (m *Manager) createContainer(ctx context.Context, userID, chatID string) (* CapDrop: []string{"ALL"}, } + // VNC port mapping for Docker Desktop (macOS/Windows) + // Only enable for VNC-capable images (playwright/desktop) when config is enabled + if m.config.VNCPortMapping && isVNCImage(image) { + // Expose VNC ports in container config + containerConfig.ExposedPorts = nat.PortSet{ + "6080/tcp": struct{}{}, // noVNC websockify + "5900/tcp": struct{}{}, // VNC + } + // Enable SANDBOX_VNC_ENABLED environment variable + containerConfig.Env = append(containerConfig.Env, "SANDBOX_VNC_ENABLED=true") + + // Map to random available ports on 127.0.0.1 + hostConfig.PortBindings = nat.PortMap{ + "6080/tcp": []nat.PortBinding{{HostIP: "127.0.0.1", HostPort: ""}}, // empty = random port + "5900/tcp": []nat.PortBinding{{HostIP: "127.0.0.1", HostPort: ""}}, + } + } + // Create container resp, err := m.dockerClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, name) if err != nil { @@ -775,12 +811,22 @@ func (m *Manager) ReadFile(ctx context.Context, name, path string) ([]byte, erro // ListDir lists directory contents in container func (m *Manager) ListDir(ctx context.Context, name, path string) ([]FileInfo, error) { + // Try GNU ls with --time-style first (for GNU coreutils) result, err := m.Exec(ctx, name, []string{"ls", "-la", "--time-style=+%s", path}, nil) + if err == nil && result.ExitCode == 0 { + return parseLS(result.Stdout, true), nil + } + + // Fall back to basic ls (for BusyBox/Alpine) + result, err = m.Exec(ctx, name, []string{"ls", "-la", path}, nil) if err != nil { return nil, err } + if result.ExitCode != 0 { + return nil, fmt.Errorf("ls failed: %s", result.Stderr) + } - return parseLS(result.Stdout), nil + return parseLS(result.Stdout, false), nil } // Stat returns file info @@ -905,3 +951,19 @@ func (m *Manager) fixIPCSocketPermissions(ctx context.Context, containerID strin // Wait briefly for the chmod to complete time.Sleep(50 * time.Millisecond) } + +// isVNCImage checks if the image is VNC-capable (playwright or desktop variants) +func isVNCImage(imageName string) bool { + return strings.Contains(imageName, "playwright") || strings.Contains(imageName, "desktop") +} + +// findAvailablePort finds an available port on the host +// This is used as a fallback; Docker can auto-assign ports when HostPort is empty +func findAvailablePort() (int, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return 0, err + } + defer listener.Close() + return listener.Addr().(*net.TCPAddr).Port, nil +} diff --git a/sandbox/proxy/convert.go b/sandbox/proxy/convert.go index 02879b43..8cc62a54 100644 --- a/sandbox/proxy/convert.go +++ b/sandbox/proxy/convert.go @@ -313,12 +313,14 @@ func (s *Server) convertResponse(resp *OpenAIResponse) *AnthropicResponse { result.StopReason = &stopReason } - // Convert usage + // Convert usage (always include - Claude CLI expects usage to be present) if resp.Usage != nil { result.Usage = &Usage{ InputTokens: resp.Usage.PromptTokens, OutputTokens: resp.Usage.CompletionTokens, } + } else { + result.Usage = &Usage{InputTokens: 0, OutputTokens: 0} } return result diff --git a/sandbox/proxy/main.go b/sandbox/proxy/main.go index 903ca969..e40afb75 100644 --- a/sandbox/proxy/main.go +++ b/sandbox/proxy/main.go @@ -310,6 +310,7 @@ func (s *Server) processStream(w http.ResponseWriter, flusher http.Flusher, body var toolCalls []*ToolCallAccumulator var contentIndex int var finishReason string + var lastUsage *Usage // Track the latest usage data from backend for scanner.Scan() { line := scanner.Text() @@ -332,19 +333,13 @@ func (s *Server) processStream(w http.ResponseWriter, flusher http.Flusher, body } if len(chunk.Choices) == 0 { - // Usage update at the end + // Usage update at the end - save it but don't send message_delta yet + // It will be included in the final message_delta below if chunk.Usage != nil { - usageEvent := AnthropicStreamEvent{ - Type: "message_delta", - Delta: &DeltaContent{ - StopReason: &finishReason, - }, - Usage: &Usage{ - InputTokens: chunk.Usage.PromptTokens, - OutputTokens: chunk.Usage.CompletionTokens, - }, + lastUsage = &Usage{ + InputTokens: chunk.Usage.PromptTokens, + OutputTokens: chunk.Usage.CompletionTokens, } - s.writeSSE(w, flusher, usageEvent) } continue } @@ -452,15 +447,20 @@ func (s *Server) processStream(w http.ResponseWriter, flusher http.Flusher, body s.writeSSE(w, flusher, stopEvent) } - // Send message_delta with stop reason + // Send message_delta with stop reason and usage + // Claude CLI expects usage to always be present in message_delta if finishReason == "" { finishReason = "end_turn" } + if lastUsage == nil { + lastUsage = &Usage{InputTokens: 0, OutputTokens: 0} + } deltaEvent := AnthropicStreamEvent{ Type: "message_delta", Delta: &DeltaContent{ StopReason: &finishReason, }, + Usage: lastUsage, } s.writeSSE(w, flusher, deltaEvent) diff --git a/sandbox/types.go b/sandbox/types.go index 7db7e63c..96f09875 100644 --- a/sandbox/types.go +++ b/sandbox/types.go @@ -66,3 +66,10 @@ const ( StatusRunning = "running" StatusStopped = "stopped" ) + +// CreateOptions contains options for creating a container +type CreateOptions struct { + UserID string // User identifier (required) + ChatID string // Chat/session identifier (required) + Image string // Docker image to use (optional, falls back to config default) +} diff --git a/sandbox/vncproxy/config.go b/sandbox/vncproxy/config.go new file mode 100644 index 00000000..4f63492b --- /dev/null +++ b/sandbox/vncproxy/config.go @@ -0,0 +1,81 @@ +package vncproxy + +import ( + "os" + "strconv" + "time" +) + +// Config holds VNC proxy configuration +type Config struct { + // Network settings + DockerNetwork string `json:"docker_network,omitempty"` // Docker network name (default: bridge) + ContainerNoVNCPort int `json:"container_novnc_port,omitempty"` // noVNC port inside container (default: 6080) + ContainerVNCPort int `json:"container_vnc_port,omitempty"` // VNC port inside container (default: 5900) + ContainerNamePrefix string `json:"container_name_prefix,omitempty"` // Container name prefix (default: yao-sandbox-) + + // Cache settings + IPCacheTTL time.Duration `json:"ip_cache_ttl,omitempty"` // IP cache TTL (default: 30s) + + // VNC status check + VNCCheckTimeout time.Duration `json:"vnc_check_timeout,omitempty"` // Timeout for VNC ready check (default: 2s) +} + +// DefaultConfig returns default configuration +func DefaultConfig() *Config { + return &Config{ + DockerNetwork: "bridge", + ContainerNoVNCPort: 6080, + ContainerVNCPort: 5900, + ContainerNamePrefix: "yao-sandbox-", + IPCacheTTL: 30 * time.Second, + VNCCheckTimeout: 2 * time.Second, + } +} + +// Init initializes config from environment variables +func (c *Config) Init() { + if env := os.Getenv("YAO_VNC_DOCKER_NETWORK"); env != "" { + c.DockerNetwork = env + } else if c.DockerNetwork == "" { + c.DockerNetwork = "bridge" + } + + if env := os.Getenv("YAO_VNC_CONTAINER_NOVNC_PORT"); env != "" { + if v, err := strconv.Atoi(env); err == nil && v > 0 { + c.ContainerNoVNCPort = v + } + } else if c.ContainerNoVNCPort == 0 { + c.ContainerNoVNCPort = 6080 + } + + if env := os.Getenv("YAO_VNC_CONTAINER_VNC_PORT"); env != "" { + if v, err := strconv.Atoi(env); err == nil && v > 0 { + c.ContainerVNCPort = v + } + } else if c.ContainerVNCPort == 0 { + c.ContainerVNCPort = 5900 + } + + if env := os.Getenv("YAO_VNC_CONTAINER_NAME_PREFIX"); env != "" { + c.ContainerNamePrefix = env + } else if c.ContainerNamePrefix == "" { + c.ContainerNamePrefix = "yao-sandbox-" + } + + if env := os.Getenv("YAO_VNC_IP_CACHE_TTL"); env != "" { + if v, err := time.ParseDuration(env); err == nil && v > 0 { + c.IPCacheTTL = v + } + } else if c.IPCacheTTL == 0 { + c.IPCacheTTL = 30 * time.Second + } + + if env := os.Getenv("YAO_VNC_CHECK_TIMEOUT"); env != "" { + if v, err := time.ParseDuration(env); err == nil && v > 0 { + c.VNCCheckTimeout = v + } + } else if c.VNCCheckTimeout == 0 { + c.VNCCheckTimeout = 2 * time.Second + } +} diff --git a/sandbox/vncproxy/proxy.go b/sandbox/vncproxy/proxy.go new file mode 100644 index 00000000..f968f1d9 --- /dev/null +++ b/sandbox/vncproxy/proxy.go @@ -0,0 +1,569 @@ +package vncproxy + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + "sync" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + "github.com/gorilla/websocket" +) + +// ipCacheEntry holds cached container IP with expiration +type ipCacheEntry struct { + IP string + ExpiresAt time.Time +} + +// Proxy handles VNC proxy requests +type Proxy struct { + config *Config + dockerClient *client.Client + ipCache sync.Map // containerName -> *ipCacheEntry + upgrader websocket.Upgrader +} + +// NewProxy creates a new VNC proxy +func NewProxy(config *Config) (*Proxy, error) { + if config == nil { + config = DefaultConfig() + } + config.Init() + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return nil, fmt.Errorf("failed to create Docker client: %w", err) + } + + // Verify Docker connection + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if _, err := cli.Ping(ctx); err != nil { + cli.Close() + return nil, fmt.Errorf("Docker not available: %w", err) + } + + return &Proxy{ + config: config, + dockerClient: cli, + upgrader: websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true // Allow all origins for VNC + }, + Subprotocols: []string{"binary"}, // noVNC uses binary subprotocol + }, + }, nil +} + +// Close closes the proxy and releases resources +func (p *Proxy) Close() error { + return p.dockerClient.Close() +} + +// extractSandboxID extracts sandbox ID from request path +// Expected format: /v1/sandbox/{id}/vnc/... +func extractSandboxID(r *http.Request) string { + path := r.URL.Path + // Remove prefix /v1/sandbox/ + path = strings.TrimPrefix(path, "/v1/sandbox/") + // Get ID (first segment before next /) + if idx := strings.Index(path, "/"); idx > 0 { + return path[:idx] + } + return path +} + +// HandleVNCStatus returns VNC status for a container +// GET /v1/sandbox/{id}/vnc +func (p *Proxy) HandleVNCStatus(w http.ResponseWriter, r *http.Request) { + sandboxID := extractSandboxID(r) + containerName := p.config.ContainerNamePrefix + sandboxID + + response := map[string]interface{}{ + "sandbox_id": sandboxID, + "container": containerName, + } + + // Check if container exists and is running + _, err := p.getContainerIP(r.Context(), containerName) + if err != nil { + response["available"] = false + response["status"] = "unavailable" + response["message"] = "Container not available" + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // Check if VNC is enabled for this container + if !p.checkVNCEnabled(r.Context(), containerName) { + response["available"] = false + response["status"] = "not_supported" + response["message"] = "VNC not available for this container type" + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // Check if VNC services are ready (try to connect to websockify port) + if !p.checkVNCReady(r.Context(), containerName) { + response["available"] = false + response["status"] = "starting" + response["message"] = "VNC services are starting..." + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // VNC is ready + response["available"] = true + response["status"] = "ready" + response["client_url"] = fmt.Sprintf("/v1/sandbox/%s/vnc/client", sandboxID) + response["websocket_url"] = fmt.Sprintf("/v1/sandbox/%s/vnc/ws", sandboxID) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// HandleVNCClient serves the noVNC client page +// GET /v1/sandbox/{id}/vnc/client?viewonly=true|false +func (p *Proxy) HandleVNCClient(w http.ResponseWriter, r *http.Request) { + sandboxID := extractSandboxID(r) + containerName := p.config.ContainerNamePrefix + sandboxID + + // Verify container exists and is running + _, err := p.getContainerIP(r.Context(), containerName) + if err != nil { + http.Error(w, "Container not available", http.StatusNotFound) + return + } + + if !p.checkVNCEnabled(r.Context(), containerName) { + http.Error(w, "VNC not available for this container", http.StatusBadRequest) + return + } + + // Get viewonly parameter (default: false = interactive) + viewOnly := r.URL.Query().Get("viewonly") == "true" + + // Serve inline noVNC HTML page with status checking + wsPath := fmt.Sprintf("/v1/sandbox/%s/vnc/ws", sandboxID) + p.serveNoVNCPage(w, sandboxID, wsPath, viewOnly) +} + +// HandleVNCWebSocket proxies WebSocket connection to container VNC +// GET /v1/sandbox/{id}/vnc/ws +func (p *Proxy) HandleVNCWebSocket(w http.ResponseWriter, r *http.Request) { + sandboxID := extractSandboxID(r) + containerName := p.config.ContainerNamePrefix + sandboxID + + // Get VNC endpoint (uses port mapping if available, otherwise container IP) + targetAddr, err := p.getVNCEndpoint(r.Context(), containerName) + if err != nil { + http.Error(w, "Container not available", http.StatusNotFound) + return + } + + // Upgrade HTTP to WebSocket (client side) + clientConn, err := p.upgrader.Upgrade(w, r, nil) + if err != nil { + return // Upgrader already sent error response + } + defer clientConn.Close() + + // Connect to container's websockify via WebSocket (not raw TCP) + // websockify expects WebSocket connections at /websockify path with binary subprotocol + wsURL := fmt.Sprintf("ws://%s/websockify", targetAddr) + dialer := websocket.Dialer{ + Subprotocols: []string{"binary"}, + HandshakeTimeout: 5 * time.Second, + } + targetConn, _, err := dialer.Dial(wsURL, nil) + if err != nil { + clientConn.WriteMessage(websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "VNC connection failed")) + return + } + defer targetConn.Close() + + // Bidirectional WebSocket proxy + done := make(chan struct{}, 2) + + // Client -> Container + go func() { + defer func() { done <- struct{}{} }() + for { + messageType, data, err := clientConn.ReadMessage() + if err != nil { + return + } + if err := targetConn.WriteMessage(messageType, data); err != nil { + return + } + } + }() + + // Container -> Client + go func() { + defer func() { done <- struct{}{} }() + for { + messageType, data, err := targetConn.ReadMessage() + if err != nil { + return + } + if err := clientConn.WriteMessage(messageType, data); err != nil { + return + } + } + }() + + // Wait for either direction to close + <-done +} + +// getContainerIP gets the IP address of a container, using cache with TTL +func (p *Proxy) getContainerIP(ctx context.Context, containerName string) (string, error) { + // Check cache + if cached, ok := p.ipCache.Load(containerName); ok { + entry := cached.(*ipCacheEntry) + if time.Now().Before(entry.ExpiresAt) { + return entry.IP, nil + } + // Cache expired, delete it + p.ipCache.Delete(containerName) + } + + // Get from Docker + info, err := p.dockerClient.ContainerInspect(ctx, containerName) + if err != nil { + return "", fmt.Errorf("container not found: %w", err) + } + + if !info.State.Running { + return "", fmt.Errorf("container not running") + } + + // Get IP from the specified network or default bridge + var ip string + if info.NetworkSettings != nil && info.NetworkSettings.Networks != nil { + if net, ok := info.NetworkSettings.Networks[p.config.DockerNetwork]; ok { + ip = net.IPAddress + } else { + // Try to get IP from any network + for _, net := range info.NetworkSettings.Networks { + if net.IPAddress != "" { + ip = net.IPAddress + break + } + } + } + } + + if ip == "" { + return "", fmt.Errorf("container has no IP address") + } + + // Cache the result + p.ipCache.Store(containerName, &ipCacheEntry{ + IP: ip, + ExpiresAt: time.Now().Add(p.config.IPCacheTTL), + }) + + return ip, nil +} + +// getVNCEndpoint returns the host:port to connect to for VNC +// It first checks for port mapping (for Docker Desktop), then falls back to container IP +func (p *Proxy) getVNCEndpoint(ctx context.Context, containerName string) (string, error) { + info, err := p.dockerClient.ContainerInspect(ctx, containerName) + if err != nil { + return "", fmt.Errorf("container not found: %w", err) + } + + if !info.State.Running { + return "", fmt.Errorf("container not running") + } + + // Check for port mapping first (for Docker Desktop on macOS/Windows) + if info.NetworkSettings != nil && info.NetworkSettings.Ports != nil { + portKey := nat.Port(fmt.Sprintf("%d/tcp", p.config.ContainerNoVNCPort)) + if bindings, ok := info.NetworkSettings.Ports[portKey]; ok && len(bindings) > 0 { + binding := bindings[0] + if binding.HostPort != "" { + // Use mapped port on localhost + host := binding.HostIP + if host == "" || host == "0.0.0.0" { + host = "127.0.0.1" + } + return net.JoinHostPort(host, binding.HostPort), nil + } + } + } + + // Fall back to container IP (works on Linux with native Docker) + ip, err := p.getContainerIP(ctx, containerName) + if err != nil { + return "", err + } + return net.JoinHostPort(ip, fmt.Sprintf("%d", p.config.ContainerNoVNCPort)), nil +} + +// checkVNCEnabled checks if container has VNC enabled by checking env vars +func (p *Proxy) checkVNCEnabled(ctx context.Context, containerName string) bool { + info, err := p.dockerClient.ContainerInspect(ctx, containerName) + if err != nil { + return false + } + + // Check environment variables for VNC_ENABLED or SANDBOX_VNC_ENABLED + for _, env := range info.Config.Env { + if strings.HasPrefix(env, "SANDBOX_VNC_ENABLED=true") || + strings.HasPrefix(env, "VNC_ENABLED=true") { + return true + } + } + + // Also check if container image is a VNC-enabled variant + imageName := info.Config.Image + if strings.Contains(imageName, "playwright") || + strings.Contains(imageName, "desktop") { + return true + } + + return false +} + +// checkVNCReady tests if VNC services are ready +// Uses docker exec to test port connectivity (works across platforms including macOS Docker Desktop) +func (p *Proxy) checkVNCReady(ctx context.Context, containerName string) bool { + // Use docker exec to test port connectivity from inside the container + // This approach works regardless of host network configuration + execConfig := container.ExecOptions{ + Cmd: []string{"sh", "-c", fmt.Sprintf("nc -z localhost %d 2>/dev/null || (echo | timeout 1 cat < /dev/tcp/localhost/%d > /dev/null 2>&1)", p.config.ContainerNoVNCPort, p.config.ContainerNoVNCPort)}, + AttachStdout: false, + AttachStderr: false, + } + + execResp, err := p.dockerClient.ContainerExecCreate(ctx, containerName, execConfig) + if err != nil { + return false + } + + err = p.dockerClient.ContainerExecStart(ctx, execResp.ID, container.ExecStartOptions{}) + if err != nil { + return false + } + + // Wait for exec to complete and check exit code + for i := 0; i < 10; i++ { + inspect, err := p.dockerClient.ContainerExecInspect(ctx, execResp.ID) + if err != nil { + return false + } + if !inspect.Running { + return inspect.ExitCode == 0 + } + time.Sleep(100 * time.Millisecond) + } + + return false +} + +// serveNoVNCPage serves an inline HTML page with noVNC client +func (p *Proxy) serveNoVNCPage(w http.ResponseWriter, sandboxID, wsPath string, viewOnly bool) { + viewOnlyStr := "false" + modeIndicator := "Interactive" + modeColor := "#4CAF50" + if viewOnly { + viewOnlyStr = "true" + modeIndicator = "View Only" + modeColor = "#FF9800" + } + + html := fmt.Sprintf(` + + + + + Sandbox - %s + + + +
+
+
Connecting to Sandbox...
+
+
+
+
%s
+
+ + + +`, sandboxID, modeColor, modeIndicator, sandboxID, wsPath, viewOnlyStr) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + io.WriteString(w, html) +} + +// RegisterRoutes registers VNC proxy routes to an HTTP mux +func (p *Proxy) RegisterRoutes(mux *http.ServeMux) { + mux.HandleFunc("/v1/sandbox/", func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + + // Match /v1/sandbox/{id}/vnc + if strings.HasSuffix(path, "/vnc") { + p.HandleVNCStatus(w, r) + return + } + + // Match /v1/sandbox/{id}/vnc/client + if strings.HasSuffix(path, "/vnc/client") { + p.HandleVNCClient(w, r) + return + } + + // Match /v1/sandbox/{id}/vnc/ws + if strings.HasSuffix(path, "/vnc/ws") { + p.HandleVNCWebSocket(w, r) + return + } + + http.NotFound(w, r) + }) +} + +// Helper function to check if request requires VNC container +func (p *Proxy) isVNCRequest(r *http.Request) bool { + path := r.URL.Path + return strings.Contains(path, "/vnc") +} diff --git a/sandbox/vncproxy/proxy_test.go b/sandbox/vncproxy/proxy_test.go new file mode 100644 index 00000000..cd13dad4 --- /dev/null +++ b/sandbox/vncproxy/proxy_test.go @@ -0,0 +1,90 @@ +package vncproxy + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestExtractSandboxID(t *testing.T) { + tests := []struct { + name string + path string + expected string + }{ + { + name: "VNC status path", + path: "/v1/sandbox/abc123/vnc", + expected: "abc123", + }, + { + name: "VNC client path", + path: "/v1/sandbox/user-chat-123/vnc/client", + expected: "user-chat-123", + }, + { + name: "VNC websocket path", + path: "/v1/sandbox/test-sandbox-id/vnc/ws", + expected: "test-sandbox-id", + }, + { + name: "Complex ID", + path: "/v1/sandbox/user_123-chat_456/vnc", + expected: "user_123-chat_456", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, tt.path, nil) + got := extractSandboxID(req) + if got != tt.expected { + t.Errorf("extractSandboxID() = %q, want %q", got, tt.expected) + } + }) + } +} + +func TestConfigDefaults(t *testing.T) { + config := DefaultConfig() + + if config.DockerNetwork != "bridge" { + t.Errorf("DockerNetwork = %q, want %q", config.DockerNetwork, "bridge") + } + if config.ContainerNoVNCPort != 6080 { + t.Errorf("ContainerNoVNCPort = %d, want %d", config.ContainerNoVNCPort, 6080) + } + if config.ContainerVNCPort != 5900 { + t.Errorf("ContainerVNCPort = %d, want %d", config.ContainerVNCPort, 5900) + } + if config.ContainerNamePrefix != "yao-sandbox-" { + t.Errorf("ContainerNamePrefix = %q, want %q", config.ContainerNamePrefix, "yao-sandbox-") + } +} + +func TestConfigInit(t *testing.T) { + config := &Config{} + config.Init() + + // Should have defaults after Init + if config.DockerNetwork != "bridge" { + t.Errorf("DockerNetwork = %q, want %q", config.DockerNetwork, "bridge") + } + if config.ContainerNoVNCPort != 6080 { + t.Errorf("ContainerNoVNCPort = %d, want %d", config.ContainerNoVNCPort, 6080) + } +} + +// Integration tests require Docker - skip if not available +func TestProxyCreation(t *testing.T) { + // This will fail if Docker is not available, which is expected in CI + proxy, err := NewProxy(nil) + if err != nil { + t.Skipf("Skipping test: Docker not available: %v", err) + } + defer proxy.Close() + + if proxy.config == nil { + t.Error("Proxy config should not be nil") + } +}