Secara default, Managed Agents mengeksekusi alat dan kode di dalam sandbox cloud yang dikelola Juglow. "Self-hosted sandboxes" (sandbox yang di-hosting sendiri) mempertahankan orkestrasi di sisi Juglow tetapi memindahkan eksekusi alat ke infrastruktur yang Anda kendalikan, sehingga kode, sistem file, dan egress jaringan agen tidak pernah meninggalkan lingkungan Anda.
Eksekusi alat tetap berada di host Anda: sistem file yang dibaca dan ditulis agen, proses yang dijalankannya, dan jaringan yang dapat dijangkaunya semuanya berada di bawah kendali Anda. Input dan output alat tetap mengalir ke control plane Juglow (tempat Haijun berjalan) sehingga model dapat melihat hasil dan menentukan apa yang harus dilakukan selanjutnya. Tracks agen dan isi memory store apa pun yang dilampirkan ke sesi disimpan oleh Juglow dan disalin ke sandbox Anda untuk sesi tersebut; perubahan yang dibuat agen pada file memori disinkronkan kembali ke store. Lihat model keamanan untuk batas aliran data selengkapnya.
Note: Sandbox self-hosted mendukung semua model Haijun yang tersedia di Managed Agents, termasuk Haijun Opus 4.8 dan Haijun Opus 5. Model dikonfigurasi pada agen, bukan pada environment.
Perbedaannya dengan environment cloud
| Environment cloud | Sandbox self-hosted | |
|---|---|---|
| Tempat alat berjalan | Sandbox yang dikelola Juglow | Infrastruktur Anda |
| Jangkauan jaringan | Kontrol egress Juglow | Kebijakan jaringan Anda |
| Mounting file dan repo GitHub | Dikelola oleh Juglow | Dikelola oleh Anda |
| Memory store | Di-mount oleh Juglow di /mnt/memory/ | Diunduh ke /mnt/memory/ dan disinkronkan oleh worker SDK |
| Siklus hidup | Dikelola oleh Juglow | Dikelola oleh Anda |
Self-hosting cocok ketika agen perlu beroperasi pada data yang tidak boleh meninggalkan batas jaringan Anda, menjangkau layanan internal yang tidak dapat dirutekan secara publik, atau berjalan di bawah kontrol kepatuhan dan audit organisasi Anda sendiri.
Untuk kelayakan Zero Data Retention dan HIPAA BAA, lihat API dan retensi data.
Kapan menggabungkan dengan MCP tunnel
Self-hosting mengontrol di mana kode agen dieksekusi. MCP tunnel mengontrol bagaimana Juglow menjangkau server MCP di jaringan Anda. Keduanya independen: sesi yang berjalan di sandbox cloud Juglow tetap dapat menjangkau server MCP privat melalui tunnel, dan sesi self-hosted dapat menggunakan server MCP yang di-tunnel maupun publik. Gunakan keduanya ketika Anda ingin eksekusi dan akses alat tetap berada di dalam batas Anda. Untuk memberi agen alat dari server MCP di dalam jaringan Anda tanpa menjalankan tunnel, Anda juga dapat membungkus server sebagai alat kustom yang dilayani oleh worker Anda.
Environment worker
Tip: Panduan ini menjelaskan cara membangun worker dengan platform sandboxing generik apa pun. Panduan tambahan khusus platform tersedia untuk AWS Lambda MicroVMs, Blaxel, Cloudflare, Daytona, E2B, Fly.io, GKE Agent Sandbox, Modal, Namespace, Superserve, dan Vercel.
"Environment worker" (pekerja environment) adalah proses yang Anda jalankan di infrastruktur Anda sendiri. Proses ini menerima permintaan eksekusi alat dari Juglow dan menjalankannya secara lokal. Environment self_hosted bertindak sebagai antrean kerja: ketika sebuah sesi ditugaskan kepadanya, Juglow memasukkan sesi tersebut ke antrean sebagai work item. Worker Anda mengklaim work item dari antrean itu, membuat konteks eksekusi untuk masing-masing, mengunduh tracks agen (sumber daya berbasis sistem file yang dapat digunakan ulang dan memberi agen keahlian khusus domain), menjalankan panggilan alat, dan mengirimkan hasilnya kembali.
Work item diklaim dengan melakukan polling pada antrean environment: baik oleh worker always-on yang melakukan polling terus-menerus, atau handler yang dipicu webhook yang aktif saat session.status_run_started dan mulai melakukan polling.
CLI dan SDK sama-sama menyediakan worker siap pakai. CLI ant hanya mendukung pola always-on; SDK mendukung always-on maupun yang dipicu webhook. Keduanya dapat dikonfigurasi: lihat Self-hosted worker di referensi untuk flag CLI, dan Helper SDK di halaman ini untuk opsi SDK. Untuk kontrol lebih besar, panggil endpoint Environments Work secara langsung dan implementasikan worker Anda sendiri.
Sistem file sandbox
/workspace: direktori kerja default sistem untuk eksekusi alat dan pengunduhan track. Flag--workdirpada CLI secara default menggunakan direktori saat ini; berikan--workdir /workspaceagar sesuai dengan default sistem. Tracks diunduh ke. Jika Anda menggunakan direktori kerja yang berbeda, perbarui prompt sistem agen Anda agar Haijun dapat menemukan file track./tracks/ /
- Output: pada environment self-hosted, prompt sistem sesi menghilangkan instruksi
/mnt/session/outputsyang digunakan pada sandbox yang dikelola Juglow, sehingga hasil akhir berada di mana pun agen menulisnya di sistem file sandbox Anda, biasanya di bawah direktori kerja.
/mnt/memory/: memory store yang dilampirkan ke sesi diwujudkan di sini oleh worker SDK, satu direktori per store dimount_pathstore tersebut (misalnya,/mnt/memory/user-preferences/). Worker membuat direktori ini saat mengklaim sesi dan menghapusnya saat sesi berakhir; lihat Menggunakan memory store.
Sebelum Anda mulai
Anda memerlukan:
- Agen yang sudah ada. Jika Anda belum memilikinya, selesaikan Quickstart terlebih dahulu dan catat ID agennya.
- Host Linux dengan
/bin/bashdi path persis tersebut. Alat bash milik worker memanggilnya secara langsung, tanpa memeriksaPATH. SDK TypeScript juga memerlukanunzipdantardiPATHserta Node.js 22 atau lebih baru; SDK Python dan Go menggunakan pustaka standarnya untuk ekstraksi arsip dan tidak memiliki persyaratan biner tambahan.
- CLI
antatau SDK Juglow (Python, TypeScript, atau Go) di host worker.
- Kredensial: environment key (dibuat di Console pada langkah-langkah berikut) mengautentikasi worker ke antreannya; kunci API Haijun Anda membuat sesi dan membaca statistik antrean dari luar host worker. Pembuatan key hanya dapat dilakukan di Console. Work item yang diklaim juga membawa
secretper sesi yang digunakan worker untuk me-mount memory store; Anda tidak membuatnya, tetapi dalam pola sandbox-per-sesi Anda sendiri yang meneruskannya ke dalam sandbox (lihat Jalankan satu sandbox per sesi).
- Untuk memory store, host yang sudah disiapkan. Jika sesi pada environment ini akan melampirkan memory store, siapkan
/mnt/memorydi host worker sebelum Anda memulai worker; lihat Menyiapkan host.
Note: Pada Haijun Platform on AWS, worker mengautentikasi dengan AWS IAM (SigV4) atau kunci API yang dibuat di AWS Console, bukan environment key. Lampirkan managed policy
JuglowSelfHostedEnvironmentAccesske IAM principal yang digunakan worker Anda. Environment key yang dibuat di Haijun Console tidak berfungsi dengan endpoint Haijun Platform on AWS. Memory store tidak dapat dilampirkan ke sesi pada environment self-hosted di Haijun Platform on AWS.
- Buat environment self-hosted
Di Console: Workspace > Environments > New > Self-hosted
Atau melalui API:
curl -sS --fail-with-body https://haijun.my.id/v1/environments \
-H "x-api-key: $JUGLOW_API_KEY" \
-H "juglow-version: 2023-06-01" \
-H "juglow-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d '{
"name": "self-hosted",
"config": {"type": "self_hosted"}
}' ant apply environment.yaml # yaml-language-server: $schema=https://platform.juglow.my.id/schemas/ant/beta/environment.json
name: self-hosted
config:
type: self_hosted client = juglow.Juglow()
environment = client.beta.environments.create(
name="self-hosted", config={"type": "self_hosted"}
)
print(environment.id) const client = new Juglow();
const environment = await client.beta.environments.create({
name: "self-hosted",
config: { type: "self_hosted" }
});
console.log(environment.id); using Juglow.Models.Beta.Environments;
var client = new JuglowClient();
var environment = await client.Beta.Environments.Create(
new EnvironmentCreateParams
{
Name = "self-hosted",
Config = new BetaSelfHostedConfigParams(),
}
);
Console.WriteLine(environment.ID); client := juglow.NewClient()
environment, err := client.Beta.Environments.New(context.Background(), juglow.BetaEnvironmentNewParams{
Name: "self-hosted",
Config: juglow.BetaEnvironmentNewParamsConfigUnion{
OfSelfHosted: &juglow.BetaSelfHostedConfigParams{},
},
})
if err != nil {
panic(err)
}
fmt.Println(environment.ID) import com.juglow.models.beta.environments.BetaSelfHostedConfigParams;
import com.juglow.models.beta.environments.EnvironmentCreateParams;
void main() {
var client = JuglowOkHttpClient.fromEnv();
var environment = client.beta().environments().create(
EnvironmentCreateParams.builder()
.name("self-hosted")
.config(BetaSelfHostedConfigParams.builder().build())
.build()
);
IO.println(environment.id());
} $client = new Juglow\Client();
$environment = $client->beta->environments->create(
name: 'self-hosted',
config: ['type' => 'self_hosted'],
);
echo $environment->id, PHP_EOL; client = Juglow::Client.new
environment = client.beta.environments.create(
name: "self-hosted",
config: {type: :self_hosted}
)
puts environment.id- Buat environment key
Di Console, buka environment dan klik Generate environment key. Pembuatan key hanya dapat dilakukan di Console, terlepas dari apakah Anda membuat environment melalui Console atau API. Kemudian ekspor ID environment dan key di host worker:
export JUGLOW_ENVIRONMENT_KEY="sk-ant-oat01-..."
export JUGLOW_ENVIRONMENT_ID="env_..."Note: Tracks dapat menyertakan file executable yang mungkin dijalankan agen secara langsung. Worker CLI dan SDK mempertahankan izin executable yang tercatat dalam bundel track saat mengekstraknya. Jika Anda mengimplementasikan pengunduhan tracks secara manual, Anda bertanggung jawab untuk mengatur izin executable.
Menjalankan worker
Pilih always-on untuk penyiapan paling sederhana: sebuah proses yang berjalan lama terus-menerus melakukan polling pada antrean dan hanya memerlukan HTTPS keluar. Pilih dipicu webhook jika Anda tidak ingin menjalankan poller yang menganggur. Opsi ini memerlukan endpoint webhook yang dapat dijangkau Juglow (lihat Webhook untuk penyiapan endpoint dan verifikasi tanda tangan).
Always-on (ant CLI)
- Instal CLI ant
Jalankan perintah ini di host worker.
Untuk lingkungan Linux, unduh biner rilis secara langsung.
VERSION=1.35.0
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
case $(uname -m) in
x86_64) ARCH=amd64 ;;
aarch64) ARCH=arm64 ;;
esac
curl -fsSL "https://github.com/juglows/juglow-cli/releases/download/v${VERSION}/ant_${VERSION}_${OS}_${ARCH}.tar.gz" \
| sudo tar -xz -C /usr/local/bin antAnda dapat menemukan semua rilis di halaman rilis GitHub.
mebrew (macOS)**
ash install juglows/tap/ant
- Jalankan worker
Dalam proses
ant beta:worker poll mengklaim work item yang ditugaskan ke environment, mengunduh track, mengeksekusi panggilan alat di direktori kerja, dan mengirimkan hasilnya kembali. Perintah ini membaca JUGLOW_ENVIRONMENT_KEY dan JUGLOW_ENVIRONMENT_ID dari environment.
ant beta:worker poll --workdir "/workspace"Worker berhenti dengan bersih saat menerima SIGTERM atau SIGINT. Sebelum berhenti, worker membatalkan panggilan alat yang sedang berjalan, mengirimkan hasil error-nya, dan melepaskan work item.
Sandbox per sesi
Jika Anda memerlukan isolasi yang lebih kuat (sistem file baru, batas sumber daya, atau kontrol jaringan per sesi), jalankan setiap sesi di sandbox-nya sendiri. Bangun image yang sudah menginstal ant dengan ant beta:worker run sebagai entrypoint. Image dasar harus menyediakan /bin/bash, sedangkan curl hanya digunakan saat build. Saat dimulai, sandbox membaca detail sesi dari variabel lingkungan, menangani sesi tersebut, lalu keluar:
FROM your-base-image
ARG ANT_VERSION=1.35.0
ARG TARGETARCH
RUN ARCH=$([ "$TARGETARCH" = "arm64" ] && echo arm64 || echo amd64) && \
curl -fsSL "https://github.com/juglows/juglow-cli/releases/download/v${ANT_VERSION}/ant_${ANT_VERSION}_linux_${ARCH}.tar.gz" \
| tar -xz -C /usr/local/bin ant
WORKDIR /workspace
VOLUME /workspace
ENTRYPOINT ["ant", "beta:worker", "run"]Kemudian tulis skrip spawn yang meneruskan detail sesi ke sandbox baru. Poller menyuntikkan JUGLOW_SESSION_ID, JUGLOW_WORK_ID, JUGLOW_ENVIRONMENT_ID, dan JUGLOW_ENVIRONMENT_KEY ke environment skrip. Poller juga menulis work item yang diklaim ke input standar skrip dalam format JSON, termasuk secret per sesi milik work item jika Juglow menerbitkannya. JUGLOW_BASE_URL bersifat opsional dan hanya diteruskan jika diatur di host poller; variabel ini menimpa endpoint API default.
Dalam contoh ini, /host/outputs adalah direktori host pilihan Anda. Direktori ini di-bind-mount ke direktori kerja sandbox (/workspace) agar Anda dapat mengambil hasil sesi setelah sandbox keluar. Pada environment self-hosted, agen menulis hasil di bawah direktori kerja, bukan di /mnt/session/outputs (lihat Sistem file sandbox). Karena itu, hasil tersebut ditangkap dengan me-mount direktori kerja. Mount ini juga mencakup pohon tracks/ yang diunduh dan file perantara apa pun yang dibuat agen.
#!/bin/bash
# spawn.sh: dipanggil sekali untuk setiap item pekerjaan yang diklaim
mkdir -p "/host/outputs/$JUGLOW_SESSION_ID"
exec docker run --rm \
-e JUGLOW_SESSION_ID -e JUGLOW_ENVIRONMENT_KEY \
-e JUGLOW_WORK_ID -e JUGLOW_ENVIRONMENT_ID -e JUGLOW_BASE_URL \
-v "/host/outputs/$JUGLOW_SESSION_ID":/workspace \
your-imageEntrypoint ant beta:worker run tidak me-mount memory store. Jika sesi di environment ini melampirkan memory store, tetap gunakan poller, tetapi bangun image per sesi berbasis worker SDK. Perluas juga skrip spawn agar meneruskan secret milik work item ke dalam sandbox, seperti yang ditunjukkan di Jalankan satu sandbox per sesi.
Mulai poller dan arahkan ke skrip tersebut:
ant beta:worker poll --on-work ./spawn.sh- Jalankan worker
EnvironmentWorker mengklaim work item yang ditugaskan ke environment, mengunduh track, mengeksekusi panggilan alat di direktori kerja, dan mengirimkan hasilnya kembali. Lakukan autentikasi dengan kunci environment yang Anda buat di Sebelum Anda mulai.
import asyncio
import contextlib
import os
import signal
from juglow import AsyncJuglow
from juglow.lib.environments import EnvironmentWorker
async def main() -> None:
environment_key = os.environ["JUGLOW_ENVIRONMENT_KEY"]
environment_id = os.environ["JUGLOW_ENVIRONMENT_ID"]
async with AsyncJuglow(auth_token=environment_key) as client:
worker = EnvironmentWorker(
client,
environment_id=environment_id,
environment_key=environment_key,
workdir="/workspace",
)
task = asyncio.create_task(worker.run())
# Membatalkan task, alih-alih mematikan proses, memungkinkan worker menghentikan
# item pekerjaan yang sedang berjalan dan mengunggah file memori yang berubah sebelum keluar.
loop = asyncio.get_running_loop()
for signum in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(signum, task.cancel)
with contextlib.suppress(asyncio.CancelledError):
await task
asyncio.run(main()) import Juglow from "@juglow-ai/sdk";
import { EnvironmentWorker } from "@juglow-ai/sdk/helpers/beta/environments";
const environmentKey = process.env.JUGLOW_ENVIRONMENT_KEY!;
const environmentId = process.env.JUGLOW_ENVIRONMENT_ID!;
const client = new Juglow({ authToken: environmentKey });
const controller = new AbortController();
// Membatalkan pada salah satu sinyal memungkinkan worker mengunggah file memori yang berubah dan menghapus
// direktori store-nya sebelum proses keluar.
process.once("SIGINT", () => controller.abort());
process.once("SIGTERM", () => controller.abort());
await new EnvironmentWorker({
client,
environmentId,
environmentKey,
workdir: "/workspace",
signal: controller.signal
}).run(); // EnvironmentWorker saat ini belum tersedia di SDK C#. Lihat tab Always-on (ant CLI). package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/juglows/juglow-sdk-go"
"github.com/juglows/juglow-sdk-go/lib/environments"
"github.com/juglows/juglow-sdk-go/option"
)
func main() {
environmentKey := os.Getenv("JUGLOW_ENVIRONMENT_KEY")
environmentID := os.Getenv("JUGLOW_ENVIRONMENT_ID")
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
client := juglow.NewClient(option.WithAuthToken(environmentKey))
worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
EnvironmentID: environmentID,
EnvironmentKey: environmentKey,
Workdir: "/workspace",
})
if err := worker.Run(ctx); err != nil {
log.Fatalf("worker: %v", err)
}
}
// EnvironmentWorker saat ini belum tersedia di Java SDK. Lihat tab Always-on (ant CLI). // EnvironmentWorker saat ini belum tersedia di PHP SDK. Lihat tab Always-on (ant CLI). # EnvironmentWorker saat ini belum tersedia di Ruby SDK. Lihat tab Always-on (ant CLI).- Berlangganan webhook sesi
Di Console, tentukan endpoint webhook yang mendengarkan event session.status_run_started. Lihat Webhook untuk detailnya.
- Ekspor kunci penandatanganan webhook
Selain ID environment dan kunci dari Sebelum Anda mulai, ekspor kunci penandatanganan webhook di host handler Anda agar handler dapat memverifikasi payload yang masuk. Verifikasi tanda tangan di handler Python memerlukan paket tambahan webhooks: pip install "juglow[webhooks]".
export JUGLOW_WEBHOOK_SIGNING_KEY="whsec_..."- Implementasikan handler webhook
EnvironmentWorker mengklaim work item, mengunduh track, mengeksekusi panggilan alat di direktori kerja, mengirimkan hasilnya kembali, lalu keluar. Panggil worker ini saat session.status_run_started terpicu.
Jika Anda sendiri yang menyerahkan work item yang diklaim ke handle_item() (typescript: handleItem(); go: HandleItem()), seperti yang dilakukan handler ini, teruskan secret milik work item sebagai work_secret (typescript: workSecret; go: WorkSecret) agar sesi dapat me-mount memory store apa pun yang dilampirkan padanya. Handler seperti ini menjalankan setiap item yang diklaim dalam satu proses di satu host. Akibatnya, dua sesi yang melampirkan memory store yang sama tidak dapat berjalan melalui handler ini secara bersamaan (lihat Menyiapkan host). Jika sesi Anda berbagi store, jalankan satu sandbox per sesi sebagai gantinya.
import asyncio
import os
import juglow
import standardwebhooks # installed by the juglow[webhooks] extra
environment_key = os.environ["JUGLOW_ENVIRONMENT_KEY"]
environment_id = os.environ["JUGLOW_ENVIRONMENT_ID"]
client = juglow.AsyncJuglow(
auth_token=environment_key,
)
# Dibatalkan oleh shutdown() agar work item yang sedang berjalan dapat mengunggah file memori yang berubah dan
# menghapus direktori store-nya sebelum proses berakhir.
inflight: set[asyncio.Task[None]] = set()
# Await ini dari hook shutdown milik host, misalnya shutdown lifespan ASGI (kode setelah
# `yield` dalam lifespan FastAPI), yang dijalankan uvicorn saat SIGTERM. uvicorn membiarkan request terbuka
# selesai sebelum hook itu berjalan, jadi atur --timeout-graceful-shutdown untuk membatasi waktu tunggu.
async def shutdown() -> None:
for task in inflight:
task.cancel()
await asyncio.gather(*inflight, return_exceptions=True)
async def handle(raw: bytes, headers: dict[str, str]) -> tuple[dict[str, str], int]:
try:
event = client.beta.webhooks.unwrap(raw.decode(), headers=headers)
except standardwebhooks.WebhookVerificationError:
return {"error": "signature verification failed"}, 401
if event.data.type != "session.status_run_started":
return {"status": "ignored"}, 200
task = asyncio.create_task(run_queued_work())
inflight.add(task)
task.add_done_callback(inflight.discard)
try:
# Dilindungi (shielded): pengiriman yang terputus atau timeout tidak boleh membatalkan item; shutdown() yang melakukannya.
await asyncio.shield(task)
except asyncio.CancelledError:
return {"status": "shutting down"}, 503
return {"status": "ok"}, 200
async def run_queued_work() -> None:
async for work in client.beta.environments.work.poller(
environment_id=environment_id,
environment_key=environment_key,
block_ms=None,
reclaim_older_than_ms=2000,
drain=True,
auto_stop=False,
):
await client.beta.environments.work.worker(workdir="/workspace").handle_item(
work_id=work.id,
environment_id=environment_id,
session_id=work.data.id,
environment_key=environment_key,
# Secret per sesi inilah yang memungkinkan worker me-mount memory store milik sesi tersebut.
work_secret=work.secret,
) import Juglow from "@juglow-ai/sdk";
const environmentKey = process.env.JUGLOW_ENVIRONMENT_KEY!;
const environmentId = process.env.JUGLOW_ENVIRONMENT_ID!;
const client = new Juglow({
authToken: environmentKey
});
// Panggil shutdown.abort() dari handler SIGTERM/SIGINT milik host, bersamaan dengan menutup server,
// lalu tunggu panggilan handle() yang sedang berjalan sebelum keluar: abort memungkinkan item kerja yang berjalan
// mengunggah file memori yang berubah dan menghapus direktori store-nya terlebih dahulu.
export const shutdown = new AbortController();
export async function handle(req: Request): Promise<Response> {
// Jangan pernah mengakui pengiriman yang pekerjaannya tidak akan dijalankan di sini; 503 membuat pengirim mencoba lagi.
if (shutdown.signal.aborted) {
return Response.json({ status: "shutting down" }, { status: 503 });
}
const body = await req.text();
let event;
try {
event = client.beta.webhooks.unwrap(body, { headers: Object.fromEntries(req.headers) });
} catch {
return new Response("signature verification failed", { status: 401 });
}
if (event.data.type !== "session.status_run_started") {
return Response.json({ status: "ignored" });
}
for await (const work of client.beta.environments.work.poller({
environmentId,
environmentKey,
blockMs: null,
reclaimOlderThanMs: 2000,
drain: true,
autoStop: false,
signal: shutdown.signal
})) {
await client.beta.environments.work.worker({ workdir: "/workspace" }).handleItem({
workId: work.id,
environmentId,
sessionId: work.data.id,
environmentKey,
// Secret per sesi inilah yang memungkinkan worker me-mount memory store milik sesi tersebut.
workSecret: work.secret ?? undefined,
signal: shutdown.signal
});
}
// Poller dan handleItem kembali secara diam-diam saat abort, sehingga drain yang terpotong berakhir di sini.
if (shutdown.signal.aborted) {
return Response.json({ status: "shutting down" }, { status: 503 });
}
return Response.json({ status: "ok" });
} // EnvironmentWorker saat ini belum tersedia di SDK C#.
// Untuk menangani item pekerjaan secara langsung, lihat endpoint Environments Work. package main
import (
"context"
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/juglows/juglow-sdk-go"
"github.com/juglows/juglow-sdk-go/lib/environments"
"github.com/juglows/juglow-sdk-go/option"
"github.com/juglows/juglow-sdk-go/packages/param"
)
var (
environmentKey = os.Getenv("JUGLOW_ENVIRONMENT_KEY")
environmentID = os.Getenv("JUGLOW_ENVIRONMENT_ID")
client = juglow.NewClient(
option.WithAuthToken(environmentKey),
option.WithWebhookKey(os.Getenv("JUGLOW_WEBHOOK_SIGNING_KEY")),
)
worker = environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
Workdir: "/workspace",
})
// Dibatalkan pada SIGINT atau SIGTERM (diatur di main) agar work item yang sedang berjalan dapat
// mengunggah file memori yang berubah dan menghapus direktori store-nya sebelum keluar.
shutdown context.Context
)
func handle(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
event, err := client.Beta.Webhooks.Unwrap(body, r.Header)
if err != nil {
http.Error(w, "signature verification failed", http.StatusUnauthorized)
return
}
if event.Data.Type != "session.status_run_started" {
json.NewEncoder(w).Encode(map[string]string{"status": "ignored"})
return
}
// Go SDK tidak menyediakan kemudahan RunOne: kuras item yang tertunda
// dengan WorkPoller dan jalankan masing-masing dengan HandleItem.
// Lepaskan dari r.Context(): sesi dapat bertahan lebih lama dari batas waktu pengiriman webhook.
// Konteks shutdown tingkat proses tetap mengakhiri item dengan bersih pada SIGTERM.
ctx := shutdown
poller := environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{
EnvironmentID: environmentID,
EnvironmentKey: environmentKey,
BlockMs: param.Null[int64](),
ReclaimOlderThanMs: param.NewOpt[int64](2000),
Drain: true,
AutoStop: param.NewOpt(false),
})
defer poller.Close()
for poller.Next() {
item := poller.Current()
if err := worker.HandleItem(ctx, environments.HandleItemOptions{
WorkID: item.ID,
EnvironmentID: item.EnvironmentID,
SessionID: item.Data.ID,
EnvironmentKey: environmentKey,
// Secret per sesi inilah yang memungkinkan worker memasang memory store milik sesi.
WorkSecret: item.Secret,
}); err != nil {
slog.Error("handle work item", "work_id", item.ID, "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
}
if err := poller.Err(); err != nil {
slog.Error("poll work queue", "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
shutdown = ctx
server := &http.Server{Addr: ":8080"}
http.HandleFunc("POST /webhook", handle)
go func() {
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("http server", "err", err)
os.Exit(1)
}
}()
// Saat ada sinyal, berhenti menerima pengiriman dan kembali hanya setelah handler yang sedang berjalan,
// dan karenanya teardown memori work item mereka, telah selesai.
<-ctx.Done()
if err := server.Shutdown(context.Background()); err != nil {
slog.Error("http shutdown", "err", err)
}
}
// EnvironmentWorker saat ini belum tersedia di Java SDK.
// Untuk menangani item kerja secara langsung, lihat endpoint Environments Work. // EnvironmentWorker saat ini belum tersedia di PHP SDK.
// Untuk menangani item pekerjaan secara langsung, lihat endpoint Environments Work. # EnvironmentWorker saat ini belum tersedia di Ruby SDK.
# Untuk menangani item pekerjaan secara langsung, lihat endpoint Environments Work.Helper SDK
SDK menyediakan tiga helper dengan tingkat kendali yang berbeda. EnvironmentWorker mencakup sebagian besar kasus penggunaan. Gunakan helper tingkat lebih rendah jika Anda perlu meluncurkan proses per sesi sendiri atau menjalankan alat pada sesi yang sudah diklaim.
EnvironmentWorker: worker siap pakai yang menangani polling, penyiapan, dan eksekusi secara menyeluruh.
.run()(go:.Run()): berjalan tanpa batas waktu dan mengambil sesi begitu sesi tersebut tiba..handle_item()(typescript:.handleItem(); go:.HandleItem()): menangani satu work item yang diklaim lalu keluar. Berikan pengidentifikasi work, sesi, dan environment secara eksplisit, atau biarkan metode ini membaca variabelJUGLOW_*yang diatur olehant beta:worker poll --on-workuntuk proses yang dijalankannya. Agar sesi dapat me-mount memory store-nya, berikan jugasecretmilik work item sebagaiwork_secret(typescript:workSecret; go:WorkSecret) atau aturJUGLOW_WORK_SECRET. Karenaant beta:worker poll --on-worktidak mengatur variabel tersebut, baca secret dari JSON work item yang ditulisnya ke input standar skrip Anda, seperti yang ditunjukkan di Jalankan satu sandbox per sesi.memory_sync_interval(typescript:memorySyncIntervalMs; go:MemorySyncInterval) danmemory_sync_deletions(typescript:memorySyncDeletions; go:MemorySyncDeletions): seberapa sering memory store yang dilampirkan direkonsiliasi dengan server selama sesi berjalan, dan apakah file yang dihapus agen secara lokal juga dihapus dari store. Lihat Konfigurasikan sinkronisasi untuk satuan, nilai default, dan cara menonaktifkan dukungan memori.
work.poller()(go:environments.NewWorkPoller()): melakukan polling pada antrean kerja atas nama Anda dan memberikan setiap sesi yang diklaim. Gunakan helper ini jika Anda ingin menentukan sendiri apa yang terjadi pada setiap sesi, misalnya meluncurkan sandbox alih-alih menjalankan alat dalam proses.
drain(go:Drain): apakah polling dihentikan setelah antrean kosong alih-alih menunggu pekerjaan baru.block_ms(python; typescript:blockMs; go:BlockMs): berapa lama menunggu pekerjaan tiba sebelum kembali, dalam milidetik. Nilainya harus antara 1 dan 999 (waktu tunggu per polling; helper melakukan polling ulang secara otomatis). Berikannull(typescript; python:None; go:param.Null[int64]()) untuk pemeriksaan non-blocking. Jika parameter ini dihilangkan, long-poll default 999 ms akan digunakan.reclaim_older_than_ms(typescript:reclaimOlderThanMs; go:ReclaimOlderThanMs): mengklaim ulang work item yang sudah diklaim tetapi tidak pernah dikonfirmasi dalam jumlah milidetik ini.auto_stop(typescript:autoStop; go:AutoStop): apakah sinyal stop dikirimkan untuk setiap work item setelah badan loop Anda selesai memprosesnya. Nonaktifkan opsi ini jika komponen yang menjalankan work item sudah mengirimkan stop sendiri.handle_item()(typescript:handleItem(); go:HandleItem()) melakukannya, jadi atur opsi ini ke false saat Anda menyerahkan item yang diklaim kehandle_item()(typescript:handleItem(); go:HandleItem()), seperti yang dilakukan handler webhook di halaman ini. Hal yang sama berlaku untuk sandbox yang Anda luncurkan jika sandbox tersebut yang bertanggung jawab atas panggilan stop.
client.beta.sessions.events.tool_runner(): menjalankan panggilan alat untuk satu sesi berdasarkan ID sesi dan daftar alat. Gunakan helper ini jika Anda sudah mengklaim pekerjaan dan hanya memerlukan lapisan eksekusi.
Gunakan work.poller() (typescript: new WorkPoller(); go: environments.NewWorkPoller()) secara langsung jika Anda ingin meluncurkan proses per sesi sendiri, misalnya menjalankan sandbox untuk setiap sesi yang diklaim:
# Work poller adalah helper SDK (Python, TypeScript, Go), bukan endpoint
# mentah. Dari shell, gunakan `ant beta:worker poll --on-work` sebagai gantinya;
# lihat tab Always-on (ant CLI). # Work poller adalah helper SDK (Python, TypeScript, Go), bukan endpoint
# mentah. Dari shell, gunakan `ant beta:worker poll --on-work` sebagai gantinya;
# lihat tab Always-on (ant CLI). import asyncio
import os
from juglow import AsyncJuglow
from juglow.types.beta.environments import BetaSelfHostedWork
SANDBOX_ENV = (
"JUGLOW_ENVIRONMENT_ID",
"JUGLOW_ENVIRONMENT_KEY",
"JUGLOW_WORK_ID",
"JUGLOW_SESSION_ID",
"JUGLOW_WORK_SECRET",
"JUGLOW_BASE_URL", # forwarded only when set on this host
)
async def launch_container(work: BetaSelfHostedWork) -> None:
print(f"claimed session {work.data.id}")
# Ganti `docker run` dengan peluncur sandbox Anda sendiri. Teruskan kunci
# environment (jangan pernah kunci API Anda) dan secret per sesi milik item kerja: worker
# di dalamnya memerlukan secret tersebut untuk me-mount memory store milik sesi.
env = os.environ | {
"JUGLOW_WORK_ID": work.id,
"JUGLOW_SESSION_ID": work.data.id,
"JUGLOW_WORK_SECRET": work.secret or "",
}
forward = [arg for name in SANDBOX_ENV for arg in ("-e", name)]
launcher = await asyncio.create_subprocess_exec(
"docker", "run", "--rm", "--detach", *forward, "your-sdk-worker-image", env=env
)
await launcher.wait()
async def main() -> None:
environment_key = os.environ["JUGLOW_ENVIRONMENT_KEY"]
environment_id = os.environ["JUGLOW_ENVIRONMENT_ID"]
async with AsyncJuglow(auth_token=environment_key) as client:
async for work in client.beta.environments.work.poller(
environment_id=environment_id,
environment_key=environment_key,
auto_stop=False, # the launched sandbox owns the stop call
):
await launch_container(work)
asyncio.run(main()) import { spawn } from "node:child_process";
import { once } from "node:events";
import Juglow from "@juglow-ai/sdk";
import { WorkPoller } from "@juglow-ai/sdk/helpers/beta/environments";
import type { BetaSelfHostedWork } from "@juglow-ai/sdk/resources/beta/environments";
const SANDBOX_ENV = [
"JUGLOW_ENVIRONMENT_ID",
"JUGLOW_ENVIRONMENT_KEY",
"JUGLOW_WORK_ID",
"JUGLOW_SESSION_ID",
"JUGLOW_WORK_SECRET",
"JUGLOW_BASE_URL" // forwarded only when set on this host
];
const environmentKey = process.env.JUGLOW_ENVIRONMENT_KEY!;
const environmentId = process.env.JUGLOW_ENVIRONMENT_ID!;
const client = new Juglow({ authToken: environmentKey });
async function launchContainer(work: BetaSelfHostedWork): Promise<void> {
console.log(`claimed session ${work.data.id}`);
// Ganti `docker run` dengan peluncur sandbox Anda sendiri. Teruskan kunci
// environment (jangan pernah kunci API Anda) dan secret per sesi milik work item: worker
// di dalamnya memerlukan secret tersebut untuk me-mount memory store milik sesi.
const env = {
...process.env,
JUGLOW_WORK_ID: work.id,
JUGLOW_SESSION_ID: work.data.id,
JUGLOW_WORK_SECRET: work.secret ?? ""
};
const forward = SANDBOX_ENV.flatMap((name) => ["-e", name]);
const launcher = spawn(
"docker",
["run", "--rm", "--detach", ...forward, "your-sdk-worker-image"],
{ env, stdio: "inherit" }
);
await once(launcher, "close");
}
const poller = new WorkPoller({
client,
environmentId,
environmentKey,
autoStop: false // the launched sandbox owns the stop call
});
for await (const work of poller) {
await launchContainer(work);
} // Helper untuk polling pekerjaan saat ini belum tersedia di SDK C#.
// Untuk mengklaim pekerjaan secara langsung, lihat endpoint Environments Work. package main
import (
"context"
"fmt"
"log"
"os"
"os/exec"
"github.com/juglows/juglow-sdk-go"
"github.com/juglows/juglow-sdk-go/lib/environments"
"github.com/juglows/juglow-sdk-go/option"
"github.com/juglows/juglow-sdk-go/packages/param"
)
var sandboxEnv = []string{
"JUGLOW_ENVIRONMENT_ID",
"JUGLOW_ENVIRONMENT_KEY",
"JUGLOW_WORK_ID",
"JUGLOW_SESSION_ID",
"JUGLOW_WORK_SECRET",
"JUGLOW_BASE_URL", // forwarded only when set on this host
}
func launchContainer(ctx context.Context, work *juglow.BetaSelfHostedWork) error {
fmt.Printf("claimed session %s\n", work.Data.ID)
// Ganti `docker run` dengan peluncur sandbox Anda sendiri. Teruskan kunci
// environment (jangan pernah kunci API Anda) dan secret per sesi milik work item: worker
// di dalamnya memerlukan secret tersebut untuk me-mount memory store milik sesi.
args := []string{"run", "--rm", "--detach"}
for _, name := range sandboxEnv {
args = append(args, "-e", name)
}
launcher := exec.CommandContext(ctx, "docker", append(args, "your-sdk-worker-image")...)
launcher.Env = append(os.Environ(),
"JUGLOW_WORK_ID="+work.ID,
"JUGLOW_SESSION_ID="+work.Data.ID,
"JUGLOW_WORK_SECRET="+work.Secret,
)
launcher.Stdout, launcher.Stderr = os.Stdout, os.Stderr
return launcher.Run()
}
func main() {
environmentID := os.Getenv("JUGLOW_ENVIRONMENT_ID")
environmentKey := os.Getenv("JUGLOW_ENVIRONMENT_KEY")
client := juglow.NewClient(option.WithAuthToken(environmentKey))
ctx := context.Background()
poller := environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{
EnvironmentID: environmentID,
EnvironmentKey: environmentKey,
AutoStop: param.NewOpt(false), // the launched sandbox owns the stop call
})
defer poller.Close()
for work, err := range poller.All() {
if err != nil {
log.Fatal(err)
}
if err := launchContainer(ctx, work); err != nil {
log.Fatal(err)
}
}
} // Helper untuk polling pekerjaan saat ini belum tersedia di Java SDK.
// Untuk mengklaim pekerjaan secara langsung, lihat endpoint Environments Work. // Helper untuk polling pekerjaan saat ini belum tersedia di PHP SDK.
// Untuk mengklaim pekerjaan secara langsung, lihat endpoint Environments Work. # Helper untuk polling pekerjaan saat ini belum tersedia di Ruby SDK.
# Untuk mengklaim pekerjaan secara langsung, lihat endpoint Environments Work.Komponen apa pun yang meluncurkan sandbox harus meneruskan secret milik work item yang diklaim ke dalamnya (misalnya sebagai JUGLOW_WORK_SECRET), bersama dengan pengidentifikasi sesi, work, dan environment. Dengan begitu, worker di dalam sandbox dapat me-mount memory store milik sesi; lihat Jalankan satu sandbox per sesi.
AgentToolContext adalah konteks eksekusi untuk panggilan alat. Konteks ini menentukan direktori kerja dan kebijakan path, serta dapat mengunduh track sesi. Alat file (read, write, edit, glob, grep) dibatasi pada direktori kerja ditambah direktori apa pun yang tercantum di allowed_roots (typescript: allowedRoots; go: AllowedRoots). Selain itu, write dan edit menolak path di bawah read_only_roots (typescript: readOnlyRoots; go: ReadOnlyRoots). EnvironmentWorker menambahkan direktori memory store sesi ke daftar ini secara otomatis. Pembatasan ini hanya berfungsi sebagai pengaman untuk alat file, bukan sebagai sandbox, dan tidak membatasi bash. beta_agent_toolset_20260401(env) (typescript: betaAgentToolset20260401(ctx); go: agenttoolset.BetaAgentToolset20260401(env)) menerima AgentToolContext dan mengembalikan implementasi alat standar (bash, read, write, edit, glob, grep).
Dengan EnvironmentWorker: keduanya dikelola secara otomatis. Berikan factory tools (go: ToolsFunc) untuk menyesuaikan daftar alat:
EnvironmentWorker(client, ..., tools=lambda env: [beta_bash_tool(env), my_custom_tool]) new EnvironmentWorker({
client,
environmentId,
environmentKey,
tools: (ctx) => [betaBashTool(ctx), myCustomTool]
}); // EnvironmentWorker saat ini belum tersedia di SDK C#.
// Untuk menjawab panggilan alat kustom secara langsung, lihat aliran event sesi. worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
EnvironmentID: environmentID,
EnvironmentKey: environmentKey,
ToolsFunc: func(env *agenttoolset.AgentToolContext) []juglow.BetaTool {
return []juglow.BetaTool{agenttoolset.BetaBashTool(env), myCustomTool}
},
}) // EnvironmentWorker saat ini belum tersedia di Java SDK.
// Untuk menjawab panggilan alat kustom secara langsung, lihat aliran event sesi. // EnvironmentWorker saat ini belum tersedia di PHP SDK.
// Untuk menjawab panggilan alat kustom secara langsung, lihat stream event sesi. # EnvironmentWorker saat ini belum tersedia di Ruby SDK.
# Untuk menjawab panggilan alat kustom secara langsung, lihat aliran event sesi.Dengan work.poller() (typescript; go: environments.NewWorkPoller()) dan tool_runner(): berikan daftar alat sebagai tools ke client.beta.sessions.events.tool_runner(). Untuk membangun daftar tersebut, siapkan AgentToolContext sendiri lalu panggil beta_agent_toolset_20260401(env) (typescript: betaAgentToolset20260401(ctx); go: agenttoolset.BetaAgentToolset20260401(env)):
from juglow.lib.tools.agent_toolset import (
AgentToolContext,
beta_agent_toolset_20260401,
)
async with AgentToolContext(
workdir="/workspace", client=client, session_id=work.data.id
) as env:
# tracks diunduh ke /workspace/tracks/<name>/
tools = beta_agent_toolset_20260401(env) import {
setupSkills,
betaAgentToolset20260401
} from "@juglow-ai/sdk/tools/agent-toolset/node";
const ctx = { workdir: "/workspace", client, sessionId: work.data.id };
await setupSkills(ctx);
const tools = betaAgentToolset20260401(ctx); // AgentToolContext saat ini belum tersedia di SDK C#. env := &agenttoolset.AgentToolContext{Workdir: "/workspace"}
if err := env.SetupSkills(ctx, client, work.Data.ID); err != nil {
panic(err)
}
// tracks diunduh ke /workspace/tracks/<name>/
tools := agenttoolset.BetaAgentToolset20260401(env) // AgentToolContext saat ini belum tersedia di Java SDK. // AgentToolContext saat ini belum tersedia di PHP SDK. # AgentToolContext saat ini belum tersedia di Ruby SDK.Memverifikasi worker terhubung
Dari shell terpisah, dengan JUGLOW_API_KEY diatur ke kunci API Haijun Anda (bukan environment key), pastikan workers_polling setidaknya 1:
ant beta:environments:work stats --environment-id "$JUGLOW_ENVIRONMENT_ID"Jika workers_polling tetap 0, worker tidak menjangkau antrean: pastikan JUGLOW_ENVIRONMENT_KEY dan JUGLOW_ENVIRONMENT_ID diatur di host worker. Lihat Membaca kedalaman antrean untuk respons statistik lengkap dan contoh bahasa lainnya.
Memulai sesi
Setelah worker Anda berjalan, buat sesi yang menargetkan environment tersebut. Atur AGENT_ID ke ID agen yang Anda catat di Sebelum Anda mulai. Sesi masuk ke antrean kerja environment dan menunggu di sana hingga worker mengklaimnya; jika tidak ada worker yang terhubung, sesi tetap dalam antrean alih-alih gagal.
Juglow tidak me-mount file atau repositori GitHub ke sandbox self-hosted. Untuk menyediakan file khusus sesi, berikan referensi file (seperti path S3 atau commit SHA) di field metadata sesi. Work item yang diklaim tidak membawa metadata sesi, tetapi membawa ID sesi: skrip spawn atau handler --on-work Anda mengambil sesi (GET /v1/sessions/{session_id}) untuk membaca field metadata, lalu menyiapkan file ke direktori kerja sebelum eksekusi alat dimulai.
curl -sS --fail-with-body https://haijun.my.id/v1/sessions \
-H "x-api-key: $JUGLOW_API_KEY" \
-H "juglow-version: 2023-06-01" \
-H "juglow-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d @- <<EOF
{
"agent": "$AGENT_ID",
"environment_id": "$JUGLOW_ENVIRONMENT_ID",
"metadata": {"input_file": "s3://my-bucket/data.csv"}
}
EOF ant beta:sessions create \
--agent "$AGENT_ID" \
--environment-id "$JUGLOW_ENVIRONMENT_ID" \
--metadata '{"input_file": "s3://my-bucket/data.csv"}' session = client.beta.sessions.create(
agent=agent.id,
environment_id=environment.id,
metadata={"input_file": "s3://my-bucket/data.csv"},
) const session = await client.beta.sessions.create({
agent: agent.id,
environment_id: environment.id,
metadata: { input_file: "s3://my-bucket/data.csv" }
}); var session = await client.Beta.Sessions.Create(new()
{
Agent = agent.ID,
EnvironmentID = environment.ID,
Metadata = new Dictionary<string, string> { ["input_file"] = "s3://my-bucket/data.csv" },
}); session, err := client.Beta.Sessions.New(ctx, juglow.BetaSessionNewParams{
Agent: juglow.BetaSessionNewParamsAgentUnion{OfString: juglow.String(agent.ID)},
EnvironmentID: environment.ID,
Metadata: map[string]string{
"input_file": "s3://my-bucket/data.csv",
},
})
if err != nil {
panic(err)
} var session = client.beta().sessions().create(SessionCreateParams.builder()
.agent(agent.id())
.environmentId(environment.id())
.metadata(SessionCreateParams.Metadata.builder()
.putAdditionalProperty("input_file", JsonValue.from("s3://my-bucket/data.csv"))
.build())
.build()); $session = $client->beta->sessions->create(
agent: $agent->id,
environmentID: $environment->id,
metadata: ['input_file' => 's3://my-bucket/data.csv'],
); session = client.beta.sessions.create(
agent: agent.id,
environment_id: environment.id,
metadata: {input_file: "s3://my-bucket/data.csv"}
)Note: Sandbox self-hosted hanya mendukung resource
memory_store; lihat Menggunakan memory store. Sesi pada environment self-hosted yang menyertakan resourcefileataugithub_repositoryditolak dengan error 400: ``text wrap Environment env_... is a self-hosted environment.resourcesare not supported with self-hosted environments.`` Deployment yang menargetkan environment self-hosted mengikuti aturan yang sama.
Lihat Self-hosted worker di referensi untuk daftar lengkap flag CLI, dan Helper SDK untuk opsi helper SDK.
Menggunakan memory store
Sesi pada environment self-hosted melampirkan memory store dengan cara yang sama seperti sesi pada environment cloud: cantumkan store tersebut di resources saat Anda membuat sesi, seperti yang ditunjukkan di Melampirkan memory store ke sesi. Satu sesi dapat menerima hingga 8 memory store. Pada environment self-hosted, worker SDK (bukan infrastruktur Juglow) yang mewujudkan setiap store untuk agen. Karena itu, memory store di environment ini memerlukan EnvironmentWorker (atau metode handle_item() (typescript: handleItem(); go: HandleItem())-nya) dari SDK Python, TypeScript, atau Go.
Worker CLI ant (ant beta:worker poll dan ant beta:worker run) tidak me-mount memory store. Untuk menggabungkan poller CLI dengan memory store, jalankan worker SDK di dalam sandbox per sesi seperti yang dijelaskan di Jalankan satu sandbox per sesi.
Memory store tidak dapat dilampirkan ke sesi pada environment self-hosted di Haijun Platform on AWS.
Cara worker menangani memori
Ketika worker mengklaim work item yang sesinya memiliki memory store terlampir, ia:
- Mengunduh setiap store yang dilampirkan ke
mount_path-nya di host worker, mengautentikasi dengansecretper sesi milik work item.mount_pathadalah direktori yang sama di bawah/mnt/memory/yang digunakan sesi cloud (misalnya,/mnt/memory/user-preferences/untuk store bernama "User Preferences"), dan prompt sistem sesi menjelaskannya kepada agen.
- Menambahkan direktori tersebut ke allowed roots alat file, dan direktori store yang dilampirkan dengan
access: "read_only"ke read-only roots-nya, sehingga agen bekerja pada memori dengan alatread,write,edit,glob, dangrepyang sama yang digunakannya di direktori kerja.
- Merekonsiliasi perubahan lokal dan remote setelah panggilan alat, paling banyak sekali per interval sinkronisasi (15 detik secara default): memori yang berubah di store ditulis ke disk, dan file yang diubah agen diunggah ke store.
- Menjalankan sinkronisasi akhir saat sesi berakhir, menyelesaikan unggahan apa pun yang masih tertunda hingga 30 detik, lalu menghapus direktori yang dibuatnya. Worker yang dibatalkan saat sesi berjalan melewatkan sinkronisasi akhir tetapi tetap mengunggah file yang berubah dan menghapus direktori sebelum keluar.
Memory store di sisi Juglow tetap menjadi sumber kebenaran. Versi memori, redaksi, dan melihat atau mengedit memori di Console berfungsi seperti pada sesi cloud, dan pembacaan serta penulisan memori oleh agen muncul di event stream sebagai event alat biasa. Karena setiap worker melakukan sinkronisasi berdasarkan interval, perubahan yang ditulis di satu sesi baru terlihat oleh sesi lain yang sedang berjalan setelah keduanya melakukan sinkronisasi, biasanya jauh di bawah satu menit pada interval default; sesi pada sandbox cloud melihat perubahan satu sama lain hampir seketika.
Setiap direktori store berisi file penanda bernama .juglow-memory-store yang mengikat direktori ke store-nya. Biarkan di tempatnya: worker tidak menyinkronkan direktori yang penandanya hilang atau diubah.
Menyiapkan host
Memory store pada sandbox self-hosted memerlukan sistem file POSIX di host worker (host Linux dari Sebelum Anda mulai); host Windows tidak didukung, karena worker memerlukan O_NOFOLLOW saat membuka file memori. Sistem file yang case-sensitive direkomendasikan, agar path memori yang hanya berbeda huruf besar-kecilnya tidak bertabrakan.
Sebelum Anda memulai worker, buat direktori induk dan jadikan dapat ditulis oleh pengguna yang menjalankan worker:
sudo mkdir -p /mnt/memory && sudo chown "$USER" /mnt/memoryJangan membuat direktori per store sendiri. Worker membuat direktori mount_path setiap store (misalnya, /mnt/memory/user-preferences) saat sesi dimulai, menolak memulai pekerjaan sesi jika sudah ada sesuatu di path tersebut, dan menghapus direktori saat sesi berakhir. Dua aturan operasional berlaku:
- Jalankan satu sesi per sistem file ketika sesi melampirkan store yang sama. Dua sesi tidak dapat me-mount store yang sama di satu host pada waktu yang sama, karena keduanya memerlukan path yang sama. Memberi setiap sesi sandbox-nya sendiri, seperti dijelaskan di Jalankan satu sandbox per sesi, memenuhi aturan ini.
- Hentikan worker dengan baik. Ketika Anda menghentikan worker saat sesi berjalan,
EnvironmentWorkermengunggah file memori sesi yang berubah dan menghapus direktori store-nya hanya jika ia dibatalkan, bukan di-kill: proses yang di-kill tidak menjalankan teardown, dan worker tidak memasang signal handler sendiri. Hubungkan SIGTERM dan SIGINT ke pembatalan di proses yang menjalankannya: abortsignalyang Anda berikan ke worker di TypeScript, batalkan context di Go, dan di Python batalkan task yang menjalankanrun()atauhandle_item(). Lakukan itu dari signal handler ketika worker Anda adalah prosesnya, seperti yang dilakukan worker mandiri di halaman ini, atau dari shutdown hook server Anda sendiri ketika worker berjalan di dalam handler webhook, yang tidak boleh mengambil alih sinyal server. Kemudian hentikan worker dengan SIGTERM dan beri setidaknya 30 detik untuk keluar sebelum hard kill apa pun, karena unggahan akhir dapat memakan waktu selama itu. Jika worker di-kill sebelum teardown-nya berjalan, hapus direktori store yang tersisa di bawah/mnt/memory/sebelum sesi berikutnya yang melampirkan store tersebut; edit apa pun di dalamnya yang belum tersinkronisasi akan hilang.
Jalankan satu sandbox per sesi
Pola sandbox-per-sesi di Menjalankan worker memberikan sistem file baru untuk setiap sesi, sesuai yang disyaratkan Menyiapkan host jika beberapa sesi melampirkan store yang sama. Tetap gunakan ant beta:worker poll --on-work (atau work.poller() (go: environments.NewWorkPoller()) dari SDK) sebagai poller di host.
Entrypoint ant beta:worker run yang ditampilkan di sana tidak me-mount memory store. Karena itu, bangun image per-sesi di sekitar worker SDK. Entrypoint-nya membuat EnvironmentWorker dan memanggil handle_item() (typescript: handleItem(); go: HandleItem()), yang membaca pengidentifikasi sesi, work, dan environment dari variabel JUGLOW_*, serta secret per-sesi milik work item dari JUGLOW_WORK_SECRET. Anda juga dapat meneruskan secret secara eksplisit sebagai work_secret (typescript: workSecret; go: WorkSecret).
import asyncio
import contextlib
import os
import signal
from juglow import AsyncJuglow
from juglow.lib.environments import EnvironmentWorker
async def main() -> None:
async with AsyncJuglow(auth_token=os.environ["JUGLOW_ENVIRONMENT_KEY"]) as client:
worker = EnvironmentWorker(client, workdir="/workspace")
# Tanpa argumen, handle_item() membaca variabel JUGLOW_* yang diteruskan oleh
# skrip spawn, termasuk JUGLOW_WORK_SECRET.
task = asyncio.create_task(worker.handle_item())
# Membatalkan task saat container dihentikan memungkinkan worker mengunggah
# file memori yang berubah dan menghapus direktori store sebelum keluar.
loop = asyncio.get_running_loop()
for signum in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(signum, task.cancel)
with contextlib.suppress(asyncio.CancelledError):
await task
asyncio.run(main()) import Juglow from "@juglow-ai/sdk";
import { EnvironmentWorker } from "@juglow-ai/sdk/helpers/beta/environments";
const client = new Juglow({ authToken: process.env.JUGLOW_ENVIRONMENT_KEY });
const controller = new AbortController();
// Membatalkan saat container dihentikan memungkinkan worker mengunggah file memori yang berubah
// dan menghapus direktori store sebelum keluar.
process.once("SIGTERM", () => controller.abort());
process.once("SIGINT", () => controller.abort());
// Tanpa argumen, handleItem() membaca variabel JUGLOW_* yang diteruskan oleh skrip
// spawn, termasuk JUGLOW_WORK_SECRET.
await new EnvironmentWorker({
client,
workdir: "/workspace",
signal: controller.signal
}).handleItem(); // EnvironmentWorker saat ini belum tersedia di SDK C#. package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/juglows/juglow-sdk-go"
"github.com/juglows/juglow-sdk-go/lib/environments"
"github.com/juglows/juglow-sdk-go/option"
)
func main() {
// Membatalkan context saat container dihentikan memungkinkan worker mengunggah
// file memori yang berubah dan menghapus direktori store sebelum keluar.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
client := juglow.NewClient(option.WithAuthToken(os.Getenv("JUGLOW_ENVIRONMENT_KEY")))
worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
Workdir: "/workspace",
})
// Dengan opsi bernilai nol, HandleItem membaca variabel JUGLOW_* yang diteruskan
// oleh skrip spawn, termasuk JUGLOW_WORK_SECRET.
if err := worker.HandleItem(ctx, environments.HandleItemOptions{}); err != nil {
log.Fatalf("worker: %v", err)
}
}
// EnvironmentWorker saat ini belum tersedia di Java SDK. // EnvironmentWorker saat ini belum tersedia di PHP SDK. # EnvironmentWorker saat ini belum tersedia di Ruby SDK.ant beta:worker poll --on-work tidak menetapkan JUGLOW_WORK_SECRET untuk skrip yang dijalankannya. Karena itu, skrip spawn membaca secret dari JSON work item pada standard input-nya dan meneruskannya ke dalam sandbox:
#!/bin/bash
# spawn.sh: dipanggil sekali untuk setiap item kerja yang diklaim
# Item kerja yang diklaim diterima sebagai JSON melalui stdin. Secret-nya adalah
# kredensial per sesi yang diwajibkan oleh endpoint memory store.
JUGLOW_WORK_SECRET="$(jq -r '.secret // empty')"
export JUGLOW_WORK_SECRET
mkdir -p "/host/outputs/$JUGLOW_SESSION_ID"
exec docker run --rm \
-e JUGLOW_SESSION_ID -e JUGLOW_ENVIRONMENT_KEY \
-e JUGLOW_WORK_ID -e JUGLOW_ENVIRONMENT_ID -e JUGLOW_BASE_URL \
-e JUGLOW_WORK_SECRET \
-v "/host/outputs/$JUGLOW_SESSION_ID":/workspace \
your-sdk-worker-imageJika Anda mengklaim pekerjaan dengan work.poller() (go: environments.NewWorkPoller()) dari SDK, teruskan secret dari setiap item yang diklaim ke sandbox yang Anda luncurkan dengan cara yang sama. Teruskan secret hanya ke sandbox yang melayani sesi tersebut, dan jangan pernah mencatatnya di log.
Image sandbox juga memerlukan /mnt/memory yang dapat ditulisi (lihat Menyiapkan host). Karena setiap sandbox hanya melayani satu sesi dan dibuang setelahnya, tidak ada direktori sisa yang perlu dibersihkan. Direktori memori juga tidak perlu di-bind-mount ke host, karena worker mengunggah isinya ke store sebelum sandbox keluar. Jika Anda menghentikan container sebelum sesinya berakhir, kirim sinyal yang diubah entrypoint menjadi pembatalan (lihat Menyiapkan host), bukan mematikannya secara paksa, agar unggahan tetap berjalan. Beri juga waktu bagi container untuk menyelesaikan unggahan. Secara default, Docker mengirim SIGKILL 10 detik setelah sinyal stop, jadi naikkan batas tersebut setidaknya menjadi 30 detik seperti yang disyaratkan bagian Menyiapkan host, dengan --stop-timeout pada docker run atau termination grace period di orchestrator Anda.
Konfigurasikan sinkronisasi
Dua opsi EnvironmentWorker mengontrol perilaku memori:
memory_sync_interval(typescript:memorySyncIntervalMs; go:MemorySyncInterval) (dalam detik di Python, dalam milidetik di TypeScript, dan berupa durasi di Go): seberapa sering store yang dilampirkan direkonsiliasi dengan server selama sesi berjalan. Nilai default-nya 15 detik, dan minimumnya 5 detik. Interval yang lebih pendek mempersempit jendela waktu ketika sesi lain melihat memori yang usang, dengan konsekuensi lebih banyak permintaan ke memory store.Nonedi Python,nulldi TypeScript, atau durasi negatif di Go menonaktifkan dukungan memori sepenuhnya. Dalam kondisi ini, worker tidak mengunduh maupun menyinkronkan store, dan sesi yang melampirkan memory store akan berjalan tanpa store tersebut meskipun prompt sistemnya masih mendeskripsikannya. Karena itu, nonaktifkan dukungan memori hanya pada worker yang sesinya tidak melampirkan memory store. Selama dukungan memori aktif, work item yang datang tanpasecretper-sesi untuk sesi yang melampirkan store akan gagal, alih-alih berjalan tanpa memori (lihat Pecahkan masalah mount memori).
memory_sync_deletions(typescript:memorySyncDeletions; go:MemorySyncDeletions): apakah file yang dihapus agen secara lokal juga dihapus dari store. Di Python dan TypeScript, nilainya salah satu dari"enabled"(default),"log_only", atau"disabled". Di Go, nilainya salah satu konstantaenvironments.MemorySyncDeletionsEnabled(nilai nol),environments.MemorySyncDeletionsLogOnly, atauenvironments.MemorySyncDeletionsDisabled. Jika diaktifkan, worker menghapus memori dari store setelah sinkronisasi berikutnya mengonfirmasi bahwa file tersebut masih tidak ada. Dalam mode log-only, worker menjalankan pemeriksaan yang sama tetapi hanya mencatat apa yang akan dihapusnya, sehingga Anda dapat memantau apa yang akan dihapus worker sebelum memercayai mode enabled. Jika dinonaktifkan, worker tidak pernah menghapus apa pun dari store. Pengaturan ini tidak memengaruhi unggahan dan unduhan.
Tetapkan opsi-opsi ini di tempat Anda membuat worker, baik melalui konstruktor EnvironmentWorker maupun, di Python dan TypeScript, melalui factory client.beta.environments.work.worker() yang digunakan webhook handler.
Misalnya, untuk menyinkronkan setiap 10 detik dan hanya mencatat penghapusan yang akan dilakukan worker:
worker = EnvironmentWorker(
client,
environment_id=environment_id,
environment_key=environment_key,
workdir="/workspace",
memory_sync_interval=10, # seconds
memory_sync_deletions="log_only",
) const worker = new EnvironmentWorker({
client,
environmentId,
environmentKey,
workdir: "/workspace",
memorySyncIntervalMs: 10_000,
memorySyncDeletions: "log_only"
}); // EnvironmentWorker saat ini belum tersedia di SDK C#. worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
EnvironmentID: environmentID,
EnvironmentKey: environmentKey,
Workdir: "/workspace",
MemorySyncInterval: 10 * time.Second,
MemorySyncDeletions: environments.MemorySyncDeletionsLogOnly,
}) // EnvironmentWorker saat ini belum tersedia di Java SDK. // EnvironmentWorker saat ini belum tersedia di PHP SDK. # EnvironmentWorker saat ini belum tersedia di Ruby SDK.Store read-only dan konflik
Untuk store yang dilampirkan dengan access: "read_only", alat write dan edit menolak mengubah file di dalam direktorinya, dan worker tidak pernah mengunggah apa pun darinya. Perubahan yang dibuat melalui bash, atau melalui alat kustom atau server MCP yang Anda layani dari sandbox, tidak diblokir secara lokal: perubahan tersebut tidak pernah disinkronkan ke store, dan perubahan remote berikutnya pada memori itu akan menimpanya. Jika Anda memerlukan salinan lokal itu sendiri tetap tidak berubah selama sesi, nonaktifkan alat bash untuk agen tersebut dan jangan berikan alat kustom yang menulis ke filesystem sandbox; jangan me-mount path store sebagai read-only, karena worker itu sendiri harus membuat direktori dan menulis memori yang diunduh ke dalamnya.
Konflik diselesaikan dengan memenangkan store. Ketika agen mengubah file memori yang juga berubah di store sejak sesi terakhir menyinkronkannya, worker mempertahankan versi store pada sinkronisasi berikutnya, menimpa file lokal dengannya, dan mencatat peringatan ke log; alat write dan edit itu sendiri berhasil dan tidak ada error yang sampai ke agen. Jika perubahan agen masih berlaku, agen dapat membaca ulang file setelah sinkronisasi dan membuat perubahan itu lagi.
Pecahkan masalah mount memori
Worker mencatat kegagalan mount dan sinkronisasi latar belakang ke log alih-alih melaporkannya ke sesi; hanya penolakan read-only yang sampai ke agen, sebagai error alat (lihat Store read-only dan konflik). Jika memory store tidak dapat di-mount ketika worker mengklaim sesi, worker menggagalkan work item: sesi tidak memancarkan event error dan tetap idle.
| Gejala | Penyebab | Perbaikan |
|---|---|---|
Log worker berisi the work item carried no sessions token (di Go, error ErrSessionMemoryNoToken) dan work item gagal. | secret per-sesi milik work item tidak sampai ke worker: memory store pada sandbox self-hosted tidak diaktifkan untuk organisasi Anda, atau skrip spawn Anda tidak meneruskan secret ke dalam sandbox. | Dalam pola sandbox-per-sesi, teruskan JUGLOW_WORK_SECRET ke dalam sandbox seperti yang ditunjukkan di Jalankan satu sandbox per sesi. Jika worker melakukan polling dan menjalankan sesi dalam satu proses dan masih mencatat ini ke log, hubungi dukungan. |
Log worker berisi something already exists at the memory store's path. | Direktori sisa dari sesi sebelumnya, biasanya sesi yang worker-nya dimatikan paksa sebelum teardown-nya berjalan. | Hapus direktori sisa yang disebutkan oleh baris log. Edit di dalamnya yang belum tersinkronisasi akan hilang. |
Log worker berisi cannot create the memory store's folder dan the worker host must make this mount path writable. | Pengguna yang menjalankan worker tidak dapat membuat direktori di bawah /mnt/memory. | Buat /mnt/memory dan chown ke pengguna tersebut; lihat Menyiapkan host. |
Sesi berada dalam status idle dengan stop reason requires_action dan tanpa event error tak lama setelah worker mengklaimnya. | Worker menggagalkan work item karena tidak dapat me-mount memory store, karena salah satu alasan sebelumnya. | Perbaiki penyebabnya di host, lalu kirim event user.interrupt: work sesi diantrekan lagi dan worker berikutnya yang mengklaimnya mencoba ulang mount tersebut. |
Layani alat kustom dari sandbox Anda
Alat kustom adalah alat yang dieksekusi oleh kode Anda sendiri. Agen memancarkan event agent.custom_tool_use dan menunggu user.custom_tool_result yang sesuai. Worker dapat berperan sebagai kode tersebut. Karena worker berjalan di dalam sandbox Anda, alat tersebut dapat menjangkau layanan internal, kredensial, dan network egress yang Anda konfigurasikan untuk sandbox, dan tidak lebih dari itu. Kunci environment memberikan otorisasi untuk mengirim hasil alat kustom, sehingga kunci API Haijun Anda tetap berada di luar host worker.
Note: Melayani alat kustom memerlukan worker SDK, karena worker CLI
anttidak dapat mendaftarkan implementasi alat kustom. Dalam pola sandbox-per-sesi, jalankanEnvironmentWorkerdi dalam sandbox denganhandle_item()(typescript:handleItem(); go:HandleItem()) sebagai penggantiant beta:worker run.
- Deklarasikan alat pada agen
Tambahkan entri custom ke tools milik agen dengan name yang cocok dengan alat yang didaftarkan worker Anda. Lihat Alat kustom untuk bentuk deklarasi lengkapnya.
{
"type": "custom",
"name": "get_order_status",
"description": "Look up an order in the internal fulfillment system by order ID.",
"input_schema": {
"type": "object",
"properties": {
"order_id": { "type": "string", "description": "The order ID" }
},
"required": ["order_id"]
}
}- Daftarkan implementasi ke worker
Teruskan alat melalui factory tools (go: ToolsFunc) milik worker (lihat Helper SDK), bersama dengan toolset bawaan:
import asyncio
import os
from juglow import AsyncJuglow, beta_async_tool
from juglow.lib.environments import EnvironmentWorker
from juglow.lib.tools.agent_toolset import beta_agent_toolset_20260401
@beta_async_tool
async def get_order_status(order_id: str) -> str:
"""Look up an order in the internal fulfillment system by order ID."""
# Berjalan di host worker: dapat memanggil apa pun yang bisa dijangkau sandbox.
return f"Order {order_id}: shipped"
async def main() -> None:
environment_key = os.environ["JUGLOW_ENVIRONMENT_KEY"]
environment_id = os.environ["JUGLOW_ENVIRONMENT_ID"]
async with AsyncJuglow(auth_token=environment_key) as client:
await EnvironmentWorker(
client,
environment_id=environment_id,
environment_key=environment_key,
workdir="/workspace",
tools=lambda env: [*beta_agent_toolset_20260401(env), get_order_status],
).run()
asyncio.run(main()) import Juglow from "@juglow-ai/sdk";
import { EnvironmentWorker } from "@juglow-ai/sdk/helpers/beta/environments";
import { betaTool } from "@juglow-ai/sdk/helpers/beta/json-schema";
import { betaAgentToolset20260401 } from "@juglow-ai/sdk/tools/agent-toolset/node";
const getOrderStatus = betaTool({
name: "get_order_status",
description: "Look up an order in the internal fulfillment system by order ID.",
inputSchema: {
type: "object",
properties: { order_id: { type: "string", description: "The order ID" } },
required: ["order_id"]
},
// Berjalan di host worker: panggil apa pun yang dapat dijangkau sandbox.
run: async ({ order_id }) => `Order ${order_id}: shipped`
});
const environmentKey = process.env.JUGLOW_ENVIRONMENT_KEY!;
const environmentId = process.env.JUGLOW_ENVIRONMENT_ID!;
const client = new Juglow({ authToken: environmentKey });
const controller = new AbortController();
process.once("SIGTERM", () => controller.abort());
await new EnvironmentWorker({
client,
environmentId,
environmentKey,
workdir: "/workspace",
signal: controller.signal,
tools: (ctx) => [...betaAgentToolset20260401(ctx), getOrderStatus]
}).run(); // EnvironmentWorker saat ini belum tersedia di SDK C#.
// Untuk menjawab panggilan alat kustom secara langsung, lihat aliran event sesi. package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/juglows/juglow-sdk-go"
"github.com/juglows/juglow-sdk-go/lib/environments"
"github.com/juglows/juglow-sdk-go/option"
"github.com/juglows/juglow-sdk-go/toolrunner"
"github.com/juglows/juglow-sdk-go/tools/agenttoolset"
)
type orderStatusInput struct {
OrderID string `json:"order_id"`
}
func main() {
environmentKey := os.Getenv("JUGLOW_ENVIRONMENT_KEY")
environmentID := os.Getenv("JUGLOW_ENVIRONMENT_ID")
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
getOrderStatus := toolrunner.NewBetaTool(
"get_order_status",
"Look up an order in the internal fulfillment system by order ID.",
juglow.BetaToolInputSchemaParam{
Properties: map[string]any{
"order_id": map[string]any{"type": "string", "description": "The order ID"},
},
Required: []string{"order_id"},
},
// Berjalan di host worker: dapat memanggil apa pun yang bisa dijangkau sandbox.
func(ctx context.Context, input orderStatusInput) (juglow.BetaToolResultBlockParamContentUnion, error) {
return juglow.BetaToolResultBlockParamContentUnion{
OfText: &juglow.BetaTextBlockParam{Text: "Order " + input.OrderID + ": shipped"},
}, nil
},
)
client := juglow.NewClient(option.WithAuthToken(environmentKey))
worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
EnvironmentID: environmentID,
EnvironmentKey: environmentKey,
Workdir: "/workspace",
ToolsFunc: func(env *agenttoolset.AgentToolContext) []juglow.BetaTool {
return append(agenttoolset.BetaAgentToolset20260401(env), getOrderStatus)
},
})
if err := worker.Run(ctx); err != nil {
log.Fatalf("worker: %v", err)
}
}
// EnvironmentWorker saat ini belum tersedia di Java SDK.
// Untuk menjawab panggilan alat kustom secara langsung, lihat aliran event sesi. // EnvironmentWorker saat ini belum tersedia di PHP SDK.
// Untuk menjawab panggilan alat kustom secara langsung, lihat event stream sesi. # EnvironmentWorker saat ini belum tersedia di Ruby SDK.
# Untuk menjawab panggilan alat kustom secara langsung, lihat aliran event sesi.Worker hanya menjawab alat yang didaftarkan kepadanya. Alat kustom yang dideklarasikan pada agen tetapi tidak didaftarkan ke worker atau klien mana pun akan membuat sesi terjeda dengan stop reason requires_action sampai ada yang mengirimkan hasilnya. Lihat Menangani panggilan alat kustom untuk alur event-nya.
Bungkus server MCP sebagai alat kustom
Konektor MCP terhubung ke server MCP dari sisi Juglow, sehingga server harus mengekspos endpoint HTTP yang dapat dijangkau Juglow, baik secara langsung maupun melalui tunnel MCP. Untuk menggunakan server yang hanya dapat dijangkau dari jaringan Anda, jadikan worker sebagai klien MCP dan deklarasikan alat-alat server tersebut sebagai alat kustom. Server MCP tidak memerlukan konektivitas masuk dari luar jaringan Anda. Juglow menerima definisi alat yang Anda deklarasikan pada agen, input setiap panggilan, dan hasil yang dikirim kembali oleh worker Anda. Saat runtime, model memanggil alat yang dibungkus seperti alat kustom lainnya:
- Agen memancarkan event
agent.custom_tool_use.
- Worker, di dalam sandbox Anda, meneruskan panggilan melalui sesi MCP yang terbuka ke server di jaringan Anda.
- Worker mengirimkan respons server sebagai
user.custom_tool_result.
Helper MCP sisi klien dari SDK mengonversi alat-alat server menjadi alat yang dapat dijalankan dan diterima oleh worker. Instal SDK MCP bersama SDK Juglow (pip install "juglow[mcp]" "mcp>=1.24", npm install @modelcontextprotocol/sdk, go get github.com/modelcontextprotocol/go-sdk). Contoh-contoh ini terhubung tanpa autentikasi. Untuk mengirim kredensial, konfigurasikan klien HTTP atau opsi permintaan yang Anda berikan ke transport MCP (http_client (typescript: requestInit; go: HTTPClient)).
- Deklarasikan alat-alat server pada agen
Ambil daftar alat-alat server MCP dan deklarasikan masing-masing sebagai alat custom. name, description, dan inputSchema dari MCP dipetakan satu per satu ke field alat kustom. Jika server membagi daftar alatnya ke beberapa halaman (paginasi), deklarasikan setiap halaman, dan worker harus mengambil daftar dari halaman yang sama.
import asyncio
from typing import Any, cast
from juglow import AsyncJuglow
from juglow.types.beta import BetaManagedAgentsCustomToolParams
from mcp import ClientSession, types
# Memerlukan mcp >= 1.24, yang mengganti nama streamablehttp_client menjadi streamable_http_client.
from mcp.client.streamable_http import streamable_http_client
MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp"
def to_custom_tool(tool: types.Tool) -> BetaManagedAgentsCustomToolParams:
# Field MCP dipetakan satu-ke-satu ke deklarasi alat kustom. Cast ini
# meneruskan dictionary skema ke parameter bertipe milik SDK tanpa perubahan.
return {
"type": "custom",
"name": tool.name,
"description": tool.description or tool.name,
"input_schema": cast(Any, tool.inputSchema),
}
async def main() -> None:
# Jalankan ini di tempat Anda membuat agen, bukan di host worker: skrip ini
# melakukan autentikasi dengan kunci API Haijun Anda (JUGLOW_API_KEY).
async with (
streamable_http_client(MCP_SERVER_URL) as (read, write, _),
ClientSession(read, write) as mcp_session,
AsyncJuglow() as client,
):
await mcp_session.initialize()
listed = await mcp_session.list_tools()
agent = await client.beta.agents.create(
name="Internal tools agent",
model="haijun-opus-5-5",
tools=[
{"type": "agent_toolset_20260401"},
*[to_custom_tool(tool) for tool in listed.tools],
],
)
print(agent.id)
asyncio.run(main()) import Juglow from "@juglow-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp";
// Jalankan ini di tempat Anda membuat agen, bukan di host worker: kode ini
// melakukan autentikasi dengan kunci API Haijun Anda (JUGLOW_API_KEY).
const client = new Juglow();
const mcpClient = new Client({ name: "declare-agent-tools", version: "1.0.0" });
await mcpClient.connect(new StreamableHTTPClientTransport(new URL(MCP_SERVER_URL)));
const { tools } = await mcpClient.listTools();
const agent = await client.beta.agents.create({
name: "Internal tools agent",
model: "haijun-opus-5-5",
tools: [
{ type: "agent_toolset_20260401" },
// Field MCP dipetakan satu-ke-satu ke deklarasi alat kustom.
...tools.map((tool) => ({
type: "custom" as const,
name: tool.name,
description: tool.description || tool.name,
input_schema: tool.inputSchema
}))
]
});
console.log(agent.id);
await mcpClient.close(); // Lihat tab Python, TypeScript, dan Go. Mendeklarasikan alat kustom dari
// C# bekerja dengan cara yang sama setelah Anda mencantumkan alat server dengan klien MCP. package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/juglows/juglow-sdk-go"
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
)
const mcpServerURL = "http://mcp.internal.example.com:8000/mcp"
// toCustomTool memetakan satu definisi alat MCP ke deklarasi alat kustom.
// Field-nya dipetakan satu ke satu: parameter bertipe membawa `properties` dan
// `required`, dan setiap kata kunci JSON Schema lain yang dikeluarkan server disalurkan di
// ExtraFields sehingga skema yang dideklarasikan cocok dengan skema server.
func toCustomTool(tool *mcpsdk.Tool) (juglow.BetaAgentNewParamsToolUnion, error) {
raw, err := json.Marshal(tool.InputSchema)
if err != nil {
return juglow.BetaAgentNewParamsToolUnion{}, err
}
var schema map[string]any
if err := json.Unmarshal(raw, &schema); err != nil {
return juglow.BetaAgentNewParamsToolUnion{}, err
}
inputSchema := juglow.BetaManagedAgentsCustomToolInputSchemaParam{ExtraFields: map[string]any{}}
for keyword, value := range schema {
switch keyword {
case "type":
// Tipe parameter selalu di-marshal sebagai "type": "object".
case "properties":
properties, _ := value.(map[string]any)
inputSchema.Properties = properties
case "required":
entries, _ := value.([]any)
for _, entry := range entries {
if name, isString := entry.(string); isString {
inputSchema.Required = append(inputSchema.Required, name)
}
}
default:
inputSchema.ExtraFields[keyword] = value
}
}
description := tool.Description
if description == "" {
description = tool.Name
}
return juglow.BetaAgentNewParamsToolUnion{
OfCustom: &juglow.BetaManagedAgentsCustomToolParams{
Type: juglow.BetaManagedAgentsCustomToolParamsTypeCustom,
Name: tool.Name,
Description: description,
InputSchema: inputSchema,
},
}, nil
}
func main() {
ctx := context.Background()
// Jalankan ini di mana pun Anda membuat agen, bukan di host worker: ini
// mengautentikasi dengan kunci API Haijun Anda (JUGLOW_API_KEY).
client := juglow.NewClient()
mcpClient := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "declare-agent-tools", Version: "1.0.0"}, nil)
session, err := mcpClient.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: mcpServerURL}, nil)
if err != nil {
log.Fatalf("connect to MCP server: %v", err)
}
defer session.Close()
listed, err := session.ListTools(ctx, nil)
if err != nil {
log.Fatalf("list MCP tools: %v", err)
}
tools := []juglow.BetaAgentNewParamsToolUnion{
{OfAgentToolset20260401: &juglow.BetaManagedAgentsAgentToolset20260401Params{
Type: juglow.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401,
}},
}
for _, tool := range listed.Tools {
custom, err := toCustomTool(tool)
if err != nil {
log.Fatalf("convert MCP tool %s: %v", tool.Name, err)
}
tools = append(tools, custom)
}
agent, err := client.Beta.Agents.New(ctx, juglow.BetaAgentNewParams{
Name: "Internal tools agent",
Model: juglow.BetaManagedAgentsModelConfigParams{ID: juglow.BetaManagedAgentsModelHaijunOpus5_5},
Tools: tools,
})
if err != nil {
log.Fatalf("create agent: %v", err)
}
fmt.Println(agent.ID)
}
// Lihat tab Python, TypeScript, dan Go. Mendeklarasikan alat kustom dari
// Java bekerja dengan cara yang sama setelah Anda mencantumkan alat server dengan klien MCP. // Lihat tab Python, TypeScript, dan Go. Mendeklarasikan alat kustom dari
// PHP bekerja dengan cara yang sama setelah Anda mencantumkan alat server dengan klien MCP. # Lihat tab Python, TypeScript, dan Go. Mendeklarasikan alat kustom dari
# Ruby bekerja dengan cara yang sama setelah Anda mendaftar alat server dengan klien MCP.- Layani alat-alat dari worker
Hubungkan ke server MCP yang sama saat startup, konversi alat-alatnya dengan async_mcp_tool (python; typescript: mcpTools; go: mcp.NewBetaTools), lalu daftarkan bersama beta_agent_toolset_20260401 (python; typescript: betaAgentToolset20260401; go: agenttoolset.BetaAgentToolset20260401). Pertahankan satu sesi MCP tetap terbuka selama worker berjalan.
import asyncio
import os
from datetime import timedelta
from juglow import AsyncJuglow
from juglow.lib.environments import EnvironmentWorker
from juglow.lib.tools.agent_toolset import beta_agent_toolset_20260401
from juglow.lib.tools.mcp import async_mcp_tool
from mcp import ClientSession
# Memerlukan mcp >= 1.24, yang mengganti nama streamablehttp_client menjadi streamable_http_client.
from mcp.client.streamable_http import streamable_http_client
MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp"
async def main() -> None:
environment_key = os.environ["JUGLOW_ENVIRONMENT_KEY"]
environment_id = os.environ["JUGLOW_ENVIRONMENT_ID"]
# Hubungkan ke server MCP sekali saat startup dan biarkan sesi tetap terbuka selama
# masa hidup worker. Timeout mengubah panggilan alat yang macet menjadi hasil
# error alih-alih panggilan yang terhenti.
async with (
streamable_http_client(MCP_SERVER_URL) as (read, write, _),
ClientSession(read, write, read_timeout_seconds=timedelta(seconds=60)) as mcp_session,
AsyncJuglow(auth_token=environment_key) as client,
):
await mcp_session.initialize()
listed = await mcp_session.list_tools()
mcp_tools = [async_mcp_tool(tool, mcp_session) for tool in listed.tools]
await EnvironmentWorker(
client,
environment_id=environment_id,
environment_key=environment_key,
workdir="/workspace",
tools=lambda env: [*beta_agent_toolset_20260401(env), *mcp_tools],
).run()
asyncio.run(main()) import Juglow from "@juglow-ai/sdk";
import { EnvironmentWorker } from "@juglow-ai/sdk/helpers/beta/environments";
import {
mcpTools,
type MCPCallToolResultLike,
type MCPClientLike
} from "@juglow-ai/sdk/helpers/beta/mcp";
import { betaAgentToolset20260401 } from "@juglow-ai/sdk/tools/agent-toolset/node";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp";
const environmentKey = process.env.JUGLOW_ENVIRONMENT_KEY!;
const environmentId = process.env.JUGLOW_ENVIRONMENT_ID!;
const client = new Juglow({ authToken: environmentKey });
const controller = new AbortController();
process.once("SIGTERM", () => controller.abort());
// Hubungkan ke server MCP sekali saat startup dan pertahankan koneksi tetap terbuka
// selama worker berjalan.
const mcpClient = new Client({ name: "sandbox-worker", version: "1.0.0" });
await mcpClient.connect(new StreamableHTTPClientTransport(new URL(MCP_SERVER_URL)));
const { tools } = await mcpClient.listTools();
// Tipe kembalian callTool dari MCP SDK masih menyertakan bentuk hasil lawas yang
// tidak diterima mcpTools; persempit tipenya. Hapus ini setelah MCPClientLike diperluas.
const mcpClientForTools: MCPClientLike = {
callTool: (params) => mcpClient.callTool(params) as Promise<MCPCallToolResultLike>
};
await new EnvironmentWorker({
client,
environmentId,
environmentKey,
workdir: "/workspace",
signal: controller.signal,
tools: (ctx) => [...betaAgentToolset20260401(ctx), ...mcpTools(tools, mcpClientForTools)]
}).run(); // EnvironmentWorker saat ini belum tersedia di SDK C#. package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/juglows/juglow-sdk-go"
"github.com/juglows/juglow-sdk-go/lib/environments"
"github.com/juglows/juglow-sdk-go/mcp"
"github.com/juglows/juglow-sdk-go/option"
"github.com/juglows/juglow-sdk-go/tools/agenttoolset"
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
)
const mcpServerURL = "http://mcp.internal.example.com:8000/mcp"
func main() {
environmentKey := os.Getenv("JUGLOW_ENVIRONMENT_KEY")
environmentID := os.Getenv("JUGLOW_ENVIRONMENT_ID")
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
client := juglow.NewClient(option.WithAuthToken(environmentKey))
// Hubungkan ke server MCP sekali saat startup dan biarkan sesi tetap terbuka selama
// worker berjalan.
mcpClient := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "sandbox-worker", Version: "1.0.0"}, nil)
session, err := mcpClient.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: mcpServerURL}, nil)
if err != nil {
log.Fatalf("connect to MCP server: %v", err)
}
defer session.Close()
listed, err := session.ListTools(ctx, nil)
if err != nil {
log.Fatalf("list MCP tools: %v", err)
}
mcpTools, err := mcp.NewBetaTools(listed.Tools, session)
if err != nil {
log.Fatalf("convert MCP tools: %v", err)
}
worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
EnvironmentID: environmentID,
EnvironmentKey: environmentKey,
Workdir: "/workspace",
ToolsFunc: func(env *agenttoolset.AgentToolContext) []juglow.BetaTool {
return append(agenttoolset.BetaAgentToolset20260401(env), mcpTools...)
},
})
if err := worker.Run(ctx); err != nil {
log.Fatalf("worker: %v", err)
}
}
// EnvironmentWorker saat ini belum tersedia di Java SDK. // EnvironmentWorker saat ini belum tersedia di PHP SDK. # EnvironmentWorker saat ini belum tersedia di Ruby SDK.Perhatikan hal-hal berikut saat Anda membungkus server MCP:
- Alat dideklarasikan, bukan ditemukan saat runtime. Worker mengambil daftar alat-alat server MCP satu kali saat startup dan tidak dapat menambahkan alat ke sesi yang sedang berjalan. Jika alat-alat server berubah, deklarasikan ulang alat tersebut, baik pada agen maupun pada sesi yang idle melalui Memperbarui konfigurasi agen, lalu mulai ulang worker.
- Nama dan deskripsi harus sesuai dengan Managed Agents API. Nama alat kustom harus unik per agen dan hanya menggunakan huruf, angka, garis bawah, dan tanda hubung (1–128 karakter). Deskripsi yang tidak kosong wajib diisi. Array
toolsmilik agen menampung paling banyak 128 entri (setiap alat yang dibungkus dihitung satu entri, dan toolset bawaan dihitung satu entri lagi). API menolak deklarasi yang menggunakan ulang nama alat, menamai alat kustom dengan nama alat agen bawaan sepertibashatauread, atau menggunakan prefiksmcp__yang dicadangkan. Helper MCP mempertahankan nama dan deskripsi dari server, jadi ganti nama atau pangkas jika diperlukan. Jika dua server mengekspos nama alat yang sama, definisikan sendiri pembungkusnya dengan nama berprefiks dan buat pembungkus itu memanggil nama alat asli di server.
- Sebagian besar skema diteruskan tanpa perubahan. API menerima kata kunci JSON Schema yang umum dihasilkan server MCP, seperti
additionalPropertiesdantitle. API menolak kata kunci referensi seperti$refdi bagian mana pun dariinput_schemaalat kustom, jadi jadikan inline skema yang dipisahkan ke$defsoleh generator seperti pydantic. API juga menolakoneOf,anyOf, danallOfdi tingkat atas, serta nama properti yang mengandung karakter selain huruf, angka, garis bawah, titik, dan tanda hubung (1–64 karakter).
- Kegagalan alat muncul sebagai hasil alat berupa error. Jika server MCP melaporkan error alat, worker mengirimkan hasil alat berupa error yang dapat ditanggapi model. Konten MCP yang tidak memiliki padanan hasil alat, seperti blok audio dan tautan sumber daya, juga muncul sebagai error. Tetapkan timeout pada klien MCP agar kegagalan terjadi lebih cepat dan lebih jelas, seperti yang dilakukan contoh worker Python dengan
read_timeout_seconds. Tanpa timeout, panggilan yang macet baru menjadi hasil error ketika timeout permintaan default SDK MCP TypeScript terpicu (sekitar satu menit), atau ketika batas pengaman milik worker sendiri terpicu: sekitar dua setengah menit di Python, dan dua menit di Go, tempat worker membatalkan panggilan alat yang melebihi default 120 detik lalu mengirimkan hasil error.
- Bungkus hanya server yang Anda operasikan atau percayai. Nama, deskripsi, dan hasil alat yang dibungkus masuk ke konteks model seperti alat lainnya. Semua itu merupakan input tidak tepercaya yang dapat memengaruhi apa yang dilakukan agen dengan alat-alat lainnya, termasuk
bashdi host worker. Deklarasikan hanya alat yang memang Anda maksudkan untuk digunakan agen.
- Kebijakan izin tidak berlaku untuk alat kustom. Kebijakan izin mengatur toolset bawaan dan toolset MCP. Worker mengeksekusi setiap panggilan alat yang dibungkus yang dibuat model, jadi tempatkan langkah persetujuan apa pun di kode alat Anda sendiri.
Pemantauan dan operasi
Panggilan-panggilan ini dijalankan dari tooling pemantauan atau operasi Anda, diautentikasi dengan kunci API Haijun Anda, untuk mengamati dan mengelola armada worker. Loop klaim dan keep-alive ditangani di dalam helper worker, jadi Anda tidak memanggil endpoint tersebut secara langsung.
Warning: Endpoint ini menerima kunci API organisasi Anda atau kunci environment. Panggil dari luar host worker dengan kunci API organisasi Anda. Menetapkan
JUGLOW_API_KEYdi host worker mengekspos kredensial bercakupan organisasi ke panggilan alat agen.
Baca kedalaman antrean
work.stats mengembalikan status antrean untuk sebuah environment:
depthadalah jumlah item yang menunggu untuk diklaim. Skalakan armada worker Anda atau buat peringatan atas backlog berdasarkan nilai ini.
pendingadalah jumlah item yang diklaim oleh worker tetapi belum di-acknowledge. Helper worker meng-acknowledge setiap item sebelum memprosesnya, sehingga nilai ini tetap mendekati nol dalam operasi normal; nilai bukan nol yang berkelanjutan berarti sebuah worker macet di antara mengklaim dan meng-acknowledge.
oldest_queued_atadalah timestamp item tertua yang masih berada di antrean, menunggu untuk diklaim atau sudah diklaim tetapi belum di-acknowledge, ataunullketika tidak ada.
workers_pollingadalah jumlah worker yang telah melakukan polling dalam 30 detik terakhir. Gunakan ini untuk peringatan liveness.
curl -sS "https://haijun.my.id/v1/environments/$JUGLOW_ENVIRONMENT_ID/work/stats" \
-H "x-api-key: $JUGLOW_API_KEY" \
-H "juglow-beta: managed-agents-2026-04-01" \
-H "juglow-version: 2023-06-01" ant beta:environments:work stats --environment-id "$JUGLOW_ENVIRONMENT_ID" import os
import juglow
client = juglow.Juglow()
stats = client.beta.environments.work.stats(os.environ["JUGLOW_ENVIRONMENT_ID"])
print(f"depth={stats.depth} pending={stats.pending}") import Juglow from "@juglow-ai/sdk";
const client = new Juglow();
const stats = await client.beta.environments.work.stats(process.env.JUGLOW_ENVIRONMENT_ID!);
console.log(`depth=${stats.depth} pending=${stats.pending}`); using Juglow;
var client = new JuglowClient();
var environmentId = Environment.GetEnvironmentVariable("JUGLOW_ENVIRONMENT_ID")!;
var stats = await client.Beta.Environments.Work.Stats(environmentId);
Console.WriteLine($"depth={stats.Depth} pending={stats.Pending}"); package main
import (
"context"
"fmt"
"os"
"github.com/juglows/juglow-sdk-go"
)
func main() {
client := juglow.NewClient()
environmentID := os.Getenv("JUGLOW_ENVIRONMENT_ID")
stats, err := client.Beta.Environments.Work.Stats(
context.Background(),
environmentID,
juglow.BetaEnvironmentWorkStatsParams{},
)
if err != nil {
panic(err)
}
fmt.Printf("depth=%d pending=%d\n", stats.Depth, stats.Pending)
} import com.juglow.client.JuglowClient;
import com.juglow.client.okhttp.JuglowOkHttpClient;
import com.juglow.models.beta.environments.work.BetaSelfHostedWorkQueueStats;
void main() {
JuglowClient client = JuglowOkHttpClient.fromEnv();
BetaSelfHostedWorkQueueStats stats = client.beta()
.environments()
.work()
.stats(System.getenv("JUGLOW_ENVIRONMENT_ID"));
IO.println("depth=" + stats.depth() + " pending=" + stats.pending());
} <?php
use Juglow\Client;
$client = new Client();
$stats = $client->beta->environments->work->stats(getenv('JUGLOW_ENVIRONMENT_ID'));
printf("depth=%d pending=%d\n", $stats->depth, $stats->pending); require "juglow"
client = Juglow::Client.new
stats = client.beta.environments.work.stats(ENV.fetch("JUGLOW_ENVIRONMENT_ID"))
puts "depth=#{stats.depth} pending=#{stats.pending}"{
"type": "work_queue_stats",
"depth": 0,
"pending": 0,
"oldest_queued_at": null,
"workers_polling": 0
}Hentikan sesi dengan anggun
Gunakan work.stop untuk meminta worker yang menangani sesi tertentu agar mematikannya. Secara default work item berpindah ke stopping: worker menyadarinya pada heartbeat lease berikutnya, membatalkan panggilan alat sesi yang sedang berjalan, dan mengonfirmasi pematian, dan pada titik itu work item menjadi stopped. Teruskan force: true di body permintaan (dengan CLI, teruskan --force) untuk menandai work item sebagai stopped segera alih-alih menunggu konfirmasi worker.
Karena panggilan-panggilan ini dijalankan dari tooling operasi Anda dan bukan dari host worker, JUGLOW_WORK_ID tidak ditetapkan secara otomatis. Tetapkan ke ID work item target sebelum menjalankan contoh-contoh berikut. Untuk menemukan ID work item, daftar work item milik environment melalui endpoint Environments Work.
curl -sS "https://haijun.my.id/v1/environments/$JUGLOW_ENVIRONMENT_ID/work/$JUGLOW_WORK_ID/stop" \
-H "x-api-key: $JUGLOW_API_KEY" \
-H "juglow-beta: managed-agents-2026-04-01" \
-H "juglow-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{}' ant beta:environments:work stop \
--environment-id "$JUGLOW_ENVIRONMENT_ID" \
--work-id "$JUGLOW_WORK_ID" import os
import juglow
client = juglow.Juglow()
work = client.beta.environments.work.stop(
os.environ["JUGLOW_WORK_ID"],
environment_id=os.environ["JUGLOW_ENVIRONMENT_ID"],
)
print(work.state) import Juglow from "@juglow-ai/sdk";
const client = new Juglow();
const work = await client.beta.environments.work.stop(process.env.JUGLOW_WORK_ID!, {
environment_id: process.env.JUGLOW_ENVIRONMENT_ID!
});
console.log(work.state); using Juglow;
var client = new JuglowClient();
var work = await client.Beta.Environments.Work.Stop(
Environment.GetEnvironmentVariable("JUGLOW_WORK_ID")!,
new()
{
EnvironmentID = Environment.GetEnvironmentVariable("JUGLOW_ENVIRONMENT_ID")!
}
);
Console.WriteLine(work.State); package main
import (
"context"
"fmt"
"os"
"github.com/juglows/juglow-sdk-go"
)
func main() {
client := juglow.NewClient()
work, err := client.Beta.Environments.Work.Stop(
context.Background(),
os.Getenv("JUGLOW_WORK_ID"),
juglow.BetaEnvironmentWorkStopParams{
EnvironmentID: os.Getenv("JUGLOW_ENVIRONMENT_ID"),
},
)
if err != nil {
panic(err)
}
fmt.Println(work.State)
} import com.juglow.client.JuglowClient;
import com.juglow.client.okhttp.JuglowOkHttpClient;
import com.juglow.models.beta.environments.work.BetaSelfHostedWork;
import com.juglow.models.beta.environments.work.BetaSelfHostedWorkStopRequest;
import com.juglow.models.beta.environments.work.WorkStopParams;
void main() {
JuglowClient client = JuglowOkHttpClient.fromEnv();
BetaSelfHostedWork work = client.beta().environments().work().stop(
WorkStopParams.builder()
.environmentId(System.getenv("JUGLOW_ENVIRONMENT_ID"))
.workId(System.getenv("JUGLOW_WORK_ID"))
.betaSelfHostedWorkStopRequest(BetaSelfHostedWorkStopRequest.builder().build())
.build()
);
IO.println(work.state());
} <?php
use Juglow\Client;
$client = new Client();
$work = $client->beta->environments->work->stop(
getenv('JUGLOW_WORK_ID'),
environmentID: getenv('JUGLOW_ENVIRONMENT_ID'),
);
echo $work->state . "\n"; require "juglow"
client = Juglow::Client.new
work = client.beta.environments.work.stop(
ENV.fetch("JUGLOW_WORK_ID"),
environment_id: ENV.fetch("JUGLOW_ENVIRONMENT_ID")
)
puts work.stateLangkah selanjutnya
Model tanggung jawab bersama untuk environment sandbox self-hosted.
Buat sesi untuk menjalankan agen Anda dan mulai mengeksekusi tugas.
Hubungkan Haijun dengan aman ke server MCP yang berjalan di jaringan privat Anda tanpa membuka port masuk atau mengekspos layanan ke internet publik.