AI
OpenAI Skills API dan Hosted Shell: SKILL.md di Container
Oktober 202612 menit baca

Ini adalah kumpulan endpoint di bawah /v1/skills untuk menyimpan bundle skill yang bisa dipakai ulang: folder dengan manifest SKILL.md berisi name dan description di YAML front matter, plus scripts, references, dan assets opsional. Skill diberi versi di server lalu dipasang ke shell tool di Responses API, atau ditemukan oleh Agents API, sehingga model mengikuti prosedur terkemas hanya saat dibutuhkan.
Kirim POST ke /v1/skills dalam bentuk multipart, dengan satu form part per file dan path relatif di filename, atau sebagai satu file zip. Batas yang terdokumentasi adalah 50 MB untuk zip, 25 MB setelah diekstrak, dan 500 file per versi, dengan tepat satu SKILL.md per bundle. Versi baru dari skill yang sudah ada dikirim ke endpoint versions milik skill tersebut.
latest_version selalu menunjuk upload terbaru, sedangkan default_version adalah versi yang didapat request yang tidak menyebut versi, dan hanya berubah saat Anda mengaturnya secara eksplisit. Dengan begitu versi baru bisa di-upload dan dievaluasi tanpa memengaruhi production. OpenAI menyarankan pin versi secara eksplisit di request production.
OpenAI mendokumentasikan hosted shell saat ini berbasis Debian 12 dengan Python 3.11, Node.js 22.16, Java 17, PHP 8.2, Ruby 3.1, dan Go 1.23, dan menyebut basisnya bisa berubah. Perintah berjalan tanpa sudo dan tanpa TTY interaktif, dan /mnt/data adalah lokasi resmi untuk output yang bisa diunduh. Akses network keluar mati kecuali Anda mengaktifkannya.
Setiap entri domain_secrets di network_policy mengikat nama dan nilai secret ke satu domain yang diizinkan. Model dan container hanya melihat placeholder seperti $API_KEY, dan auth-translation sidecar milik OpenAI menyisipkan nilai asli hanya pada request ke domain itu. Ini melindungi credential-nya, tetapi Anda tetap butuh allowlist yang ketat dan approval karena konten yang diambil bisa membawa prompt injection.

