// // Copyright 2026 The InfiniFlow Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // tenki.go is the Go port of // `agent/sandbox/providers/tenki.py::TenkiProvider`. It runs each // CodeExec call in a disposable Tenki microVM: CreateInstance // provisions a fresh sandbox, ExecuteCode runs `python3 -c ` // or `node -e ` inside it, and DestroyInstance terminates it. // The provider uses only Tenki's create/exec/destroy operations; it // does not use volumes or snapshots. // // The code-wrapping protocol is shared with SelfManaged, Aliyun and // e2b, so the `__RAGFLOW_RESULT__:` marker extraction works uniformly. // Like the Go e2b provider, this port does not collect run artifacts // (artifact collection lives only in the local/ssh providers). // // SDK: github.com/LuxorLabs/tenki-sdk-go/sandbox (MIT). The auth // token and API URL are read from env by Initialize; image and // tunables come from the admin-panel config map. Sandbox scope is // inferred from the API key by the SDK. package sandbox import ( "context" "errors" "fmt" "ragflow/internal/common" "sync" "time" tenkisdk "github.com/LuxorLabs/tenki-sdk-go/sandbox" ) // tenkiDefaultSandboxTimeout bounds a single CodeExec call. A fresh // sandbox is provisioned per CreateInstance and destroyed on // DestroyInstance, so the lifetime need only outlast one execution. const tenkiDefaultSandboxTimeout = 5 * time.Minute // TenkiProvider is the Go port of the Python TenkiProvider. type TenkiProvider struct { client *tenkisdk.Client apiKey string apiURL string image string allowOutbound bool sandboxTimeout time.Duration mu sync.Mutex initialized bool } // newTenkiProviderFromEnv reads TENKI_* env vars and returns a // provider ready for Initialize. func newTenkiProviderFromEnv() *TenkiProvider { return newTenkiProviderFromConfig(tenkiConfigFromEnv()) } // tenkiConfigFromEnv builds a config map from the TENKI_* env vars, // mirroring the admin-panel settings JSON shape. func tenkiConfigFromEnv() map[string]any { return map[string]any{ "API_KEY": common.GetEnv(common.EnvTenkiApiKey), "API_URL": common.GetEnv(common.EnvTenkiAPIURL), "IMAGE": common.GetEnv(common.EnvTenkiImage), "TIMEOUT": common.GetEnv(common.EnvTenkiTimeout), "ALLOW_OUTBOUND": common.GetEnv(common.EnvTenkiAllowOutbound), } } // newTenkiProviderFromConfig builds the provider from a JSON config // map (admin-panel settings or the env-backed map above). func newTenkiProviderFromConfig(cfg map[string]any) *TenkiProvider { p := &TenkiProvider{ apiKey: configString(cfg, "API_KEY"), apiURL: configString(cfg, "API_URL"), image: configString(cfg, "IMAGE"), // Outbound network is opt-in: sandboxed code has no egress // unless ALLOW_OUTBOUND is explicitly "true". This matches // the self_managed sandbox, which treats network access as an // unauthorized-access event by default. allowOutbound: configString(cfg, "ALLOW_OUTBOUND") == "true", } timeoutSec := configInt(cfg, "TIMEOUT", int(tenkiDefaultSandboxTimeout.Seconds())) if timeoutSec > 0 { p.sandboxTimeout = time.Duration(timeoutSec) * time.Second } else { p.sandboxTimeout = tenkiDefaultSandboxTimeout } return p } // ProviderType returns ProviderTenki. func (p *TenkiProvider) ProviderType() ProviderType { return ProviderTenki } // Initialize builds the Tenki SDK client. The auth token comes from // the admin config or TENKI_API_KEY; we check explicitly so the // manager does not register a broken provider. func (p *TenkiProvider) Initialize(ctx context.Context) error { apiKey := p.apiKey if apiKey == "" { apiKey = common.GetEnv(common.EnvTenkiApiKey) } if apiKey == "" { return errors.New("tenki: API key is required (set it in Admin > Sandbox Settings or TENKI_API_KEY)") } apiURL := p.apiURL if apiURL == "" { apiURL = common.GetEnv(common.EnvTenkiAPIURL) } opts := []tenkisdk.Option{tenkisdk.WithAuthToken(apiKey)} if apiURL != "" { opts = append(opts, tenkisdk.WithBaseURL(apiURL)) } c, err := tenkisdk.New(opts...) if err != nil { return fmt.Errorf("tenki: build client: %w", err) } p.client = c p.mu.Lock() p.initialized = true p.mu.Unlock() return nil } // SupportedLanguages returns the languages the default Tenki image can // run. The default image ships with python3 and node. func (p *TenkiProvider) SupportedLanguages() []string { return []string{"python", "javascript"} } // CreateInstance provisions a fresh Tenki sandbox. As with the e2b // provider, the template argument is treated as the language hint; the // actual base image comes from the configured image (empty = Tenki's // default image). InstanceID is the Tenki session id. func (p *TenkiProvider) CreateInstance(ctx context.Context, template string) (*SandboxInstance, error) { if !p.isInitialized() { return nil, fmt.Errorf("tenki: provider not initialized") } lang := normalizeLanguage(template) if lang == "" { return nil, fmt.Errorf("tenki: unsupported language %q", template) } opts := []tenkisdk.CreateOption{ tenkisdk.WithAllowOutbound(p.allowOutbound), tenkisdk.WithMaxDuration(p.sandboxTimeout), tenkisdk.WithWaitTimeout(p.sandboxTimeout), } if p.image != "" { opts = append(opts, tenkisdk.WithImage(p.image)) } sess, err := p.client.Create(ctx, opts...) if err != nil { return nil, fmt.Errorf("tenki: Create: %w", err) } return &SandboxInstance{ InstanceID: sess.ID, Provider: ProviderTenki, Status: "running", Metadata: map[string]any{ "language": lang, "image": p.image, "sandbox_timeout": p.sandboxTimeout.String(), }, }, nil } // ExecuteCode runs the user's code inside the sandbox via // `python3 -c ` (Python) or `node -e ` (JS). The // wrapped code carries the `__RAGFLOW_RESULT__:` marker so the // structured main() return value comes back as before. func (p *TenkiProvider) ExecuteCode( ctx context.Context, inst *SandboxInstance, code, language string, timeoutSec int, args map[string]any, ) (*ExecutionResult, error) { if !p.isInitialized() { return nil, fmt.Errorf("tenki: provider not initialized") } if inst == nil || inst.InstanceID == "" { return nil, fmt.Errorf("tenki: instance id required") } lang := normalizeLanguage(language) if lang == "" { return nil, fmt.Errorf("tenki: unsupported language %q", language) } timeout, err := validateTimeout(timeoutSec) if err != nil { return nil, err } if timeout == 0 { timeout = int(p.sandboxTimeout.Seconds()) } argsJSON, err := argsToJSON(args) if err != nil { return nil, err } var wrapped, cmd string var runArgs []string if lang == "python" { cmd = "python3" wrapped = BuildPythonWrapper(code, argsJSON) runArgs = []string{"-c", wrapped} } else { cmd = "node" wrapped = BuildJavaScriptWrapper(code, argsJSON) runArgs = []string{"-e", wrapped} } // Re-obtain a handle to the sandbox created in CreateInstance and // wait for its data plane to be ready before exec. sess, err := p.client.Session(inst.InstanceID) if err != nil { return nil, fmt.Errorf("tenki: Session(%s): %w", inst.InstanceID, err) } if err := sess.WaitReady(ctx, p.sandboxTimeout); err != nil { return nil, fmt.Errorf("tenki: WaitReady(%s): %w", inst.InstanceID, err) } start := time.Now() res, err := sess.Exec(ctx, cmd, tenkisdk.WithArgs(runArgs...), tenkisdk.WithTimeout(time.Duration(timeout)*time.Second), ) if err != nil { // A non-zero exit is reported as a *Result, not an error; // a nil result here means a transport, timeout or session // error that we cannot map to stdout/stderr. Do not include // runArgs — they embed the user's code and arguments. return nil, fmt.Errorf("tenki: exec %s: %w", cmd, err) } return buildTenkiExecutionResult(res, lang, start), nil } // buildTenkiExecutionResult maps the Tenki Result to our // sandbox.ExecutionResult. Stdout is scanned (untrimmed) for the // `__RAGFLOW_RESULT__:` marker so the model gets the structured // main() return value. func buildTenkiExecutionResult(r *tenkisdk.Result, lang string, start time.Time) *ExecutionResult { stdout, structured := ExtractStructuredResult(string(r.Stdout)) return &ExecutionResult{ Stdout: stdout, Stderr: string(r.Stderr), ExitCode: int(r.ExitCode), ExecutionTime: time.Since(start).Seconds(), Metadata: map[string]any{ "language": lang, "structured_result": structured, "tenki_status": string(r.Status), }, } } // DestroyInstance terminates the sandbox. A session that is already // gone (not found, terminated, or expired past its max duration) is // treated as success so the call is idempotent. func (p *TenkiProvider) DestroyInstance(ctx context.Context, inst *SandboxInstance) error { if !p.isInitialized() { return fmt.Errorf("tenki: provider not initialized") } if inst == nil || inst.InstanceID == "" { return fmt.Errorf("tenki: instance id required") } sess, err := p.client.Session(inst.InstanceID) if err != nil { return fmt.Errorf("tenki: Session(%s): %w", inst.InstanceID, err) } if err := sess.Close(ctx); err != nil { if errors.Is(err, tenkisdk.ErrSessionNotFound) || errors.Is(err, tenkisdk.ErrSessionTerminated) || errors.Is(err, tenkisdk.ErrSessionExpired) { return nil } return fmt.Errorf("tenki: Close(%s): %w", inst.InstanceID, err) } return nil } // HealthCheck probes the Tenki control plane. There is no dedicated // ping endpoint, so a successful WhoAmI is our probe. func (p *TenkiProvider) HealthCheck(ctx context.Context) error { if !p.isInitialized() { return errors.New("tenki: provider not initialized") } if _, err := p.client.WhoAmI(ctx); err != nil { return fmt.Errorf("tenki: WhoAmI: %w", err) } return nil } func (p *TenkiProvider) isInitialized() bool { p.mu.Lock() defer p.mu.Unlock() return p.initialized }