Ringkasan Utama
OpenAI Skills API menyimpan bundle SKILL.md berversi di /v1/skills dan me-mount-nya ke hosted shell milik Responses API, yaitu container Debian 12 yang secara default tidak punya akses network. Pin versi skill di production, perlakukan perubahan default_version seperti deploy, dan kirim API key lewat domain_secrets supaya model hanya melihat placeholder.
OpenAI Skills API menjawab pertanyaan yang terus muncul di pekerjaan laporan ERP: di mana sebuah prosedur panjang yang berulang seharusnya disimpan? Laporan aging piutang bukan satu tool call. Isinya script, aturan pengelompokan, format output, dan daftar hal yang tidak boleh dilakukan. Menempelkan semua itu ke system prompt di setiap request itu boros dan mustahil diberi versi. Jawaban OpenAI, yang masuk ke Responses API pada Februari 2026, adalah skill: sebuah folder dengan file SKILL.md, di-upload sekali, diberi versi di server, lalu di-mount ke container tempat model bisa menjalankan perintah.
Tulisan ini menelusuri seluruh alurnya dengan dokumentasi resmi OpenAI sebagai acuan: format manifest, endpoint upload beserta batasnya, hubungan default_version dan latest_version, empat cara skill sampai ke runtime, isi sebenarnya dari hosted shell, dan kontrol network yang menentukan apakah skill boleh memanggil ERP Anda tanpa model pernah memegang key-nya. Skill di Claude Code memakai format file yang sama, dan post lain tentang Agents API membahas sisi session; tulisan ini fokus pada model upload, versioning, dan container.
Skill adalah direktori dengan tepat satu SKILL.md di root-nya. File itu diawali YAML front matter berisi dua field wajib, name dan description, lalu diikuti instruksi dalam Markdown. File pendukung diletakkan di sebelahnya sesuai konvensi: scripts untuk aksi berulang, references untuk materi latar, assets untuk template dan data contoh. OpenAI mengikuti spesifikasi terbuka Agent Skills, yang membatasi name maksimal 64 karakter berupa huruf kecil, angka, dan tanda hubung tunggal, mewajibkannya sama dengan nama folder, serta mengizinkan description hingga 1024 karakter.
ar-aging-report/
├── SKILL.md
├── requirements.txt
├── scripts/
│ └── aging.py
└── references/
└── bucket-rules.md
# ar-aging-report/SKILL.md
---
name: ar-aging-report
description: Build an accounts-receivable aging report (0-30, 31-60, 61-90, 90+ days)
from an exported invoices CSV. Use when the user uploads open invoices and asks
for aging, overdue buckets or collection priorities. Do not use for AP or GL data.
---
# AR aging report
## How to run
python scripts/aging.py --input /mnt/data/invoices.csv --outdir /mnt/data/aging
## Rules
- Bucket by due_date, not invoice_date. See references/bucket-rules.md.
- If a dependency is missing, report it. Do not attempt a network install.
## Outputs
- /mnt/data/aging/report.md
- /mnt/data/aging/aging.csvBagian yang paling menentukan adalah description. Saat skill dipasang, platform menambahkan name, description, dan path setiap skill ke konteks prompt, dan model baru membaca SKILL.md lengkap lewat path itu setelah memutuskan skill tersebut relevan. Jadi description harus menjelaskan kapan skill dipakai dan, sama pentingnya, kapan tidak dipakai. Cookbook OpenAI menyarankan contoh negatif justru karena alasan ini, dan spesifikasinya menyarankan isi SKILL.md di bawah 500 baris, dengan detail dipindah ke file reference yang dibuka model saat dibutuhkan.
Ada dua bentuk upload untuk POST /v1/skills. Multipart mengirim satu form part per file, dengan field filename berisi path relatif supaya struktur folder tetap utuh. Zip mengirim seluruh bundle sebagai satu part. Saya akan memakai zip di CI, karena hasilnya satu artifact yang bisa di-hash, disimpan, dan di-upload ulang, dan karena batas ukuran yang terdokumentasi memang dinyatakan untuk bentuk ini.
# Option 1: multipart, one part per file. The filename= carries the folder path,
# so the bundle keeps its layout on the server.
curl --fail-with-body 'https://api.openai.com/v1/skills' \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F 'files[]=@./ar-aging-report/SKILL.md;filename=ar-aging-report/SKILL.md;type=text/markdown' \
-F 'files[]=@./ar-aging-report/scripts/aging.py;filename=ar-aging-report/scripts/aging.py;type=text/plain'
# Option 2: one zip. Easier in CI, and it is what the 50 MB limit applies to.
(cd ar-aging-report/.. && zip -r ar-aging-report.zip ar-aging-report)
curl -X POST 'https://api.openai.com/v1/skills' \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F 'files=@./ar-aging-report.zip;type=application/zip'
# The response carries the skill id plus default_version and latest_version.
# Store the id in config; it is what every request references.Batas 25 MB setelah diekstrak inilah yang paling sering menjebak. Skill yang menyertakan Python wheel atau dataset contoh bisa jauh di bawah 50 MB saat di-zip tetapi tetap gagal. Jangan masukkan input besar ke bundle; kirim sebagai file saat request. Skill seharusnya menjelaskan cara memproses data, bukan membawa datanya.
Setiap skill punya dua pointer. latest_version selalu mengikuti upload terbaru, yang dibuat lewat POST /v1/skills/skill_id/versions. default_version adalah versi yang didapat request yang tidak menyebut versi, dan hanya berubah kalau Anda mengubahnya secara eksplisit di skill. Pemisahan inilah inti model rilisnya: upload sebebasnya, evaluasi versi baru dengan pin di request uji, lalu promosikan.
# Ship a new version. This moves latest_version, NOT default_version.
curl -X POST "https://api.openai.com/v1/skills/$SKILL_ID/versions" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F 'files=@./ar-aging-report.zip;type=application/zip'
# Promote it once the eval run passes. Requests that omit "version"
# switch over at this moment, so treat it like a deploy.
curl -X POST "https://api.openai.com/v1/skills/$SKILL_ID" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{"default_version": 3}'
# Rollback is the same call with the old number: {"default_version": 2}skill_reference menerima versi berupa integer atau string latest. Cookbook OpenAI menyarankan pin versi di production alih-alih mengikuti latest, serta pin model bersama skill agar sebuah run bisa direproduksi. Saya akan melangkah lebih jauh: perlakukan perubahan default_version sebagai deploy yang dicatat di changelog, karena setiap pemanggil yang tidak menyebut versi langsung berubah perilakunya saat itu juga, tanpa ada perubahan code yang bisa ditunjuk.
Ada dua aturan penghapusan yang perlu diketahui sebelum Anda menulis script cleanup. Versi yang sedang menjadi default_version tidak bisa dihapus sebelum default_version dipindah ke versi lain, dan menghapus versi terakhir yang tersisa akan menghapus skill itu sendiri, termasuk id-nya. Loop yang asal memangkas versi lama bisa ikut menghapus skill_id production.
Upload sebenarnya opsional. Bundle SKILL.md yang sama bisa sampai ke model lewat empat jalur, dan pilihan itu menentukan di mana file berada dan berapa lama bertahan.
| Jalur | Cara skill dipasang | Masa hidup | Pakai saat |
|---|---|---|---|
| Hosted shell, container_auto | skill_reference dengan skill_id dan version opsional di tools.environment.skills | Container baru per request, disediakan OpenAI | Job sekali jalan, misalnya membuat laporan dari file upload |
| Hosted shell, container_reference | Skill di-mount ke container yang dibuat lewat Containers API, lalu dirujuk dengan container_id | Sampai dihapus atau kedaluwarsa, 20 menit setelah aktivitas terakhir di contoh dokumentasi | Pekerjaan multi-turn yang butuh file dan package terpasang tetap ada |
| Bundle inline | Zip berenkode base64 di request, type inline, dengan name dan description | Hanya container tempat ia dikirim | Prototyping, atau skill yang dibuat per tenant dan tidak ingin disimpan |
| Local shell | name, description, dan path di filesystem; skill_reference tidak didukung | Mesin atau server Anda sendiri | Data yang wajib tetap on-premise; Anda sendiri yang menjalankan perintahnya |
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-6-astra",
tools=[
{
"type": "shell",
"environment": {
"type": "container_auto",
"skills": [
# Wrong in production: no version means default_version,
# which someone can move without touching this code.
# {"type": "skill_reference", "skill_id": AGING_SKILL_ID},
# Right: pin the version you evaluated.
{"type": "skill_reference", "skill_id": AGING_SKILL_ID, "version": 3},
# A curated first-party skill, referenced by its id.
{"type": "skill_reference", "skill_id": "openai-spreadsheets", "version": "latest"},
],
},
}
],
input="Build the AR aging report from the attached invoices export.",
)
print(response.output_text)Baris yang di-comment di request itu adalah kesalahan yang paling mungkin dibuat tim di awal: berjalan normal, lolos review, lalu diam-diam mengikuti apa pun default_version bulan depan. Skill kurasi openai-spreadsheets menunjukkan sisi lain dari trade-off ini, di mana mengikuti latest masuk akal karena OpenAI yang memeliharanya dan bukan Anda yang mengevaluasi setiap rilis. Ingat juga bahwa skill di hosted shell dibuang saat container kedaluwarsa, jadi apa pun yang di-mount bukan penyimpanan permanen.
OpenAI mendokumentasikan runtime hosted saat ini berbasis Debian 12, dengan Python 3.11, Node.js 22.16, Java 17, PHP 8.2, Ruby 3.1, dan Go 1.23 terpasang, dan menyebut basisnya bisa berubah seiring waktu. Perintah berjalan tanpa sudo dan tanpa TTY interaktif, jadi script skill yang meminta input atau memasang package sistem akan macet atau gagal. /mnt/data selalu tersedia dan merupakan lokasi resmi untuk apa pun yang perlu bisa diunduh pengguna; artifact di sana diambil dengan container files API yang sama dengan code interpreter.
// What the model emits: a batch of commands with its own limits.
{
"type": "shell_call",
"call_id": "call_9d14...",
"action": {
"commands": ["python scripts/aging.py --input /mnt/data/invoices.csv --outdir /mnt/data/aging"],
"timeout_ms": 120000,
"max_output_length": 4096
},
"status": "in_progress"
}
// What comes back. Log the exit code, not just stdout: a non-zero exit
// with a confident final answer is the failure you want an alert on.
{
"type": "shell_call_output",
"call_id": "call_9d14...",
"output": [
{ "stdout": "...", "stderr": "...", "outcome": { "type": "exit", "exit_code": 0 } }
]
}Setiap shell_call bisa membawa beberapa perintah dengan timeout_ms dan max_output_length per call, dan setiap hasil melaporkan stdout, stderr, serta outcome exit. Saat membuat container sendiri, Anda juga menentukan memory_limit dan expires_after, misalnya satu gigabyte dan 20 menit sejak last_active_at, dan Anda bisa melakukan DELETE begitu job selesai alih-alih membayar waktu idle. Shell tool hanya tersedia lewat Responses API, tidak lewat Chat Completions.
Tulis script skill agar mencetak satu baris ringkasan terstruktur saat sukses dan error yang jelas saat gagal. Output dipotong di max_output_length, dan model memutuskan langkah berikutnya dari bagian yang tersisa. Script yang menumpahkan sepuluh ribu baris ke stdout tidak mengajari model apa pun dan membuang satu turn.
Container hosted secara default tidak punya akses network keluar. Untuk mengaktifkannya ada dua lapis: admin mengatur allowlist organisasi di dashboard, lalu setiap request menambahkan network_policy bertipe allowlist di dalam environment shell. Daftar di request hanya bisa mempersempit daftar organisasi; request yang menyebut domain di luar daftar itu akan gagal, bukan diam-diam memperluas akses, dan memang begitulah perilaku yang diharapkan dari sebuah kontrol keamanan.
"tools": [
{
"type": "shell",
"environment": {
"type": "container_auto",
"network_policy": {
"type": "allowlist",
// Must be a subset of the org allowlist set in the dashboard,
// or the request fails rather than silently widening access.
"allowed_domains": ["pypi.org", "files.pythonhosted.org", "erp.example.co.id"],
"domain_secrets": [
{
"domain": "erp.example.co.id",
"name": "ERP_API_KEY",
// The model and the container only ever see $ERP_API_KEY.
// The real value is applied by OpenAI's auth sidecar on the way
// out, and only for this domain.
"value": "<read from your secret manager at request time>"
}
]
}
}
}
]domain_secrets membuat skill yang memanggil API privat jadi bisa diterima. Setiap entri mengikat name dan value ke satu domain. Model dan runtime hanya melihat placeholder, misalnya $ERP_API_KEY, dan auth-translation sidecar milik OpenAI mengganti dengan nilai asli hanya pada request ke tujuan yang disetujui. Script skill tetap bisa autentikasi ke ERP Anda, sementara model yang terkena prompt injection lalu mencetak environment-nya hanya mencetak placeholder, dan request ke host lain tidak membawa apa pun yang berguna.
domain_secrets melindungi key, bukan data. Panduan OpenAI memperingatkan bahwa konten apa pun yang diambil lewat network bisa berisi instruksi tersembunyi yang ditujukan ke model, dan panduan skill-nya menyebut prompt injection dan exfiltration data sebagai risiko utama. Batasi allowlist hanya ke domain yang benar-benar dibutuhkan skill, utamakan credential read-only, dan pasang approval eksplisit untuk aksi tulis.
Agents API menemukan skill dengan cara berbeda. Alih-alih daftar referensi, environment sebuah session menyebut capability_directories, lalu platform mencari file SKILL.md di dalamnya. Path harus absolut tanpa segmen titik, harus sudah ada di sandbox, dan satu session bisa mendaftarkan paling banyak 32 direktori.
{
"environment": {
"type": "self_hosted",
"workspace_directory": "/workspace",
"capability_directories": [
"/workspace/capabilities/finance",
"/workspace/capabilities/inventory"
]
}
}
// Rules the platform enforces:
// absolute paths only, no "." or ".." segments
// at most 32 directories per session
// each directory must already exist in the sandbox
// Any SKILL.md found under them is registered as a skill.Batas itu memengaruhi cara Anda menata skill. Tiga puluh dua direktori sangat cukup kalau setiap direktori adalah satu domain, misalnya finance, inventory, atau engineering, yang berisi beberapa skill, tetapi sempit kalau setiap skill punya direktori top-level sendiri. Pengelompokan per domain juga memberi unit permission yang alami: session agent sales cukup tidak me-mount direktori finance, sehingga skill-skill itu sama sekali tidak muncul di prompt-nya.
Cookbook OpenAI menarik garisnya dengan jelas: system prompt berisi perilaku global yang selalu aktif; tool melakukan sesuatu di dunia nyata dengan side effect; skill adalah prosedur terkemas yang dipanggil model secara kondisional, dengan script dan template yang dijalankan di sandbox. Laporan aging piutang, checklist rekonsiliasi akhir bulan, atau format export standar adalah skill. Posting jurnal adalah tool, dengan approval gate sendiri. Urutan rilis skill baru versi saya:
Format SKILL.md yang sama dipakai oleh OpenAI, Claude Code, dan tool lain yang mengikuti spesifikasi Agent Skills, jadi folder skill yang ditulis dengan baik bersifat portabel. Yang tidak ikut pindah adalah infrastruktur di sekitarnya: skill id, pointer versi, mode container, dan network policy adalah fitur khusus API OpenAI.
Model mental yang berguna: skill adalah artifact yang di-deploy, bukan prompt. Ia punya id, versi bernomor, langkah promosi, runtime dengan bahasa dan batas yang diketahui, serta batas network yang Anda konfigurasi. Perlakukan dengan disiplin yang sama seperti deploy lainnya: pin apa yang sudah diuji, promosikan dengan sengaja, dan jangan pernah biarkan credential sampai ke model sebagai teks.