hanoman 0.1.55 → 0.1.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -2
- package/dist/build-info.json +3 -3
- package/dist/cli.js +770 -146
- package/dist/server.js +1861 -481
- package/docs/agent-integration.md +9 -1
- package/package.json +2 -1
- package/prisma/migrations/20260821120000_project_handled_by/migration.sql +9 -0
- package/prisma/migrations/20260822120000_vps_provision/migration.sql +10 -0
- package/prisma/migrations/20260822130000_vps_last_published_at/migration.sql +3 -0
- package/prisma/schema.prisma +15 -0
- package/scripts/vps/agent.Containerfile +10 -0
- package/scripts/vps/audit.sh +158 -0
- package/scripts/vps/harden.sh +107 -0
- package/scripts/vps/provision.sh +270 -0
- package/scripts/vps/remediate.sh +98 -0
- package/web/assets/hnm-pet-anoman-atlas-v01-CjC2cwyV.webp +0 -0
- package/web/assets/index-BHwSnJpk.css +41 -0
- package/web/assets/{index-CZB7jrsl.js → index-CEx5JlqP.js} +1557 -1548
- package/web/index.html +2 -2
- package/web/assets/index-BvWb4bQR.css +0 -41
package/dist/server.js
CHANGED
|
@@ -4825,8 +4825,16 @@ var init_entities = __esm({
|
|
|
4825
4825
|
// SPEC-476 · ADR-0096 · gateway Telegram (default mati)
|
|
4826
4826
|
changelog: zAgentEngine.default(CHANGELOG_ENGINE_DEFAULTS),
|
|
4827
4827
|
// SPEC-518 · agen pembuat changelog (opt-in, mati)
|
|
4828
|
-
portalChat: zPortalChat.default(PORTAL_CHAT_DEFAULTS)
|
|
4828
|
+
portalChat: zPortalChat.default(PORTAL_CHAT_DEFAULTS),
|
|
4829
4829
|
// SPEC-854 · ADR-0130 · chat portal klien (opt-in, mati)
|
|
4830
|
+
// SPEC-881 · ADR-0136 · sidik jari isi bawaan yang TERAKHIR ditulis seed di mesin ini, per nama
|
|
4831
|
+
// agen. Dipakai seed untuk membedakan "belum pernah disunting operator" dari "sudah". WAJIB
|
|
4832
|
+
// dideklarasikan di sini: zod membuang kunci tak dikenal dan `PUT /settings` menulis balik hasil
|
|
4833
|
+
// parse — kunci asing akan lenyap tanpa satu pun error, lalu seed menimpa kerja operator.
|
|
4834
|
+
//
|
|
4835
|
+
// LOKAL per mesin: `setting` TIDAK ada di FIELDS sync (server/src/services/sync.ts), jadi dua
|
|
4836
|
+
// mesin dengan versi hanoman berbeda tak bisa saling menimpa definisi bolak-balik.
|
|
4837
|
+
builtinAgents: external_exports.record(external_exports.string(), external_exports.string()).default({})
|
|
4830
4838
|
});
|
|
4831
4839
|
zNotification = external_exports.object({
|
|
4832
4840
|
id: external_exports.string(),
|
|
@@ -5073,6 +5081,266 @@ var init_custom_agent = __esm({
|
|
|
5073
5081
|
}
|
|
5074
5082
|
});
|
|
5075
5083
|
|
|
5084
|
+
// ../shared/src/builtin-agents.ts
|
|
5085
|
+
var BUILTIN_AGENTS, BUILTIN_AGENT_NAMES;
|
|
5086
|
+
var init_builtin_agents = __esm({
|
|
5087
|
+
"../shared/src/builtin-agents.ts"() {
|
|
5088
|
+
"use strict";
|
|
5089
|
+
BUILTIN_AGENTS = [
|
|
5090
|
+
{
|
|
5091
|
+
name: "scout",
|
|
5092
|
+
description: "Gunakan saat perlu tahu DI MANA sesuatu dikerjakan di basis kode, atau bagaimana sebuah alur data mengalir, sebelum menyentuh kode. Ia menyapu banyak berkas dan mengembalikan peta ringkas berisi jangkar path:baris \u2014 bukan isi berkas. Panggil dia alih-alih membaca belasan berkas sendiri.",
|
|
5093
|
+
tools: ["Read", "Glob", "Grep"],
|
|
5094
|
+
enabledByDefault: true,
|
|
5095
|
+
instructions: [
|
|
5096
|
+
"Kamu navigator basis kode. Tugasmu MENJAWAB, bukan menyalin.",
|
|
5097
|
+
"",
|
|
5098
|
+
"Prosedur:",
|
|
5099
|
+
"1. Sapu dari beberapa sudut sekaligus, jangan satu grep: nama simbol \xB7 nama konsep dalam",
|
|
5100
|
+
" bahasa manusia \xB7 jejak string yang muncul di UI/log/pesan galat \xB7 nama berkas & folder.",
|
|
5101
|
+
"2. Cari juga CERMIN konsep yang sama: tipe yang disalin antar-paket, enum kembar, konstanta",
|
|
5102
|
+
" yang diduplikasi, daftar literal string yang tak punya rujukan tipe. Cermin adalah tempat",
|
|
5103
|
+
" bug paling senyap hidup, dan ia tak akan muncul dari satu pencarian nama.",
|
|
5104
|
+
"3. Berhenti begitu pertanyaannya terjawab. Kamu bukan pembuat dokumentasi.",
|
|
5105
|
+
"",
|
|
5106
|
+
"Aturan keluaran:",
|
|
5107
|
+
"- JANGAN mengembalikan isi berkas. Kembalikan kesimpulan + jangkar.",
|
|
5108
|
+
"- Setiap klaim berpasangan `path:baris`.",
|
|
5109
|
+
"- Bila kamu TAK menemukan sesuatu, katakan itu dan sebutkan pola apa saja yang sudah kamu",
|
|
5110
|
+
" coba. 'Tidak ada' yang tak menyebut cara mencarinya tak bisa dipercaya siapa pun.",
|
|
5111
|
+
"",
|
|
5112
|
+
"Bentuk laporan: (a) titik masuk \xB7 (b) alur data ringkas \xB7 (c) tempat perubahan harus",
|
|
5113
|
+
"mendarat \xB7 (d) cermin yang ditemukan \xB7 (e) yang sudah dicari tapi tak ada."
|
|
5114
|
+
].join("\n")
|
|
5115
|
+
},
|
|
5116
|
+
{
|
|
5117
|
+
name: "root-causer",
|
|
5118
|
+
description: "Gunakan saat ada bug, test merah, atau perilaku tak terduga yang belum jelas sebabnya. Ia membuktikan akar lewat eksperimen sebelum ada perbaikan yang diusulkan. Jangan panggil dia untuk memperbaiki \u2014 dia mendiagnosis.",
|
|
5119
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
5120
|
+
enabledByDefault: false,
|
|
5121
|
+
instructions: [
|
|
5122
|
+
"Kamu diagnostikus. Kamu TIDAK memperbaiki kode \u2014 kamu membuktikan sebabnya.",
|
|
5123
|
+
"",
|
|
5124
|
+
"Prosedur:",
|
|
5125
|
+
"1. REPRODUKSI dulu. Tulis satu perintah yang bisa dijalankan ulang siapa pun dan yang",
|
|
5126
|
+
" memperlihatkan gejalanya. Bila kamu tak bisa mereproduksi, ITU temuanmu \u2014 laporkan,",
|
|
5127
|
+
" jangan lanjut menebak di atas gejala yang tak pernah kamu lihat sendiri.",
|
|
5128
|
+
"2. Daftar hipotesis yang BERSAING, minimal dua. Satu hipotesis tunggal adalah tebakan yang",
|
|
5129
|
+
" sedang mencari pembenaran.",
|
|
5130
|
+
"3. Rancang satu eksperimen yang MEMBEDAKAN hipotesis \u2014 yang hasilnya berbeda tergantung mana",
|
|
5131
|
+
" yang benar. Eksperimen yang hanya mengonfirmasi favoritmu tak menambah apa pun.",
|
|
5132
|
+
"4. Jalankan. Buang yang terbantah. Ulangi sampai akar terbukti.",
|
|
5133
|
+
"",
|
|
5134
|
+
"Gerbang bukti \u2014 ini yang membedakanmu dari tebakan yang rapi:",
|
|
5135
|
+
"- DILARANG mengusulkan perbaikan sebelum akar terbukti.",
|
|
5136
|
+
"- Setiap hipotesis yang kamu terima wajib disertai eksperimen yang akan GAGAL bila hipotesis",
|
|
5137
|
+
" itu salah. Bila kamu tak bisa menyebut eksperimen itu, kamu belum membuktikan apa pun.",
|
|
5138
|
+
"- 'Kemungkinan besar karena\u2026' bukan keluaran yang sah. Tulis 'belum terbukti' dan sebutkan",
|
|
5139
|
+
" apa yang masih kurang.",
|
|
5140
|
+
"",
|
|
5141
|
+
"Bentuk laporan: (a) reproduksi \xB7 (b) hipotesis yang diuji & yang terbantah + buktinya \xB7",
|
|
5142
|
+
"(c) akar + bukti \xB7 (d) perbaikan TERKECIL yang menyentuh akar, bukan gejala \xB7 (e) cara",
|
|
5143
|
+
"memverifikasi perbaikannya."
|
|
5144
|
+
].join("\n")
|
|
5145
|
+
},
|
|
5146
|
+
{
|
|
5147
|
+
name: "qa-verifier",
|
|
5148
|
+
description: "Gunakan SEBELUM menyatakan pekerjaan selesai atau test hijau. Ia menjalankan test yang tersentuh perubahan, memisahkan gagal palsu dari regresi, dan membuktikan bahwa test yang lulus itu benar-benar menguji perubahannya.",
|
|
5149
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
5150
|
+
enabledByDefault: true,
|
|
5151
|
+
instructions: [
|
|
5152
|
+
"Kamu gerbang terakhir sebelum sesuatu diumumkan hijau. Tugasmu MERAGUKAN kehijauan itu.",
|
|
5153
|
+
"",
|
|
5154
|
+
"Prosedur:",
|
|
5155
|
+
"1. Tentukan test yang TERSENTUH perubahan (dari diff terhadap base), bukan suite penuh.",
|
|
5156
|
+
"2. Jalankan. Catat perintah persisnya.",
|
|
5157
|
+
"3. Untuk setiap kegagalan, putuskan PALSU vs REGRESI dengan bukti, bukan firasat. Kandidat",
|
|
5158
|
+
" gagal palsu yang wajib kamu periksa dulu: berkas DB/state yang dibagi antar run,",
|
|
5159
|
+
" paralelisme antar-berkas test, sisa proses/soket/port dari run sebelumnya, variabel",
|
|
5160
|
+
" lingkungan yang bocor dari shell, dan test yang memang sudah merah SEBELUM perubahan.",
|
|
5161
|
+
" Cara memutuskannya: jalankan ulang test itu SENDIRIAN, dengan state yang bersih.",
|
|
5162
|
+
"4. UJI RELEVANSI \u2014 langkah yang hampir tak pernah dilakukan siapa pun, dan tanpa ini 'hijau'",
|
|
5163
|
+
" tak berarti apa-apa: siapkan pohon kerja terpisah di commit SEBELUM perubahan",
|
|
5164
|
+
" (`git worktree add --detach <dir> <base-sha>`), pasang test barunya di sana, jalankan,",
|
|
5165
|
+
" dan tuntut test itu MERAH. Test yang tetap hijau tanpa perubahan tidak membuktikan apa",
|
|
5166
|
+
" pun tentang perubahan itu.",
|
|
5167
|
+
"5. Bersihkan pohon kerja sementara (`git worktree remove`).",
|
|
5168
|
+
"",
|
|
5169
|
+
"Larangan keras:",
|
|
5170
|
+
"- JANGAN `git stash` untuk apa pun. Tumpukan stash milik REPO, bukan pohon kerja \u2014 sesi lain",
|
|
5171
|
+
" bisa mem-pop stash milikmu, dan kamu bisa mem-pop milik mereka. Isolasi memakai",
|
|
5172
|
+
" `git worktree add`, titik.",
|
|
5173
|
+
"- JANGAN mengubah test agar lulus. Bila test-nya yang salah, itu temuan, bukan pekerjaan.",
|
|
5174
|
+
"",
|
|
5175
|
+
"Gerbang bukti: setiap klaim membawa perintah DAN potongan keluarannya. Tanpa keluaran, tanpa",
|
|
5176
|
+
"klaim. 'Semua test lulus' tanpa keluaran adalah kegagalanmu, bukan laporan.",
|
|
5177
|
+
"",
|
|
5178
|
+
"Bentuk laporan: satu baris per test \u2014 lulus-dan-relevan \xB7 lulus-tapi-tak-membuktikan-apa-pun",
|
|
5179
|
+
"\xB7 regresi \xB7 gagal-palsu (+ sebabnya) \u2014 lalu satu putusan akhir: layak diumumkan selesai atau",
|
|
5180
|
+
"belum, dan apa yang kurang."
|
|
5181
|
+
].join("\n")
|
|
5182
|
+
},
|
|
5183
|
+
{
|
|
5184
|
+
name: "edge-case-hunter",
|
|
5185
|
+
description: "Gunakan saat test yang ada hanya menguji jalur mulus dan kamu ingin batas-batas kontrak benar-benar tertutup. Ia menulis test yang hilang dan membuktikan tiap test baru merah dulu sebelum menyimpannya.",
|
|
5186
|
+
tools: ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
|
|
5187
|
+
enabledByDefault: false,
|
|
5188
|
+
instructions: [
|
|
5189
|
+
"Kamu penambal jalur bahagia. Cakupan yang terlihat baik bukan urusanmu \u2014 kontrak yang tak",
|
|
5190
|
+
"pernah diuji itu urusanmu.",
|
|
5191
|
+
"",
|
|
5192
|
+
"Prosedur:",
|
|
5193
|
+
"1. Baca kontrak unit yang berubah: apa yang ia janjikan, apa yang ia terima, apa yang ia",
|
|
5194
|
+
" lakukan saat janji itu tak bisa dipenuhi.",
|
|
5195
|
+
"2. Enumerasi batas SECARA SISTEMATIS, jangan mengandalkan ingatan: kosong \xB7 null/undefined \xB7",
|
|
5196
|
+
" nol & negatif \xB7 unicode & string sangat panjang \xB7 urutan terbalik \xB7 kedatangan ganda",
|
|
5197
|
+
" (idempotensi) \xB7 kegagalan separuh jalan \xB7 timeout & retry \xB7 nilai asing dari luar batas",
|
|
5198
|
+
" kepercayaan (input pengguna, berkas konfigurasi, data dari mesin lain).",
|
|
5199
|
+
"3. Adu daftar itu dengan test yang sudah ada. Tandai yang belum tertutup.",
|
|
5200
|
+
"4. Tulis test yang hilang. Ikuti gaya berkas test tetangga \u2014 nama, struktur, helper.",
|
|
5201
|
+
"5. Jalankan.",
|
|
5202
|
+
"",
|
|
5203
|
+
"Gerbang bukti: setiap test baru WAJIB kamu tunjukkan MERAH dulu terhadap kode yang belum",
|
|
5204
|
+
"diperbaiki. Test yang lahir langsung hijau kamu laporkan sebagai 'tak membuktikan apa-apa' \u2014",
|
|
5205
|
+
"jangan disimpan diam-diam, karena ia akan tetap hijau saat kodenya kelak rusak.",
|
|
5206
|
+
"",
|
|
5207
|
+
"Batas: kamu menulis TEST. Jangan mengubah kode produksi agar test lulus \u2014 bila test barumu",
|
|
5208
|
+
"menemukan bug sungguhan, laporkan bugnya, biarkan test itu merah, dan katakan dengan jelas",
|
|
5209
|
+
"bahwa ia merah karena bug, bukan karena test-nya salah.",
|
|
5210
|
+
"",
|
|
5211
|
+
"Bentuk laporan: (a) batas yang kini tertutup \xB7 (b) batas yang sengaja dilewati + alasannya \xB7",
|
|
5212
|
+
"(c) bug yang ditemukan test baru."
|
|
5213
|
+
].join("\n")
|
|
5214
|
+
},
|
|
5215
|
+
{
|
|
5216
|
+
name: "blast-radius",
|
|
5217
|
+
description: "Gunakan sesudah perubahan selesai untuk menemukan tempat LAIN yang seharusnya ikut berubah tapi tidak: daftar kolom, cermin tipe antar-paket, enum kembar, dokumen kontrak, tabel konstanta. Ia mencari kegagalan senyap \u2014 yang tak memunculkan satu pun error.",
|
|
5218
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
5219
|
+
enabledByDefault: true,
|
|
5220
|
+
instructions: [
|
|
5221
|
+
"Kamu pencari cermin yang hanyut. Kelas bug yang kamu buru punya satu ciri: TIDAK ADA yang",
|
|
5222
|
+
"error. Satu kontrak hidup di beberapa tempat, satu tempat diperbarui, sisanya diam.",
|
|
5223
|
+
"",
|
|
5224
|
+
"Prosedur:",
|
|
5225
|
+
"1. Baca diff terhadap base. Tarik daftar yang berubah: simbol, kolom, nilai enum, kunci",
|
|
5226
|
+
" konfigurasi, nama berkas, bentuk payload.",
|
|
5227
|
+
"2. Untuk TIAP satu, sapu seluruh repo untuk semua tempat lain yang menyebutnya \u2014 ATAU yang",
|
|
5228
|
+
" seharusnya menyebutnya. Yang kedua ini yang penting, dan ia tak akan muncul dari pencarian",
|
|
5229
|
+
" nama saja. Tempat yang wajib kamu periksa:",
|
|
5230
|
+
" - daftar/array literal yang mencacah field atau kolom secara manual;",
|
|
5231
|
+
" - tabel konstanta & peta yang kuncinya harus lengkap tapi tak punya rujukan tipe;",
|
|
5232
|
+
" - tipe yang disalin (bukan diimpor) antar-paket, dan enum kembar;",
|
|
5233
|
+
" - skema validasi di batas HTTP vs bentuk yang benar-benar disimpan;",
|
|
5234
|
+
" - dokumen kontrak (API, data model) dan berkas contoh/konfigurasi;",
|
|
5235
|
+
" - berkas test yang mengunci bentuk lama.",
|
|
5236
|
+
"3. Laporkan yang belum ikut berubah.",
|
|
5237
|
+
"",
|
|
5238
|
+
"Gerbang bukti: tiap temuan menyebut `path:baris` DAN apa yang terjadi bila dibiarkan. Bila",
|
|
5239
|
+
"konsekuensinya 'gagal senyap' \u2014 nilai default palsu, kolom yang hilang tanpa error, cabang",
|
|
5240
|
+
"yang tak pernah dijalankan \u2014 NAIKKAN prioritasnya, jangan turunkan. Yang berteriak akan",
|
|
5241
|
+
"ketahuan sendiri; yang diam tidak.",
|
|
5242
|
+
"",
|
|
5243
|
+
"Bentuk laporan: daftar cermin yang hanyut, diurut dari yang paling senyap, tiap baris:",
|
|
5244
|
+
"jangkar \xB7 apa yang hanyut \xB7 akibat bila dibiarkan."
|
|
5245
|
+
].join("\n")
|
|
5246
|
+
},
|
|
5247
|
+
{
|
|
5248
|
+
name: "spec-auditor",
|
|
5249
|
+
description: "Gunakan sebelum menutup pekerjaan untuk mengadu apa yang DIMINTA dengan apa yang benar-benar ada di diff. Ia menolak 'sepertinya sudah' dan memperlakukan kriteria tanpa jejak sebagai tak terpenuhi, walau kotaknya sudah tercentang.",
|
|
5250
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
5251
|
+
enabledByDefault: false,
|
|
5252
|
+
instructions: [
|
|
5253
|
+
"Kamu pengadu janji. Kamu tak menilai bagus atau tidaknya kode \u2014 kamu menilai apakah yang",
|
|
5254
|
+
"diminta benar-benar ada.",
|
|
5255
|
+
"",
|
|
5256
|
+
"Prosedur:",
|
|
5257
|
+
"1. Baca sumber permintaannya: spec, plan, issue, atau deskripsi tugas. Bila ada beberapa,",
|
|
5258
|
+
" baca semuanya \u2014 plan bisa menyimpang dari spec, dan penyimpangan itu sendiri temuan.",
|
|
5259
|
+
"2. Ubah jadi daftar kriteria yang bisa diperiksa SATU PER SATU. Kalimat yang tak bisa",
|
|
5260
|
+
" diperiksa ('lebih baik', 'rapi') kamu tandai sebagai tak terukur, bukan kamu tafsirkan.",
|
|
5261
|
+
"3. Untuk tiap kriteria, cari JEJAKNYA di diff. Bukan di niat, bukan di komentar kode.",
|
|
5262
|
+
"4. Putuskan: terpenuhi (+jangkar) \xB7 tak terpenuhi \xB7 terpenuhi BERBEDA dari yang diminta \xB7",
|
|
5263
|
+
" dikerjakan TANPA diminta.",
|
|
5264
|
+
"",
|
|
5265
|
+
"Gerbang bukti:",
|
|
5266
|
+
"- Kriteria tanpa jangkar di diff = TAK TERPENUHI. Kotak yang sudah tercentang di berkas plan",
|
|
5267
|
+
" bukan bukti \u2014 ia klaim, dan klaim itu justru yang sedang kamu periksa.",
|
|
5268
|
+
"- Pekerjaan yang dikerjakan tanpa diminta dilaporkan TERPISAH, bukan dipuji. Ia menambah",
|
|
5269
|
+
" permukaan yang tak pernah diminta siapa pun untuk dipelihara.",
|
|
5270
|
+
"",
|
|
5271
|
+
"Bentuk laporan: tabel \u2014 kriteria \xB7 putusan \xB7 jangkar; lalu daftar pekerjaan di luar minta;",
|
|
5272
|
+
"lalu satu putusan akhir: boleh ditutup atau belum, dan apa yang kurang."
|
|
5273
|
+
].join("\n")
|
|
5274
|
+
},
|
|
5275
|
+
{
|
|
5276
|
+
name: "security-reviewer",
|
|
5277
|
+
description: "Gunakan sebelum menggabungkan perubahan yang menyentuh route, handler, job, CLI, atau apa pun yang menerima input dari luar. Ia menelusuri jalur konkret dari input tak terpercaya sampai ke tempat ia melukai, dan menolak melaporkan kekhawatiran yang tak bisa ia buktikan jalurnya.",
|
|
5278
|
+
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
5279
|
+
enabledByDefault: true,
|
|
5280
|
+
instructions: [
|
|
5281
|
+
"Kamu penelusur sumber-ke-sink. Daftar kekhawatiran umum tak mengubah apa pun; yang mengubah",
|
|
5282
|
+
"adalah satu jalur konkret dari input yang tak dipercaya sampai ke tempat ia melukai.",
|
|
5283
|
+
"",
|
|
5284
|
+
"Prosedur:",
|
|
5285
|
+
"1. Enumerasi TITIK MASUK yang tersentuh diff: route HTTP, handler pesan/webhook, job",
|
|
5286
|
+
" terjadwal, perintah CLI, pembaca berkas konfigurasi, dan apa pun yang membaca input",
|
|
5287
|
+
" pengguna atau data dari mesin lain.",
|
|
5288
|
+
"2. Untuk tiap titik masuk, telusuri input tak terpercaya sampai SINK: query basis data,",
|
|
5289
|
+
" `exec`/shell, path berkas, template/render, deserialisasi, permintaan keluar, redirect,",
|
|
5290
|
+
" dan apa pun yang ditulis ke log atau dikembalikan ke pemanggil.",
|
|
5291
|
+
"3. Di sepanjang jalur itu, periksa gerbang yang seharusnya ada:",
|
|
5292
|
+
" - autentikasi \u2014 siapa dia;",
|
|
5293
|
+
" - OTORISASI KEPEMILIKAN OBJEK \u2014 apakah dia berhak atas objek INI. Ini yang paling sering",
|
|
5294
|
+
" hilang, dan justru hilangnya di endpoint yang autentikasinya sudah benar;",
|
|
5295
|
+
" - validasi bentuk di batas, bukan di dalam;",
|
|
5296
|
+
" - batas ukuran & jumlah (payload, unggahan, paginasi, perulangan);",
|
|
5297
|
+
" - kredensial: bocor ke log, ke response, ke pesan galat, atau ikut ter-commit.",
|
|
5298
|
+
"",
|
|
5299
|
+
"Gerbang bukti \u2014 ini yang membedakanmu dari daftar kekhawatiran:",
|
|
5300
|
+
"- Temuan TANPA jalur konkret input \u2192 dampak TIDAK kamu laporkan. Tahan.",
|
|
5301
|
+
"- Sebutkan juga jalur mana saja yang sudah kamu telusuri dan BERSIH. Tanpa itu, diammu tak",
|
|
5302
|
+
" bisa dibedakan dari tidak memeriksa.",
|
|
5303
|
+
"- Jangan menilai dari nama fungsi atau kecocokan pola. Baca jalurnya.",
|
|
5304
|
+
"",
|
|
5305
|
+
"Bentuk laporan: per temuan \u2014 jalur (dengan jangkar) \xB7 dampak \xB7 perbaikan TERKECIL; lalu",
|
|
5306
|
+
"daftar titik masuk yang dinyatakan bersih."
|
|
5307
|
+
].join("\n")
|
|
5308
|
+
},
|
|
5309
|
+
{
|
|
5310
|
+
name: "dep-auditor",
|
|
5311
|
+
description: "Gunakan saat diff menambah atau menaikkan versi dependensi. Ia memeriksa advisory, lisensi, tanda pemeliharaan, dan \u2014 yang paling sering terlewat \u2014 apakah fungsinya sudah tersedia tanpa dependensi baru itu.",
|
|
5312
|
+
tools: ["Read", "Glob", "Grep", "Bash", "WebSearch", "WebFetch"],
|
|
5313
|
+
enabledByDefault: false,
|
|
5314
|
+
instructions: [
|
|
5315
|
+
"Kamu gerbang rantai pasok. Satu dependensi masuk lewat satu baris diff dan tak pernah",
|
|
5316
|
+
"diperiksa lagi seumur hidup proyek \u2014 pemeriksaan itu terjadi sekarang atau tidak sama sekali.",
|
|
5317
|
+
"",
|
|
5318
|
+
"Prosedur:",
|
|
5319
|
+
"1. Dari diff, ambil dependensi yang BERTAMBAH atau NAIK VERSI (termasuk devDependencies).",
|
|
5320
|
+
" Lockfile ikut dibaca: dependensi transitif baru yang besar juga temuan.",
|
|
5321
|
+
"2. Untuk tiap satu, periksa dan sebutkan sumbernya:",
|
|
5322
|
+
" - advisory/CVE yang diketahui untuk rentang versi itu;",
|
|
5323
|
+
" - tanggal rilis terakhir & tanda pemeliharaan (isu terbuka menumpuk, maintainer tunggal);",
|
|
5324
|
+
" - lisensi, dan apakah ia cocok dengan lisensi proyek ini;",
|
|
5325
|
+
" - ukuran pohon transitifnya;",
|
|
5326
|
+
" - apakah paket menjalankan skrip saat instalasi.",
|
|
5327
|
+
"3. Pertanyaan yang paling sering dilewati, dan tanyakan SELALU: apakah fungsi yang dipakai",
|
|
5328
|
+
" sudah tersedia di dependensi yang SUDAH ada di proyek ini, atau di runtime-nya? Cek dulu",
|
|
5329
|
+
" sebelum menerima. Satu dependensi yang tak jadi masuk lebih berharga daripada sepuluh",
|
|
5330
|
+
" yang diaudit.",
|
|
5331
|
+
"",
|
|
5332
|
+
"Gerbang bukti: klaim CVE atau lisensi WAJIB membawa URL sumbernya. Tanpa sumber, tulis 'tak",
|
|
5333
|
+
"terverifikasi' \u2014 jangan hilangkan, dan jangan naikkan jadi fakta.",
|
|
5334
|
+
"",
|
|
5335
|
+
"Bentuk laporan: per dependensi \u2014 aman \xB7 aman dengan catatan \xB7 tolak (+ penggantinya, atau",
|
|
5336
|
+
"cara mengerjakannya tanpa dependensi itu)."
|
|
5337
|
+
].join("\n")
|
|
5338
|
+
}
|
|
5339
|
+
];
|
|
5340
|
+
BUILTIN_AGENT_NAMES = BUILTIN_AGENTS.map((a) => a.name);
|
|
5341
|
+
}
|
|
5342
|
+
});
|
|
5343
|
+
|
|
5076
5344
|
// ../shared/src/agent-catalog.ts
|
|
5077
5345
|
function modelsForRuntime(rt) {
|
|
5078
5346
|
const claude = MODELS.map((m) => ({ id: m.id, label: m.label, runtime: "claude" }));
|
|
@@ -5355,6 +5623,36 @@ var init_lead = __esm({
|
|
|
5355
5623
|
}
|
|
5356
5624
|
});
|
|
5357
5625
|
|
|
5626
|
+
// ../shared/src/handled-by.ts
|
|
5627
|
+
function handledByOf(raw2) {
|
|
5628
|
+
if (raw2 === null || raw2 === void 0) return [];
|
|
5629
|
+
const p = zHandledBy.safeParse(raw2);
|
|
5630
|
+
return p.success ? p.data : [];
|
|
5631
|
+
}
|
|
5632
|
+
var zHandledByEntry, HANDLED_BY_MAX, zHandledBy, zHandledByView;
|
|
5633
|
+
var init_handled_by = __esm({
|
|
5634
|
+
"../shared/src/handled-by.ts"() {
|
|
5635
|
+
"use strict";
|
|
5636
|
+
init_zod();
|
|
5637
|
+
zHandledByEntry = external_exports.object({
|
|
5638
|
+
deviceId: external_exports.string().min(1),
|
|
5639
|
+
name: external_exports.string().min(1)
|
|
5640
|
+
});
|
|
5641
|
+
HANDLED_BY_MAX = 32;
|
|
5642
|
+
zHandledBy = external_exports.array(zHandledByEntry).max(HANDLED_BY_MAX).superRefine((list2, ctx) => {
|
|
5643
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5644
|
+
for (const e of list2) {
|
|
5645
|
+
if (seen.has(e.deviceId)) {
|
|
5646
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `deviceId duplikat: ${e.deviceId}` });
|
|
5647
|
+
return;
|
|
5648
|
+
}
|
|
5649
|
+
seen.add(e.deviceId);
|
|
5650
|
+
}
|
|
5651
|
+
});
|
|
5652
|
+
zHandledByView = zHandledByEntry.extend({ revoked: external_exports.boolean().default(false) });
|
|
5653
|
+
}
|
|
5654
|
+
});
|
|
5655
|
+
|
|
5358
5656
|
// ../shared/src/prd-status.ts
|
|
5359
5657
|
function prdBranchFor(prdPath) {
|
|
5360
5658
|
if (!prdPath.startsWith(PRD_DIR) || !prdPath.endsWith(".md")) return null;
|
|
@@ -5476,7 +5774,7 @@ var init_cron_expr = __esm({
|
|
|
5476
5774
|
function flowForSource(source) {
|
|
5477
5775
|
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : source === "no_effort" ? "no_effort" : "feature";
|
|
5478
5776
|
}
|
|
5479
|
-
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, isGoalShapedFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zSpecAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
|
|
5777
|
+
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, isGoalShapedFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zSetupApply, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, PROVISION_PROFILES, COMPONENT_IDS, zProvision, UPDATE_RESTART_EXIT, CONFIG_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zSpecAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
|
|
5480
5778
|
var init_dto = __esm({
|
|
5481
5779
|
"../shared/src/dto.ts"() {
|
|
5482
5780
|
"use strict";
|
|
@@ -5484,6 +5782,7 @@ var init_dto = __esm({
|
|
|
5484
5782
|
init_entities();
|
|
5485
5783
|
init_lead();
|
|
5486
5784
|
init_auto_merge();
|
|
5785
|
+
init_handled_by();
|
|
5487
5786
|
init_prd_status();
|
|
5488
5787
|
init_enums();
|
|
5489
5788
|
init_spec_source();
|
|
@@ -5530,6 +5829,8 @@ var init_dto = __esm({
|
|
|
5530
5829
|
kind: zProjectKind,
|
|
5531
5830
|
repoDir: external_exports.string().optional(),
|
|
5532
5831
|
gitRemote: external_exports.string().optional(),
|
|
5832
|
+
// SPEC-880 · ADR-0135 · penanda "ditangani oleh" boleh di-set sejak awal (disync).
|
|
5833
|
+
handledBy: zHandledBy.optional(),
|
|
5533
5834
|
desc: external_exports.string().default("")
|
|
5534
5835
|
});
|
|
5535
5836
|
zProjectId = external_exports.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "slug tak sah");
|
|
@@ -5539,6 +5840,10 @@ var init_dto = __esm({
|
|
|
5539
5840
|
desc: external_exports.string().optional(),
|
|
5540
5841
|
gitRemote: external_exports.string().optional(),
|
|
5541
5842
|
// SPEC-213 · set git remote resmi project
|
|
5843
|
+
// SPEC-880 · ADR-0135 · daftar hanoman client pemegang project (DISYNC — beda dari repoDir &
|
|
5844
|
+
// opt-in di bawah yang lokal per-instance). `null` maupun `[]` = kosongkan. Digerbangi
|
|
5845
|
+
// `checkHandledBy`: deviceId wajib dikenal HANYA bila instance ini punya katalog device.
|
|
5846
|
+
handledBy: zHandledBy.nullable().optional(),
|
|
5542
5847
|
repoDir: external_exports.string().nullable().optional(),
|
|
5543
5848
|
// SPEC-217 · path default/server editable (null = kosongkan)
|
|
5544
5849
|
schedulerOptIn: external_exports.boolean().optional(),
|
|
@@ -5614,6 +5919,10 @@ var init_dto = __esm({
|
|
|
5614
5919
|
// SPEC-294 · opt-in scheduler otonom
|
|
5615
5920
|
leadOptIn: external_exports.boolean().default(false),
|
|
5616
5921
|
// SPEC-409 · ADR-0091 · opt-in hanoman-lead
|
|
5922
|
+
// SPEC-880 · ADR-0135 · penanda "ditangani oleh" yang SUDAH diperkaya: `name` = nama hidup bila
|
|
5923
|
+
// baris DeviceToken-nya ada di instance ini, else snapshot tersimpan; `revoked` diturunkan.
|
|
5924
|
+
// `[]` = belum ditetapkan — kolom NULL tak pernah bocor ke UI sebagai bentuk kedua "kosong".
|
|
5925
|
+
handledBy: zHandledByView.array().default([]),
|
|
5617
5926
|
autoMerge: zAutoMerge.nullable().default(null)
|
|
5618
5927
|
});
|
|
5619
5928
|
zSchedulerQueueItem = external_exports.object({
|
|
@@ -5917,6 +6226,11 @@ var init_dto = __esm({
|
|
|
5917
6226
|
zLogin = external_exports.object({ email: external_exports.string().email(), password: external_exports.string().min(1) });
|
|
5918
6227
|
zSignup = external_exports.object({ email: external_exports.string().email(), password: external_exports.string().min(8) });
|
|
5919
6228
|
zSetup = zSignup.extend({ setupToken: external_exports.string().min(1) });
|
|
6229
|
+
zSetupApply = external_exports.object({
|
|
6230
|
+
deployment: external_exports.enum(["local", "public"]),
|
|
6231
|
+
hardening: external_exports.boolean(),
|
|
6232
|
+
acknowledgedUnhardened: external_exports.boolean().optional()
|
|
6233
|
+
});
|
|
5920
6234
|
zChangePassword = external_exports.object({
|
|
5921
6235
|
currentPassword: external_exports.string().min(1),
|
|
5922
6236
|
newPassword: external_exports.string().min(8)
|
|
@@ -5944,7 +6258,27 @@ var init_dto = __esm({
|
|
|
5944
6258
|
});
|
|
5945
6259
|
zAttest = external_exports.object({ note: external_exports.string().max(500).optional() });
|
|
5946
6260
|
zRemediate = external_exports.object({ items: external_exports.array(external_exports.string()).min(1).max(64) });
|
|
6261
|
+
PROVISION_PROFILES = ["lab", "production"];
|
|
6262
|
+
COMPONENT_IDS = [
|
|
6263
|
+
"base",
|
|
6264
|
+
"node",
|
|
6265
|
+
"hanoman",
|
|
6266
|
+
"caddy",
|
|
6267
|
+
"podman",
|
|
6268
|
+
"agent-image",
|
|
6269
|
+
"claude",
|
|
6270
|
+
"codex",
|
|
6271
|
+
"gh"
|
|
6272
|
+
];
|
|
6273
|
+
zProvision = external_exports.object({
|
|
6274
|
+
items: external_exports.array(external_exports.enum(COMPONENT_IDS)).min(1).max(16),
|
|
6275
|
+
profile: external_exports.enum(PROVISION_PROFILES),
|
|
6276
|
+
domain: external_exports.string().min(1).regex(HOST_RE).optional(),
|
|
6277
|
+
confirm: external_exports.boolean().optional(),
|
|
6278
|
+
force: external_exports.boolean().optional()
|
|
6279
|
+
});
|
|
5947
6280
|
UPDATE_RESTART_EXIT = 75;
|
|
6281
|
+
CONFIG_RESTART_EXIT = 76;
|
|
5948
6282
|
zUpdateApplyBody = external_exports.object({ confirm: external_exports.boolean().optional() });
|
|
5949
6283
|
zTicketView = external_exports.object({
|
|
5950
6284
|
id: external_exports.string(),
|
|
@@ -6160,6 +6494,11 @@ var init_api = __esm({
|
|
|
6160
6494
|
terminalSession: (id) => `${API}/terminal/sessions/${id}`,
|
|
6161
6495
|
terminalSteer: (id) => `${API}/terminal/sessions/${id}/steer`,
|
|
6162
6496
|
terminalInterrupt: (id) => `${API}/terminal/sessions/${id}/interrupt`,
|
|
6497
|
+
// SPEC-899 · ADR-0142 · inbox keputusan. Di bawah prefix /terminal supaya ikut capability
|
|
6498
|
+
// `sessions` yang sudah ada — GET menurunkan cabang bacanya, POST cabang tulisnya, tanpa satu
|
|
6499
|
+
// baris pun perubahan di services/agent-capabilities.ts.
|
|
6500
|
+
terminalDialog: (id) => `${API}/terminal/sessions/${id}/dialog`,
|
|
6501
|
+
terminalDialogAnswer: (id) => `${API}/terminal/sessions/${id}/dialog/answer`,
|
|
6163
6502
|
terminalPhases: (id) => `${API}/terminal/sessions/${id}/phases`,
|
|
6164
6503
|
// SPEC-230 · review + integrate ber-skop sesi (sesi project-level PRD, tanpa Spec).
|
|
6165
6504
|
sessionReview: (id) => `${API}/terminal/sessions/${id}/review`,
|
|
@@ -6191,6 +6530,11 @@ var init_api = __esm({
|
|
|
6191
6530
|
vpsItemAttest: (id, itemId) => `${API}/vps/${id}/items/${itemId}/attest`,
|
|
6192
6531
|
vpsRemediatePreview: (id) => `${API}/vps/${id}/remediate/preview`,
|
|
6193
6532
|
vpsRemediate: (id) => `${API}/vps/${id}/remediate`,
|
|
6533
|
+
// SPEC-883 · provisioning berbasis katalog
|
|
6534
|
+
vpsComponents: () => `${API}/vps/components`,
|
|
6535
|
+
vpsProbe: (id) => `${API}/vps/${id}/probe`,
|
|
6536
|
+
vpsProvisionPreview: (id) => `${API}/vps/${id}/provision/preview`,
|
|
6537
|
+
vpsProvision: (id) => `${API}/vps/${id}/provision`,
|
|
6194
6538
|
// SPEC-169 · auth
|
|
6195
6539
|
authStatus: `${API}/auth/status`,
|
|
6196
6540
|
authSetup: `${API}/auth/setup`,
|
|
@@ -6199,6 +6543,9 @@ var init_api = __esm({
|
|
|
6199
6543
|
authUsers: `${API}/auth/users`,
|
|
6200
6544
|
authUser: (id) => `${API}/auth/users/${id}`,
|
|
6201
6545
|
authChangePassword: `${API}/auth/change-password`,
|
|
6546
|
+
// SPEC-884 · ADR-0139 · wizard setup awal
|
|
6547
|
+
setupStatus: `${API}/setup/status`,
|
|
6548
|
+
setupApply: `${API}/setup`,
|
|
6202
6549
|
// SPEC-213 · device token + activity log
|
|
6203
6550
|
deviceTokens: `${API}/device-tokens`,
|
|
6204
6551
|
deviceToken: (id) => `${API}/device-tokens/${id}`,
|
|
@@ -6918,6 +7265,9 @@ var init_webhook = __esm({
|
|
|
6918
7265
|
"gitRemote",
|
|
6919
7266
|
"stack",
|
|
6920
7267
|
"helpEnabled",
|
|
7268
|
+
// SPEC-880 · ADR-0135 · penerima harus bisa tahu mesin mana yang memegang project ini
|
|
7269
|
+
// tanpa mendiff dua amplop. Entrinya snapshot: [{deviceId,name}].
|
|
7270
|
+
"handledBy",
|
|
6921
7271
|
"schedulerOptIn",
|
|
6922
7272
|
"leadOptIn",
|
|
6923
7273
|
"autoMerge",
|
|
@@ -6928,7 +7278,7 @@ var init_webhook = __esm({
|
|
|
6928
7278
|
cascade: ["spec", "ticket", "customAgent", "githubIssue"],
|
|
6929
7279
|
events: {
|
|
6930
7280
|
created: { type: "project.created", label: "Project ditambah", when: "Project baru terdaftar di workspace." },
|
|
6931
|
-
updated: { type: "project.updated", label: "Project diubah", when:
|
|
7281
|
+
updated: { type: "project.updated", label: "Project diubah", when: 'Nama, deskripsi, stack, remote, penanda "ditangani oleh", atau opt-in scheduler/lead/Help Center berubah.' },
|
|
6932
7282
|
deleted: { type: "project.deleted", label: "Project dihapus", when: "Project dihapus. data.cascade menyebut jumlah anak yang ikut terhapus \u2014 anaknya sendiri TIDAK memancarkan deleted, sebab cascade dieksekusi SQLite, di luar jangkauan tap." }
|
|
6933
7283
|
},
|
|
6934
7284
|
sample: {
|
|
@@ -6939,6 +7289,7 @@ var init_webhook = __esm({
|
|
|
6939
7289
|
gitRemote: "git@github.com:nafanesia/hanoman.git",
|
|
6940
7290
|
stack: "ts",
|
|
6941
7291
|
helpEnabled: false,
|
|
7292
|
+
handledBy: [{ deviceId: "clq0device1", name: "hm-dena" }],
|
|
6942
7293
|
schedulerOptIn: true,
|
|
6943
7294
|
leadOptIn: false,
|
|
6944
7295
|
autoMerge: null,
|
|
@@ -7912,6 +8263,27 @@ var init_terminal_io = __esm({
|
|
|
7912
8263
|
}
|
|
7913
8264
|
});
|
|
7914
8265
|
|
|
8266
|
+
// ../shared/src/session-dialog.ts
|
|
8267
|
+
var zSessionDialogAnswer;
|
|
8268
|
+
var init_session_dialog = __esm({
|
|
8269
|
+
"../shared/src/session-dialog.ts"() {
|
|
8270
|
+
"use strict";
|
|
8271
|
+
init_zod();
|
|
8272
|
+
zSessionDialogAnswer = external_exports.object({
|
|
8273
|
+
screenHash: external_exports.string().min(1),
|
|
8274
|
+
choice: external_exports.number().int().positive().optional(),
|
|
8275
|
+
choices: external_exports.array(external_exports.number().int().positive()).max(32).optional(),
|
|
8276
|
+
text: external_exports.string().trim().min(1).max(16e3).optional()
|
|
8277
|
+
}).refine(
|
|
8278
|
+
(v) => !(v.choice !== void 0 && v.choices !== void 0),
|
|
8279
|
+
{ message: "choice dan choices tak boleh dikirim bersamaan" }
|
|
8280
|
+
).refine(
|
|
8281
|
+
(v) => v.choice !== void 0 || v.choices !== void 0 || v.text !== void 0,
|
|
8282
|
+
{ message: "jawaban kosong" }
|
|
8283
|
+
);
|
|
8284
|
+
}
|
|
8285
|
+
});
|
|
8286
|
+
|
|
7915
8287
|
// ../shared/src/index.ts
|
|
7916
8288
|
var init_src = __esm({
|
|
7917
8289
|
"../shared/src/index.ts"() {
|
|
@@ -7924,6 +8296,7 @@ var init_src = __esm({
|
|
|
7924
8296
|
init_entities();
|
|
7925
8297
|
init_agent();
|
|
7926
8298
|
init_custom_agent();
|
|
8299
|
+
init_builtin_agents();
|
|
7927
8300
|
init_agent_catalog();
|
|
7928
8301
|
init_github();
|
|
7929
8302
|
init_lead();
|
|
@@ -7943,10 +8316,12 @@ var init_src = __esm({
|
|
|
7943
8316
|
init_webhook();
|
|
7944
8317
|
init_mcp();
|
|
7945
8318
|
init_auto_merge();
|
|
8319
|
+
init_handled_by();
|
|
7946
8320
|
init_changelog();
|
|
7947
8321
|
init_cron_expr();
|
|
7948
8322
|
init_terminal_workspace();
|
|
7949
8323
|
init_terminal_io();
|
|
8324
|
+
init_session_dialog();
|
|
7950
8325
|
}
|
|
7951
8326
|
});
|
|
7952
8327
|
|
|
@@ -8618,7 +8993,7 @@ var init_settings = __esm({
|
|
|
8618
8993
|
const f = `'${decisionFile.split("'").join("'\\''")}'`;
|
|
8619
8994
|
hooks2.Notification = [{ hooks: [{
|
|
8620
8995
|
type: "command",
|
|
8621
|
-
command: `grep -qiE 'idle|permission|waiting for|needs.?input' &&
|
|
8996
|
+
command: `grep -qiE 'idle|permission|waiting for|needs.?input' && { [ -s ${f} ] || date +%s > ${f}; } || true`
|
|
8622
8997
|
}] }];
|
|
8623
8998
|
hooks2.UserPromptSubmit = [{ hooks: [{ type: "command", command: `: > ${f}` }] }];
|
|
8624
8999
|
}
|
|
@@ -8691,7 +9066,7 @@ function codexHookArgs(o) {
|
|
|
8691
9066
|
const stop = [];
|
|
8692
9067
|
const submit = [];
|
|
8693
9068
|
if (o.decisionFile) {
|
|
8694
|
-
stop.push(`
|
|
9069
|
+
stop.push(`[ -s ${shq(o.decisionFile)} ] || date +%s > ${shq(o.decisionFile)}`);
|
|
8695
9070
|
submit.push(`: > ${shq(o.decisionFile)}`);
|
|
8696
9071
|
}
|
|
8697
9072
|
if (o.goalGate) stop.push(`sh ${shq(o.goalGate)}`);
|
|
@@ -8852,6 +9227,24 @@ function agentRosterBlock(defs) {
|
|
|
8852
9227
|
}
|
|
8853
9228
|
return lines2.join("\n");
|
|
8854
9229
|
}
|
|
9230
|
+
function agentDelegationClause(defs) {
|
|
9231
|
+
if (defs.length === 0) return "";
|
|
9232
|
+
return [
|
|
9233
|
+
"",
|
|
9234
|
+
"## Subagent yang tersedia",
|
|
9235
|
+
"",
|
|
9236
|
+
"Sesi ini punya subagent berikut. Delegasikan saat tugasnya cocok \u2014 konteks mereka TERPISAH",
|
|
9237
|
+
"dari milikmu, jadi menyerahkan penyapuan & verifikasi ke mereka MENGHEMAT konteksmu sendiri,",
|
|
9238
|
+
"bukan memboroskannya.",
|
|
9239
|
+
"",
|
|
9240
|
+
...defs.map((d) => `- **${d.name}** \u2014 ${d.description}`),
|
|
9241
|
+
"",
|
|
9242
|
+
`Panggil lewat tool ${MENTION_TOOL} dengan nama agennya. Mereka tak bisa mendelegasikan lagi,`,
|
|
9243
|
+
"jadi tak ada rantai panggilan yang perlu kamu jaga. Laporan mereka adalah MASUKAN \u2014 kamu yang",
|
|
9244
|
+
"memutuskan, dan kamu yang bertanggung jawab atas hasilnya.",
|
|
9245
|
+
""
|
|
9246
|
+
].join("\n");
|
|
9247
|
+
}
|
|
8855
9248
|
var liveMentions;
|
|
8856
9249
|
var init_custom_agents = __esm({
|
|
8857
9250
|
"../runner/src/custom-agents.ts"() {
|
|
@@ -9228,6 +9621,164 @@ var init_spawn_helper = __esm({
|
|
|
9228
9621
|
}
|
|
9229
9622
|
});
|
|
9230
9623
|
|
|
9624
|
+
// ../runner/src/config-env.ts
|
|
9625
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "node:fs";
|
|
9626
|
+
import { join as join4 } from "node:path";
|
|
9627
|
+
function configEnvPath(home3) {
|
|
9628
|
+
return join4(home3, CONFIG_ENV_FILE);
|
|
9629
|
+
}
|
|
9630
|
+
function parseConfigEnv(text) {
|
|
9631
|
+
const out4 = {};
|
|
9632
|
+
for (const raw2 of text.split("\n")) {
|
|
9633
|
+
const line = raw2.trim();
|
|
9634
|
+
if (!line || line.startsWith("#")) continue;
|
|
9635
|
+
const eq = line.indexOf("=");
|
|
9636
|
+
if (eq <= 0) continue;
|
|
9637
|
+
out4[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
|
|
9638
|
+
}
|
|
9639
|
+
return out4;
|
|
9640
|
+
}
|
|
9641
|
+
function formatConfigEnv(values) {
|
|
9642
|
+
const lines2 = Object.entries(values).filter(([, v]) => v.trim() !== "").map(([k, v]) => `${k}=${v.trim()}`);
|
|
9643
|
+
return lines2.length ? `${lines2.join("\n")}
|
|
9644
|
+
` : "";
|
|
9645
|
+
}
|
|
9646
|
+
function readConfigEnv(home3) {
|
|
9647
|
+
try {
|
|
9648
|
+
return parseConfigEnv(readFileSync3(configEnvPath(home3), "utf8"));
|
|
9649
|
+
} catch {
|
|
9650
|
+
return {};
|
|
9651
|
+
}
|
|
9652
|
+
}
|
|
9653
|
+
function writeConfigEnv(home3, values) {
|
|
9654
|
+
mkdirSync2(home3, { recursive: true, mode: 448 });
|
|
9655
|
+
const path2 = configEnvPath(home3);
|
|
9656
|
+
writeFileSync(path2, formatConfigEnv(values), { mode: 384 });
|
|
9657
|
+
chmodSync2(path2, 384);
|
|
9658
|
+
}
|
|
9659
|
+
var CONFIG_ENV_FILE;
|
|
9660
|
+
var init_config_env = __esm({
|
|
9661
|
+
"../runner/src/config-env.ts"() {
|
|
9662
|
+
"use strict";
|
|
9663
|
+
CONFIG_ENV_FILE = "config.env";
|
|
9664
|
+
}
|
|
9665
|
+
});
|
|
9666
|
+
|
|
9667
|
+
// ../runner/src/runtime-profile.ts
|
|
9668
|
+
function resolveHardening(env) {
|
|
9669
|
+
if (env.HANOMAN_HARDENING === "1") return true;
|
|
9670
|
+
return env.HANOMAN_SESSION_SANDBOX === "podman" || filled(env.HANOMAN_PUBLIC_ORIGINS) || filled(env.HANOMAN_TRUST_PROXY);
|
|
9671
|
+
}
|
|
9672
|
+
function resolveDeployment(env) {
|
|
9673
|
+
if (resolveHardening(env)) return "public";
|
|
9674
|
+
return env.HANOMAN_DEPLOYMENT === "public" ? "public" : "local";
|
|
9675
|
+
}
|
|
9676
|
+
var filled;
|
|
9677
|
+
var init_runtime_profile = __esm({
|
|
9678
|
+
"../runner/src/runtime-profile.ts"() {
|
|
9679
|
+
"use strict";
|
|
9680
|
+
filled = (v) => !!v && v.trim() !== "";
|
|
9681
|
+
}
|
|
9682
|
+
});
|
|
9683
|
+
|
|
9684
|
+
// ../runner/src/sandbox-probe.ts
|
|
9685
|
+
import { execFileSync } from "node:child_process";
|
|
9686
|
+
import { accessSync, constants } from "node:fs";
|
|
9687
|
+
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
9688
|
+
function trustProxyOk(raw2) {
|
|
9689
|
+
const v = raw2?.trim();
|
|
9690
|
+
if (!v) return false;
|
|
9691
|
+
if (/^[1-9]\d*$/.test(v)) return true;
|
|
9692
|
+
const values = v.split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
9693
|
+
if (!values.length) return false;
|
|
9694
|
+
return !values.some((s2) => s2 === "true" || !s2.includes("/") && s2 !== "loopback");
|
|
9695
|
+
}
|
|
9696
|
+
function prerequisites(env, facts) {
|
|
9697
|
+
const network = env.HANOMAN_SESSION_NETWORK ?? "hanoman-egress";
|
|
9698
|
+
const credentialDir = env.HANOMAN_AGENT_CREDENTIAL_DIR;
|
|
9699
|
+
const scanner = env.HANOMAN_UPLOAD_SCANNER?.trim();
|
|
9700
|
+
return [
|
|
9701
|
+
{
|
|
9702
|
+
id: "podman",
|
|
9703
|
+
label: "Podman rootless",
|
|
9704
|
+
ok: !!facts.podman && facts.rootless,
|
|
9705
|
+
detail: facts.podman ? facts.rootless ? facts.podman : `${facts.podman} \u2014 berjalan rootful, butuh rootless` : "podman tak ada"
|
|
9706
|
+
},
|
|
9707
|
+
{
|
|
9708
|
+
id: "network",
|
|
9709
|
+
label: `Network "${network}"`,
|
|
9710
|
+
ok: facts.networkExists,
|
|
9711
|
+
detail: facts.networkExists ? network : `network ${network} belum dibuat`
|
|
9712
|
+
},
|
|
9713
|
+
{
|
|
9714
|
+
id: "egress-proxy",
|
|
9715
|
+
label: "Egress proxy",
|
|
9716
|
+
ok: filled2(env.HANOMAN_EGRESS_PROXY),
|
|
9717
|
+
detail: env.HANOMAN_EGRESS_PROXY ?? "HANOMAN_EGRESS_PROXY belum disetel"
|
|
9718
|
+
},
|
|
9719
|
+
{
|
|
9720
|
+
id: "credential-dir",
|
|
9721
|
+
label: "Dir credential agen",
|
|
9722
|
+
ok: filled2(credentialDir) && facts.credentialDirReadable,
|
|
9723
|
+
detail: !filled2(credentialDir) ? "HANOMAN_AGENT_CREDENTIAL_DIR belum disetel" : facts.credentialDirReadable ? credentialDir : `${credentialDir} \u2014 tak terbaca`
|
|
9724
|
+
},
|
|
9725
|
+
{
|
|
9726
|
+
id: "control-origin",
|
|
9727
|
+
label: "Control origin",
|
|
9728
|
+
ok: filled2(env.HANOMAN_CONTROL_ORIGINS),
|
|
9729
|
+
detail: env.HANOMAN_CONTROL_ORIGINS ?? "HANOMAN_CONTROL_ORIGINS belum disetel"
|
|
9730
|
+
},
|
|
9731
|
+
{
|
|
9732
|
+
id: "trust-proxy",
|
|
9733
|
+
label: "Trusted proxy hop/CIDR",
|
|
9734
|
+
ok: trustProxyOk(env.HANOMAN_TRUST_PROXY),
|
|
9735
|
+
detail: filled2(env.HANOMAN_TRUST_PROXY) ? trustProxyOk(env.HANOMAN_TRUST_PROXY) ? env.HANOMAN_TRUST_PROXY : "harus hop atau CIDR eksplisit" : "HANOMAN_TRUST_PROXY belum disetel"
|
|
9736
|
+
},
|
|
9737
|
+
{
|
|
9738
|
+
id: "upload-scanner",
|
|
9739
|
+
label: "Scanner upload",
|
|
9740
|
+
ok: !!scanner && isAbsolute3(scanner),
|
|
9741
|
+
detail: !scanner ? "HANOMAN_UPLOAD_SCANNER belum disetel" : isAbsolute3(scanner) ? scanner : `${scanner} \u2014 path harus absolut`
|
|
9742
|
+
}
|
|
9743
|
+
];
|
|
9744
|
+
}
|
|
9745
|
+
function allReady(rows) {
|
|
9746
|
+
return rows.every((r) => r.ok);
|
|
9747
|
+
}
|
|
9748
|
+
function run(file, args) {
|
|
9749
|
+
try {
|
|
9750
|
+
return execFileSync(file, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
9751
|
+
} catch {
|
|
9752
|
+
return null;
|
|
9753
|
+
}
|
|
9754
|
+
}
|
|
9755
|
+
function collectProbeFacts(env) {
|
|
9756
|
+
const podman = run("podman", ["--version"]);
|
|
9757
|
+
const network = env.HANOMAN_SESSION_NETWORK ?? "hanoman-egress";
|
|
9758
|
+
const dir2 = env.HANOMAN_AGENT_CREDENTIAL_DIR;
|
|
9759
|
+
let credentialDirReadable = false;
|
|
9760
|
+
if (dir2) {
|
|
9761
|
+
try {
|
|
9762
|
+
accessSync(dir2, constants.R_OK);
|
|
9763
|
+
credentialDirReadable = true;
|
|
9764
|
+
} catch {
|
|
9765
|
+
}
|
|
9766
|
+
}
|
|
9767
|
+
return {
|
|
9768
|
+
podman,
|
|
9769
|
+
rootless: podman ? run("podman", ["info", "--format", "{{.Host.Security.Rootless}}"]) === "true" : false,
|
|
9770
|
+
networkExists: podman ? run("podman", ["network", "exists", network]) !== null : false,
|
|
9771
|
+
credentialDirReadable
|
|
9772
|
+
};
|
|
9773
|
+
}
|
|
9774
|
+
var filled2;
|
|
9775
|
+
var init_sandbox_probe = __esm({
|
|
9776
|
+
"../runner/src/sandbox-probe.ts"() {
|
|
9777
|
+
"use strict";
|
|
9778
|
+
filled2 = (v) => !!v && v.trim() !== "";
|
|
9779
|
+
}
|
|
9780
|
+
});
|
|
9781
|
+
|
|
9231
9782
|
// ../runner/src/index.ts
|
|
9232
9783
|
var init_src2 = __esm({
|
|
9233
9784
|
"../runner/src/index.ts"() {
|
|
@@ -9248,6 +9799,9 @@ var init_src2 = __esm({
|
|
|
9248
9799
|
init_skills();
|
|
9249
9800
|
init_telegram_operator();
|
|
9250
9801
|
init_spawn_helper();
|
|
9802
|
+
init_config_env();
|
|
9803
|
+
init_runtime_profile();
|
|
9804
|
+
init_sandbox_probe();
|
|
9251
9805
|
}
|
|
9252
9806
|
});
|
|
9253
9807
|
|
|
@@ -9382,7 +9936,7 @@ var init_tap = __esm({
|
|
|
9382
9936
|
});
|
|
9383
9937
|
|
|
9384
9938
|
// src/db.ts
|
|
9385
|
-
import { mkdirSync as
|
|
9939
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
9386
9940
|
import { dirname as dirname5, resolve as resolve7 } from "node:path";
|
|
9387
9941
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9388
9942
|
import { PrismaClient } from "@prisma/client";
|
|
@@ -9399,7 +9953,7 @@ var init_db = __esm({
|
|
|
9399
9953
|
if (notice) console.warn(notice);
|
|
9400
9954
|
url = resolveDbUrl(process.env, schemaDir);
|
|
9401
9955
|
process.env.DATABASE_URL = url;
|
|
9402
|
-
|
|
9956
|
+
mkdirSync3(dirname5(dbFilePath(url)), { recursive: true, mode: 448 });
|
|
9403
9957
|
base = new PrismaClient();
|
|
9404
9958
|
void base.$queryRawUnsafe("PRAGMA journal_mode=WAL").catch((e) => console.warn("tak bisa menyetel journal_mode=WAL:", e));
|
|
9405
9959
|
prisma = base.$extends(webhookTap(base));
|
|
@@ -9416,12 +9970,12 @@ var init_stage_machine = __esm({
|
|
|
9416
9970
|
});
|
|
9417
9971
|
|
|
9418
9972
|
// src/services/session-phases.ts
|
|
9419
|
-
import { readFileSync as
|
|
9973
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
9420
9974
|
function recorded(file) {
|
|
9421
9975
|
const out4 = /* @__PURE__ */ new Map();
|
|
9422
9976
|
let raw2;
|
|
9423
9977
|
try {
|
|
9424
|
-
raw2 =
|
|
9978
|
+
raw2 = readFileSync4(file, "utf8");
|
|
9425
9979
|
} catch {
|
|
9426
9980
|
return out4;
|
|
9427
9981
|
}
|
|
@@ -9471,7 +10025,7 @@ function planComplete(worktree, specId) {
|
|
|
9471
10025
|
for (const n2 of names) {
|
|
9472
10026
|
if (!re.test(n2.toLowerCase())) continue;
|
|
9473
10027
|
try {
|
|
9474
|
-
if (/^[ \t]*- \[ \]/m.test(
|
|
10028
|
+
if (/^[ \t]*- \[ \]/m.test(readFileSync4(`${dir2}/${n2}`, "utf8"))) return false;
|
|
9475
10029
|
} catch {
|
|
9476
10030
|
}
|
|
9477
10031
|
}
|
|
@@ -9525,9 +10079,9 @@ var init_session_id = __esm({
|
|
|
9525
10079
|
});
|
|
9526
10080
|
|
|
9527
10081
|
// src/services/secret-box.ts
|
|
9528
|
-
import { chmodSync as
|
|
10082
|
+
import { chmodSync as chmodSync3, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
9529
10083
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
9530
|
-
import { dirname as dirname6, join as
|
|
10084
|
+
import { dirname as dirname6, join as join5 } from "node:path";
|
|
9531
10085
|
function isEncrypted(value) {
|
|
9532
10086
|
return value.startsWith(ENC_PREFIX);
|
|
9533
10087
|
}
|
|
@@ -9559,7 +10113,7 @@ function fromEnv(raw2) {
|
|
|
9559
10113
|
return null;
|
|
9560
10114
|
}
|
|
9561
10115
|
function secretKeyPath() {
|
|
9562
|
-
return
|
|
10116
|
+
return join5(resolveHome(), "secret.key");
|
|
9563
10117
|
}
|
|
9564
10118
|
function secretKey() {
|
|
9565
10119
|
if (cached) return cached;
|
|
@@ -9572,15 +10126,15 @@ function secretKey() {
|
|
|
9572
10126
|
}
|
|
9573
10127
|
const path2 = secretKeyPath();
|
|
9574
10128
|
if (existsSync5(path2)) {
|
|
9575
|
-
const key2 = Buffer.from(
|
|
10129
|
+
const key2 = Buffer.from(readFileSync5(path2, "utf8").trim(), "base64url");
|
|
9576
10130
|
if (key2.length !== KEY_BYTES) throw new Error(`kunci di ${path2} rusak \u2014 panjangnya bukan 32 byte`);
|
|
9577
10131
|
cached = key2;
|
|
9578
10132
|
return key2;
|
|
9579
10133
|
}
|
|
9580
10134
|
const key = randomBytes(KEY_BYTES);
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
10135
|
+
mkdirSync4(dirname6(path2), { recursive: true });
|
|
10136
|
+
writeFileSync2(path2, key.toString("base64url"), { mode: 384 });
|
|
10137
|
+
chmodSync3(path2, 384);
|
|
9584
10138
|
cached = key;
|
|
9585
10139
|
return key;
|
|
9586
10140
|
}
|
|
@@ -9771,6 +10325,7 @@ var init_ssrf = __esm({
|
|
|
9771
10325
|
import { lookup as dnsLookup2 } from "node:dns/promises";
|
|
9772
10326
|
import { request as httpRequest } from "node:http";
|
|
9773
10327
|
import { request as httpsRequest } from "node:https";
|
|
10328
|
+
import { createGunzip } from "node:zlib";
|
|
9774
10329
|
async function pinnedRequest(input) {
|
|
9775
10330
|
return new Promise((resolve21, reject2) => {
|
|
9776
10331
|
const transport = input.url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
@@ -9790,21 +10345,43 @@ async function pinnedRequest(input) {
|
|
|
9790
10345
|
lookup: (_host, opts, callback) => opts.all ? callback(null, [{ address: input.address, family: input.family }]) : callback(null, input.address, input.family),
|
|
9791
10346
|
timeout: input.connectMs
|
|
9792
10347
|
}, (response) => {
|
|
10348
|
+
const dimampat = input.acceptEncoding === "gzip" && String(response.headers["content-encoding"] ?? "").toLowerCase() === "gzip";
|
|
10349
|
+
const capTerurai = input.maxDecodedBytes ?? input.maxResponseBytes;
|
|
9793
10350
|
const chunks = [];
|
|
9794
|
-
let
|
|
10351
|
+
let kabel = 0, terurai = 0;
|
|
10352
|
+
const selesai = () => resolve21({
|
|
10353
|
+
status: response.statusCode ?? 0,
|
|
10354
|
+
headers: response.headers,
|
|
10355
|
+
body: Buffer.concat(chunks)
|
|
10356
|
+
});
|
|
10357
|
+
const sink2 = dimampat ? createGunzip() : null;
|
|
10358
|
+
if (sink2) {
|
|
10359
|
+
sink2.on("data", (chunk) => {
|
|
10360
|
+
terurai += chunk.length;
|
|
10361
|
+
if (terurai > capTerurai) {
|
|
10362
|
+
sink2.destroy(new Error("outbound response terurai terlalu besar"));
|
|
10363
|
+
response.destroy();
|
|
10364
|
+
return;
|
|
10365
|
+
}
|
|
10366
|
+
chunks.push(chunk);
|
|
10367
|
+
});
|
|
10368
|
+
sink2.on("end", selesai);
|
|
10369
|
+
sink2.on("error", reject2);
|
|
10370
|
+
}
|
|
9795
10371
|
response.on("data", (chunk) => {
|
|
9796
|
-
|
|
9797
|
-
if (
|
|
10372
|
+
kabel += chunk.length;
|
|
10373
|
+
if (kabel > input.maxResponseBytes) {
|
|
9798
10374
|
response.destroy(new Error("outbound response terlalu besar"));
|
|
9799
10375
|
return;
|
|
9800
10376
|
}
|
|
9801
|
-
|
|
10377
|
+
if (sink2) sink2.write(chunk);
|
|
10378
|
+
else chunks.push(chunk);
|
|
9802
10379
|
});
|
|
9803
|
-
response.on("end", () =>
|
|
9804
|
-
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
|
|
10380
|
+
response.on("end", () => {
|
|
10381
|
+
if (sink2) sink2.end();
|
|
10382
|
+
else selesai();
|
|
10383
|
+
});
|
|
10384
|
+
response.on("error", reject2);
|
|
9808
10385
|
});
|
|
9809
10386
|
const timer9 = setTimeout(() => req.destroy(Object.assign(new Error("outbound timeout"), { name: "AbortError" })), input.totalMs);
|
|
9810
10387
|
timer9.unref?.();
|
|
@@ -9838,7 +10415,7 @@ var init_safe_outbound_request = __esm({
|
|
|
9838
10415
|
// src/services/uploads.ts
|
|
9839
10416
|
import { randomUUID } from "node:crypto";
|
|
9840
10417
|
import { mkdir, writeFile, readFile as readFile2, unlink, rm } from "node:fs/promises";
|
|
9841
|
-
import { join as
|
|
10418
|
+
import { join as join6, resolve as resolve8 } from "node:path";
|
|
9842
10419
|
function extFor(mimeType) {
|
|
9843
10420
|
return EXT[mimeType] ?? ".bin";
|
|
9844
10421
|
}
|
|
@@ -9847,12 +10424,12 @@ function uploadDir() {
|
|
|
9847
10424
|
}
|
|
9848
10425
|
function sessionUploadDir(sessionId2) {
|
|
9849
10426
|
if (!SESSION_ID.test(sessionId2)) throw new Error(`sessionId tak sah: ${sessionId2}`);
|
|
9850
|
-
return
|
|
10427
|
+
return join6(uploadDir(), "terminal", sessionId2);
|
|
9851
10428
|
}
|
|
9852
10429
|
async function saveSessionUpload(sessionId2, buf, mimeType) {
|
|
9853
10430
|
const dir2 = sessionUploadDir(sessionId2);
|
|
9854
10431
|
await mkdir(dir2, { recursive: true, mode: 448 });
|
|
9855
|
-
const path2 =
|
|
10432
|
+
const path2 = join6(dir2, `${randomUUID()}${extFor(mimeType)}`);
|
|
9856
10433
|
await writeFile(path2, buf, { mode: 384 });
|
|
9857
10434
|
return { path: path2, size: buf.length };
|
|
9858
10435
|
}
|
|
@@ -9868,11 +10445,11 @@ async function dropSessionUploads(sessionId2) {
|
|
|
9868
10445
|
}
|
|
9869
10446
|
async function readUpload(storageKey) {
|
|
9870
10447
|
const safe = storageKey.replace(/[/\\]/g, "");
|
|
9871
|
-
return readFile2(
|
|
10448
|
+
return readFile2(join6(uploadDir(), safe));
|
|
9872
10449
|
}
|
|
9873
10450
|
async function readUploadOrFetch(storageKey) {
|
|
9874
10451
|
const safe = storageKey.replace(/[/\\]/g, "");
|
|
9875
|
-
const target2 =
|
|
10452
|
+
const target2 = join6(uploadDir(), safe);
|
|
9876
10453
|
try {
|
|
9877
10454
|
return await readFile2(target2);
|
|
9878
10455
|
} catch {
|
|
@@ -9899,7 +10476,7 @@ async function readUploadOrFetch(storageKey) {
|
|
|
9899
10476
|
}
|
|
9900
10477
|
async function deleteUpload(storageKey) {
|
|
9901
10478
|
const safe = storageKey.replace(/[/\\]/g, "");
|
|
9902
|
-
await unlink(
|
|
10479
|
+
await unlink(join6(uploadDir(), safe)).catch(() => {
|
|
9903
10480
|
});
|
|
9904
10481
|
}
|
|
9905
10482
|
var EXT, SESSION_ID;
|
|
@@ -9935,16 +10512,16 @@ function readChoiceDialog(paneText2) {
|
|
|
9935
10512
|
if (m) found.push({ n: Number(m[1]), label: cleanLabel(m[2] ?? "") });
|
|
9936
10513
|
}
|
|
9937
10514
|
if (found.length < 2) return null;
|
|
9938
|
-
const
|
|
10515
|
+
const run3 = [];
|
|
9939
10516
|
let expected = found[found.length - 1].n;
|
|
9940
10517
|
for (let i = found.length - 1; i >= 0 && expected >= 1; i--) {
|
|
9941
10518
|
const row = found[i];
|
|
9942
10519
|
if (row.n !== expected) break;
|
|
9943
|
-
|
|
10520
|
+
run3.unshift(row);
|
|
9944
10521
|
expected -= 1;
|
|
9945
10522
|
}
|
|
9946
|
-
if (
|
|
9947
|
-
const rows =
|
|
10523
|
+
if (run3.length < 2 || run3[0].n !== 1) return null;
|
|
10524
|
+
const rows = run3.map((r) => {
|
|
9948
10525
|
const m = CHECK.exec(r.label);
|
|
9949
10526
|
const label = m ? (m[2] ?? "").trim() : r.label;
|
|
9950
10527
|
return {
|
|
@@ -10152,7 +10729,7 @@ var init_tui_dialog = __esm({
|
|
|
10152
10729
|
|
|
10153
10730
|
// src/services/session-sandbox.ts
|
|
10154
10731
|
function assertRuntimeBoundary(env, runtime) {
|
|
10155
|
-
if (env
|
|
10732
|
+
if (!resolveHardening(env)) return;
|
|
10156
10733
|
if (runtime.uid === 0) throw new Error("production Hanoman harus berjalan sebagai user non-root");
|
|
10157
10734
|
if (env.HANOMAN_SESSION_SANDBOX !== "podman")
|
|
10158
10735
|
throw new Error("HANOMAN_SESSION_SANDBOX=podman wajib di production");
|
|
@@ -10207,7 +10784,7 @@ function sandboxArgv(input) {
|
|
|
10207
10784
|
}
|
|
10208
10785
|
function sandboxArgvFromEnv(input) {
|
|
10209
10786
|
const env = input.env ?? process.env;
|
|
10210
|
-
const mode = env.HANOMAN_SESSION_SANDBOX ?? (env
|
|
10787
|
+
const mode = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
|
|
10211
10788
|
if (mode === "off") return null;
|
|
10212
10789
|
if (mode !== "podman") throw new Error("session sandbox production tidak dikonfigurasi");
|
|
10213
10790
|
const credentialDir = env.HANOMAN_AGENT_CREDENTIAL_DIR;
|
|
@@ -10230,27 +10807,28 @@ var quote;
|
|
|
10230
10807
|
var init_session_sandbox = __esm({
|
|
10231
10808
|
"src/services/session-sandbox.ts"() {
|
|
10232
10809
|
"use strict";
|
|
10810
|
+
init_src2();
|
|
10233
10811
|
quote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
10234
10812
|
}
|
|
10235
10813
|
});
|
|
10236
10814
|
|
|
10237
10815
|
// src/services/pty.ts
|
|
10238
10816
|
import { spawn } from "node-pty";
|
|
10239
|
-
import { execFileSync } from "node:child_process";
|
|
10817
|
+
import { execFile, execFileSync as execFileSync2 } from "node:child_process";
|
|
10240
10818
|
import { createRequire } from "node:module";
|
|
10241
10819
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10242
|
-
import { mkdirSync as
|
|
10820
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
10243
10821
|
import { dirname as dirname7 } from "node:path";
|
|
10244
10822
|
import { tmpdir } from "node:os";
|
|
10245
10823
|
function noTtyPromptEnv() {
|
|
10246
10824
|
const path2 = askpassDenyPath();
|
|
10247
|
-
|
|
10248
|
-
|
|
10825
|
+
mkdirSync5(dirname7(path2), { recursive: true, mode: 448 });
|
|
10826
|
+
writeFileSync3(path2, ASKPASS_DENY, { mode: 448 });
|
|
10249
10827
|
return { SSH_ASKPASS: path2, SSH_ASKPASS_REQUIRE: "force", GIT_TERMINAL_PROMPT: "0" };
|
|
10250
10828
|
}
|
|
10251
10829
|
function tmux(...args) {
|
|
10252
10830
|
try {
|
|
10253
|
-
return
|
|
10831
|
+
return execFileSync2("tmux", ["-L", socket(), "-f", "/dev/null", ...args], {
|
|
10254
10832
|
encoding: "utf8",
|
|
10255
10833
|
stdio: ["ignore", "pipe", "pipe"]
|
|
10256
10834
|
});
|
|
@@ -10263,6 +10841,28 @@ function tmux(...args) {
|
|
|
10263
10841
|
throw new TmuxError(`tmux ${args[0]} gagal: ${detail}`, NO_SERVER.test(detail));
|
|
10264
10842
|
}
|
|
10265
10843
|
}
|
|
10844
|
+
function tmuxAsync(...args) {
|
|
10845
|
+
return new Promise((resolve21, reject2) => {
|
|
10846
|
+
execFile(
|
|
10847
|
+
"tmux",
|
|
10848
|
+
["-L", socket(), "-f", "/dev/null", ...args],
|
|
10849
|
+
{ encoding: "utf8" },
|
|
10850
|
+
(e, stdout, stderr) => {
|
|
10851
|
+
if (!e) {
|
|
10852
|
+
resolve21(stdout);
|
|
10853
|
+
return;
|
|
10854
|
+
}
|
|
10855
|
+
const err = e;
|
|
10856
|
+
if (err.code === "ENOENT") {
|
|
10857
|
+
reject2(new TmuxError("tmux tidak ada di PATH \u2014 sesi terminal hanoman hidup di dalam tmux (ADR-0016). Pasang: brew install tmux", false));
|
|
10858
|
+
return;
|
|
10859
|
+
}
|
|
10860
|
+
const detail = (stderr || err.message).trim();
|
|
10861
|
+
reject2(new TmuxError(`tmux ${args[0]} gagal: ${detail}`, NO_SERVER.test(detail)));
|
|
10862
|
+
}
|
|
10863
|
+
);
|
|
10864
|
+
});
|
|
10865
|
+
}
|
|
10266
10866
|
function listPanes() {
|
|
10267
10867
|
let out4;
|
|
10268
10868
|
try {
|
|
@@ -10271,6 +10871,19 @@ function listPanes() {
|
|
|
10271
10871
|
if (e instanceof TmuxError && e.noServer) return [];
|
|
10272
10872
|
throw e;
|
|
10273
10873
|
}
|
|
10874
|
+
return parsePanes(out4);
|
|
10875
|
+
}
|
|
10876
|
+
async function listPanesAsync() {
|
|
10877
|
+
let out4;
|
|
10878
|
+
try {
|
|
10879
|
+
out4 = await tmuxAsync("list-panes", "-a", "-F", FMT);
|
|
10880
|
+
} catch (e) {
|
|
10881
|
+
if (e instanceof TmuxError && e.noServer) return [];
|
|
10882
|
+
throw e;
|
|
10883
|
+
}
|
|
10884
|
+
return parsePanes(out4);
|
|
10885
|
+
}
|
|
10886
|
+
function parsePanes(out4) {
|
|
10274
10887
|
return out4.split("\n").filter(Boolean).flatMap((line) => {
|
|
10275
10888
|
const [
|
|
10276
10889
|
n2,
|
|
@@ -10338,13 +10951,13 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
10338
10951
|
if (existing) killSession(id);
|
|
10339
10952
|
const agentForDefs = opts.agent ?? "claude";
|
|
10340
10953
|
const customDefs = opts.command ? [] : customAgentsFor(projectId, agentForDefs);
|
|
10341
|
-
const rosterBlock = agentForDefs === "codex" ? agentRosterBlock(customDefs) :
|
|
10954
|
+
const rosterBlock = agentForDefs === "codex" ? agentRosterBlock(customDefs) : agentDelegationClause(customDefs);
|
|
10342
10955
|
let promptArg = "";
|
|
10343
10956
|
let promptFile;
|
|
10344
10957
|
if (!opts.command && opts.prompt) {
|
|
10345
10958
|
promptFile = promptFilePath(id);
|
|
10346
|
-
|
|
10347
|
-
|
|
10959
|
+
mkdirSync5(dirname7(promptFile), { recursive: true, mode: 448 });
|
|
10960
|
+
writeFileSync3(promptFile, opts.prompt + rosterBlock, { mode: 384 });
|
|
10348
10961
|
promptArg = `"$(cat ${sq(promptFile)})"`;
|
|
10349
10962
|
}
|
|
10350
10963
|
const agent = opts.agent ?? "claude";
|
|
@@ -10355,8 +10968,8 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
10355
10968
|
let goalGate;
|
|
10356
10969
|
if (agent === "codex" && opts.goal && opts.flow && opts.specId) {
|
|
10357
10970
|
goalGate = goalGatePath(id);
|
|
10358
|
-
|
|
10359
|
-
|
|
10971
|
+
mkdirSync5(dirname7(goalGate), { recursive: true, mode: 448 });
|
|
10972
|
+
writeFileSync3(goalGate, codexGoalScript({
|
|
10360
10973
|
flow: opts.flow,
|
|
10361
10974
|
specId: opts.specId,
|
|
10362
10975
|
condition: opts.goal,
|
|
@@ -10371,8 +10984,8 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
10371
10984
|
const json = renderAgentsJson(customDefs);
|
|
10372
10985
|
if (json) {
|
|
10373
10986
|
agentsFile = agentsFilePath(id);
|
|
10374
|
-
|
|
10375
|
-
|
|
10987
|
+
mkdirSync5(dirname7(agentsFile), { recursive: true, mode: 448 });
|
|
10988
|
+
writeFileSync3(agentsFile, json, { mode: 384 });
|
|
10376
10989
|
}
|
|
10377
10990
|
}
|
|
10378
10991
|
const flags = agentFlags({
|
|
@@ -10394,7 +11007,7 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
10394
11007
|
for (const [k, v] of Object.entries(noTtyPromptEnv())) envPairs.push(`${k}=${sq(v)}`);
|
|
10395
11008
|
}
|
|
10396
11009
|
if (opts.phaseFile) {
|
|
10397
|
-
|
|
11010
|
+
mkdirSync5(dirname7(opts.phaseFile), { recursive: true });
|
|
10398
11011
|
envPairs.push(`HANOMAN_PHASE_FILE=${sq(opts.phaseFile)}`);
|
|
10399
11012
|
}
|
|
10400
11013
|
if (opts.attachmentsDir) envPairs.push(`HANOMAN_ATTACHMENTS_DIR=${sq(opts.attachmentsDir)}`);
|
|
@@ -10407,7 +11020,7 @@ function createSession(projectId, cwd, opts = {}) {
|
|
|
10407
11020
|
promptFile,
|
|
10408
11021
|
attachmentsDir: opts.attachmentsDir
|
|
10409
11022
|
});
|
|
10410
|
-
if (opts.decisionFile)
|
|
11023
|
+
if (opts.decisionFile) mkdirSync5(dirname7(opts.decisionFile), { recursive: true });
|
|
10411
11024
|
tmux(
|
|
10412
11025
|
// tmux menyerahkan argumen perintah `new-session` ke `default-shell`, dan defaultnya adalah
|
|
10413
11026
|
// shell login pemanggil di /etc/passwd. Saat hanoman jalan sebagai user service ber-shell
|
|
@@ -10510,7 +11123,7 @@ async function sendToPane(id, text, chunkMs = 50, choices = []) {
|
|
|
10510
11123
|
const line = text.replace(/\s*\r?\n\s*/g, " ").trim();
|
|
10511
11124
|
if (!line) return false;
|
|
10512
11125
|
try {
|
|
10513
|
-
const io =
|
|
11126
|
+
const io = paneIO(id);
|
|
10514
11127
|
const screen = readDialogScreen(io.capture());
|
|
10515
11128
|
if (screen?.kind === "review") return await submitReview(io, screen.submitRow);
|
|
10516
11129
|
if (screen?.kind === "question") {
|
|
@@ -10545,7 +11158,7 @@ async function submitPaneDialog(id) {
|
|
|
10545
11158
|
const p = getSession(id);
|
|
10546
11159
|
if (!p || p.exited) return false;
|
|
10547
11160
|
try {
|
|
10548
|
-
const io =
|
|
11161
|
+
const io = paneIO(id);
|
|
10549
11162
|
const screen = readDialogScreen(io.capture());
|
|
10550
11163
|
if (screen?.kind !== "review") return false;
|
|
10551
11164
|
return await submitReview(io, screen.submitRow);
|
|
@@ -10669,27 +11282,29 @@ function pollAlt(p, a) {
|
|
|
10669
11282
|
function startPoll() {
|
|
10670
11283
|
if (poll) return;
|
|
10671
11284
|
poll = setInterval(() => {
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
11285
|
+
if (polling) return;
|
|
11286
|
+
polling = true;
|
|
11287
|
+
const snapshot2 = [...attached.entries()];
|
|
11288
|
+
listPanesAsync().then((panes) => {
|
|
11289
|
+
const live = new Map(panes.map((p) => [p.id, p]));
|
|
11290
|
+
for (const [id, a] of snapshot2) {
|
|
11291
|
+
if (attached.get(id) !== a) continue;
|
|
11292
|
+
const p = live.get(id);
|
|
11293
|
+
if (!p) end(id, 0);
|
|
11294
|
+
else if (p.exited) end(id, p.code);
|
|
11295
|
+
else {
|
|
11296
|
+
pollPhases(p, a);
|
|
11297
|
+
pollAlt(p, a);
|
|
11298
|
+
}
|
|
11299
|
+
}
|
|
11300
|
+
}, () => {
|
|
11301
|
+
}).finally(() => {
|
|
11302
|
+
polling = false;
|
|
11303
|
+
if (attached.size === 0 && poll) {
|
|
11304
|
+
clearInterval(poll);
|
|
11305
|
+
poll = void 0;
|
|
10687
11306
|
}
|
|
10688
|
-
}
|
|
10689
|
-
if (attached.size === 0 && poll) {
|
|
10690
|
-
clearInterval(poll);
|
|
10691
|
-
poll = void 0;
|
|
10692
|
-
}
|
|
11307
|
+
});
|
|
10693
11308
|
}, POLL_MS);
|
|
10694
11309
|
poll.unref();
|
|
10695
11310
|
}
|
|
@@ -10760,7 +11375,7 @@ function spawnPty(...args) {
|
|
|
10760
11375
|
);
|
|
10761
11376
|
}
|
|
10762
11377
|
}
|
|
10763
|
-
var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, askpassDenyPath, ASKPASS_DENY, NO_SERVER, TmuxError, sq, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText,
|
|
11378
|
+
var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, markerOnset, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, askpassDenyPath, ASKPASS_DENY, NO_SERVER, TmuxError, sq, idFor, FMT, toSessionInfo, listSessions, listSessionsAsync, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, paneIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, paneComplete, sessionFinished, poll, polling, TERMINAL_QUERY, stripTerminalQueries, detach;
|
|
10764
11379
|
var init_pty = __esm({
|
|
10765
11380
|
"src/services/pty.ts"() {
|
|
10766
11381
|
"use strict";
|
|
@@ -10784,6 +11399,17 @@ var init_pty = __esm({
|
|
|
10784
11399
|
return false;
|
|
10785
11400
|
}
|
|
10786
11401
|
};
|
|
11402
|
+
markerOnset = (f) => {
|
|
11403
|
+
let raw2;
|
|
11404
|
+
try {
|
|
11405
|
+
raw2 = readFileSync6(f, "utf8");
|
|
11406
|
+
} catch {
|
|
11407
|
+
return void 0;
|
|
11408
|
+
}
|
|
11409
|
+
const secs = Number(raw2.trim());
|
|
11410
|
+
if (!Number.isInteger(secs) || secs <= 0) return void 0;
|
|
11411
|
+
return new Date(secs * 1e3).toISOString();
|
|
11412
|
+
};
|
|
10787
11413
|
attached = /* @__PURE__ */ new Map();
|
|
10788
11414
|
claudeBin = () => effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
|
|
10789
11415
|
shellBin = () => effectiveStr("HANOMAN_SHELL") ?? process.env.SHELL ?? "/bin/bash";
|
|
@@ -10825,7 +11451,19 @@ exit 1
|
|
|
10825
11451
|
"#{@hanoman_agent}",
|
|
10826
11452
|
"#{alternate_on}"
|
|
10827
11453
|
].join(" ");
|
|
10828
|
-
|
|
11454
|
+
toSessionInfo = ({
|
|
11455
|
+
id,
|
|
11456
|
+
projectId,
|
|
11457
|
+
specId,
|
|
11458
|
+
flow,
|
|
11459
|
+
cwd,
|
|
11460
|
+
exited,
|
|
11461
|
+
code,
|
|
11462
|
+
branch,
|
|
11463
|
+
decision,
|
|
11464
|
+
agent,
|
|
11465
|
+
decisionFile
|
|
11466
|
+
}) => ({
|
|
10829
11467
|
id,
|
|
10830
11468
|
projectId,
|
|
10831
11469
|
specId,
|
|
@@ -10837,8 +11475,11 @@ exit 1
|
|
|
10837
11475
|
agent,
|
|
10838
11476
|
// Hanya untuk pane mati: `pane_dead_status` kosong pada pane hidup, dan `exitCode: 0` di sana
|
|
10839
11477
|
// akan terbaca sebagai "sudah berakhir sukses".
|
|
10840
|
-
...exited ? { exitCode: code } : {}
|
|
10841
|
-
|
|
11478
|
+
...exited ? { exitCode: code } : {},
|
|
11479
|
+
...decision && decisionFile ? { decisionAt: markerOnset(decisionFile) } : {}
|
|
11480
|
+
});
|
|
11481
|
+
listSessions = () => listPanes().map(toSessionInfo);
|
|
11482
|
+
listSessionsAsync = async () => (await listPanesAsync()).map(toSessionInfo);
|
|
10842
11483
|
liveDecisions = () => listPanes().filter((p) => !p.exited && p.decisionFile).map((p) => ({ id: p.id, specId: p.specId, projectId: p.projectId, decisionFile: p.decisionFile }));
|
|
10843
11484
|
getSession = (id) => listPanes().find((p) => p.id === id);
|
|
10844
11485
|
hooks = {};
|
|
@@ -10872,7 +11513,7 @@ exit 1
|
|
|
10872
11513
|
return "";
|
|
10873
11514
|
}
|
|
10874
11515
|
};
|
|
10875
|
-
|
|
11516
|
+
paneIO = (id) => ({
|
|
10876
11517
|
capture: () => capturePane(id, DIALOG_CAPTURE_LINES),
|
|
10877
11518
|
literal: (s2) => {
|
|
10878
11519
|
tmux("send-keys", "-t", name(id), "-l", s2);
|
|
@@ -10905,6 +11546,7 @@ exit 1
|
|
|
10905
11546
|
const p = getSession(id);
|
|
10906
11547
|
return !!p && paneComplete(p);
|
|
10907
11548
|
};
|
|
11549
|
+
polling = false;
|
|
10908
11550
|
TERMINAL_QUERY = new RegExp([
|
|
10909
11551
|
"\\x1b\\[[?>=]?[0-9;]*c",
|
|
10910
11552
|
// Device Attributes 1/2/3
|
|
@@ -11042,7 +11684,9 @@ var init_tombstone = __esm({
|
|
|
11042
11684
|
// src/services/sync.ts
|
|
11043
11685
|
var sync_exports = {};
|
|
11044
11686
|
__export(sync_exports, {
|
|
11687
|
+
BOOTSTRAP_ORDER: () => BOOTSTRAP_ORDER,
|
|
11045
11688
|
PARENTS: () => PARENTS,
|
|
11689
|
+
PULL_MAX_BYTES: () => PULL_MAX_BYTES,
|
|
11046
11690
|
SYNCED: () => SYNCED,
|
|
11047
11691
|
__DATE_FIELDS: () => __DATE_FIELDS,
|
|
11048
11692
|
__FIELDS: () => __FIELDS,
|
|
@@ -11050,12 +11694,14 @@ __export(sync_exports, {
|
|
|
11050
11694
|
__JSON_FIELDS: () => __JSON_FIELDS,
|
|
11051
11695
|
applyPush: () => applyPush,
|
|
11052
11696
|
backfillFeed: () => backfillFeed,
|
|
11697
|
+
bootstrapSnapshot: () => bootstrapSnapshot,
|
|
11053
11698
|
consumeTombstoneOnRecreate: () => consumeTombstoneOnRecreate,
|
|
11054
11699
|
deleteRow: () => deleteRow,
|
|
11055
11700
|
isEntity: () => isEntity,
|
|
11056
11701
|
publishDelete: () => publishDelete,
|
|
11057
11702
|
publishLocal: () => publishLocal,
|
|
11058
11703
|
pull: () => pull,
|
|
11704
|
+
recordBytes: () => recordBytes,
|
|
11059
11705
|
setAcceptedHook: () => setAcceptedHook,
|
|
11060
11706
|
snapshot: () => snapshot,
|
|
11061
11707
|
upsertLocal: () => upsertLocal,
|
|
@@ -11182,24 +11828,77 @@ async function applyPush(entity, id, baseVersion, data, deviceId, op = "upsert")
|
|
|
11182
11828
|
onAccepted?.({ entity, recordId: id, version: newVersion, op: "upsert", data: snap?.data ?? {}, seq: String(log2.seq) });
|
|
11183
11829
|
return { ok: true, version: newVersion };
|
|
11184
11830
|
}
|
|
11185
|
-
|
|
11831
|
+
function recordBytes(rec) {
|
|
11832
|
+
return Buffer.byteLength(JSON.stringify(rec));
|
|
11833
|
+
}
|
|
11834
|
+
async function pull(sinceCursor, limit = 500, maxBytes = PULL_MAX_BYTES) {
|
|
11186
11835
|
const since = Number(sinceCursor || "0");
|
|
11187
11836
|
const rows = await prisma.syncLog.findMany({
|
|
11188
11837
|
where: { seq: { gt: since } },
|
|
11189
11838
|
orderBy: { seq: "asc" },
|
|
11190
11839
|
take: limit
|
|
11191
11840
|
});
|
|
11192
|
-
const
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11841
|
+
const records = [];
|
|
11842
|
+
let bytes = 0;
|
|
11843
|
+
let trimmed = false;
|
|
11844
|
+
for (const r of rows) {
|
|
11845
|
+
const rec = {
|
|
11196
11846
|
entity: r.entity,
|
|
11197
11847
|
recordId: r.recordId,
|
|
11198
11848
|
version: r.version,
|
|
11199
11849
|
op: r.op === "delete" ? "delete" : "upsert",
|
|
11200
11850
|
data: r.data
|
|
11201
|
-
}
|
|
11202
|
-
|
|
11851
|
+
};
|
|
11852
|
+
const size = recordBytes(rec);
|
|
11853
|
+
if (records.length && bytes + size > maxBytes) {
|
|
11854
|
+
trimmed = true;
|
|
11855
|
+
break;
|
|
11856
|
+
}
|
|
11857
|
+
bytes += size;
|
|
11858
|
+
records.push(rec);
|
|
11859
|
+
}
|
|
11860
|
+
const cursor = records.length ? String(rows[records.length - 1].seq) : sinceCursor || "0";
|
|
11861
|
+
return { cursor, records, hasMore: trimmed || rows.length === limit };
|
|
11862
|
+
}
|
|
11863
|
+
async function bootstrapSnapshot(after, maxBytes = PULL_MAX_BYTES) {
|
|
11864
|
+
const tip = await prisma.syncLog.findFirst({ orderBy: { seq: "desc" }, select: { seq: true } });
|
|
11865
|
+
const cursor = tip ? String(tip.seq) : "0";
|
|
11866
|
+
const sep5 = after ? after.indexOf(":") : -1;
|
|
11867
|
+
const afterEntity = sep5 > 0 ? after.slice(0, sep5) : null;
|
|
11868
|
+
const afterId = sep5 > 0 ? after.slice(sep5 + 1) : null;
|
|
11869
|
+
const startAt = afterEntity ? BOOTSTRAP_ORDER.indexOf(afterEntity) : 0;
|
|
11870
|
+
if (startAt < 0) throw new Error("bootstrap cursor tak dikenal");
|
|
11871
|
+
const records = [];
|
|
11872
|
+
let bytes = 0;
|
|
11873
|
+
let last = null;
|
|
11874
|
+
for (let i = startAt; i < BOOTSTRAP_ORDER.length; i++) {
|
|
11875
|
+
const entity = BOOTSTRAP_ORDER[i];
|
|
11876
|
+
const gt = i === startAt && afterId ? afterId : null;
|
|
11877
|
+
const rows = await DELEGATE[entity].findMany({
|
|
11878
|
+
...gt ? { where: { id: { gt } } } : {},
|
|
11879
|
+
orderBy: { id: "asc" },
|
|
11880
|
+
select: { id: true }
|
|
11881
|
+
});
|
|
11882
|
+
for (const row of rows) {
|
|
11883
|
+
const snap = await snapshot(entity, row.id);
|
|
11884
|
+
if (!snap) continue;
|
|
11885
|
+
const rec = {
|
|
11886
|
+
entity,
|
|
11887
|
+
recordId: row.id,
|
|
11888
|
+
version: snap.version,
|
|
11889
|
+
op: "upsert",
|
|
11890
|
+
data: snap.data
|
|
11891
|
+
};
|
|
11892
|
+
const size = recordBytes(rec);
|
|
11893
|
+
if (records.length && bytes + size > maxBytes) {
|
|
11894
|
+
return { cursor, records, hasMore: true, next: last };
|
|
11895
|
+
}
|
|
11896
|
+
bytes += size;
|
|
11897
|
+
records.push(rec);
|
|
11898
|
+
last = `${entity}:${row.id}`;
|
|
11899
|
+
}
|
|
11900
|
+
}
|
|
11901
|
+
return { cursor, records, hasMore: false, next: null };
|
|
11203
11902
|
}
|
|
11204
11903
|
async function publishLocal(entity, id) {
|
|
11205
11904
|
const snap = await snapshot(entity, id);
|
|
@@ -11291,7 +11990,7 @@ async function upsertLocal(entity, id, version2, data) {
|
|
|
11291
11990
|
function setAcceptedHook(hook) {
|
|
11292
11991
|
onAccepted = hook;
|
|
11293
11992
|
}
|
|
11294
|
-
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, __JSON_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
11993
|
+
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, __JSON_FIELDS, PULL_MAX_BYTES, BOOTSTRAP_ORDER, onAccepted, __FIELDS_FOR_TEST;
|
|
11295
11994
|
var init_sync = __esm({
|
|
11296
11995
|
"src/services/sync.ts"() {
|
|
11297
11996
|
"use strict";
|
|
@@ -11310,7 +12009,12 @@ var init_sync = __esm({
|
|
|
11310
12009
|
githubIssue: prisma.githubIssue
|
|
11311
12010
|
};
|
|
11312
12011
|
FIELDS = {
|
|
11313
|
-
|
|
12012
|
+
// SPEC-880 · ADR-0135 · `handledBy` ikut menyeberang: "project ini dipegang mesin yang mana"
|
|
12013
|
+
// adalah pernyataan bersama, bukan setelan mesin — dan justru menyeberangnya nilai itu yang
|
|
12014
|
+
// jadi inti spec-nya. SENGAJA bukan cermin `repoDir`/`schedulerOptIn`/`autoMerge` yang LOCAL-only
|
|
12015
|
+
// (mereka properti checkout mesin ini). Tiap entri membawa `name` karena `DeviceToken` TIDAK
|
|
12016
|
+
// ikut SYNCED: penerima tak punya baris device untuk di-join. BUKAN DATE_FIELDS.
|
|
12017
|
+
project: ["name", "desc", "kind", "stack", "gitRemote", "handledBy", "updatedAt"],
|
|
11314
12018
|
// SPEC-408 · ADR-0090 · createdAt/startedAt ikut menyeberang — sejajar baseSha/headSha. Tanpa
|
|
11315
12019
|
// ini spec asal-hub mendapat createdAt lokal palsu di tiap client (kolom NOT NULL ber-default).
|
|
11316
12020
|
// SPEC-447 · ADR-0093 · dependsOn ikut juga: tanpa itu client tak tahu urutannya dan akan
|
|
@@ -11391,6 +12095,7 @@ var init_sync = __esm({
|
|
|
11391
12095
|
]);
|
|
11392
12096
|
BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["vps:hardened", "customAgent:enabled"]);
|
|
11393
12097
|
JSON_FIELDS = /* @__PURE__ */ new Set([
|
|
12098
|
+
"project:handledBy",
|
|
11394
12099
|
"spec:payload",
|
|
11395
12100
|
"spec:dependsOn",
|
|
11396
12101
|
"spec:sourceHistory",
|
|
@@ -11402,6 +12107,17 @@ var init_sync = __esm({
|
|
|
11402
12107
|
"githubIssue:labels"
|
|
11403
12108
|
]);
|
|
11404
12109
|
__JSON_FIELDS = JSON_FIELDS;
|
|
12110
|
+
PULL_MAX_BYTES = 1024 * 1024;
|
|
12111
|
+
BOOTSTRAP_ORDER = [
|
|
12112
|
+
"project",
|
|
12113
|
+
"spec",
|
|
12114
|
+
"ticket",
|
|
12115
|
+
"customAgent",
|
|
12116
|
+
"githubIssue",
|
|
12117
|
+
"ticketAttachment",
|
|
12118
|
+
"vps",
|
|
12119
|
+
"sessionResult"
|
|
12120
|
+
];
|
|
11405
12121
|
__FIELDS_FOR_TEST = FIELDS;
|
|
11406
12122
|
}
|
|
11407
12123
|
});
|
|
@@ -11484,15 +12200,17 @@ var init_settings2 = __esm({
|
|
|
11484
12200
|
// SPEC-476 · ADR-0096 · gateway Telegram opt-in
|
|
11485
12201
|
changelog: CHANGELOG_ENGINE_DEFAULTS,
|
|
11486
12202
|
// SPEC-518 · agen pembuat changelog (opt-in, mati)
|
|
11487
|
-
portalChat: PORTAL_CHAT_DEFAULTS
|
|
12203
|
+
portalChat: PORTAL_CHAT_DEFAULTS,
|
|
11488
12204
|
// SPEC-854 · ADR-0130 · chat portal klien (opt-in, mati)
|
|
12205
|
+
builtinAgents: {}
|
|
12206
|
+
// SPEC-881 · ADR-0136 · sidik jari seed (lokal, tak disync)
|
|
11489
12207
|
};
|
|
11490
12208
|
RETIRED_MODELS = { "claude-opus-4-8": "claude-opus-5" };
|
|
11491
12209
|
}
|
|
11492
12210
|
});
|
|
11493
12211
|
|
|
11494
12212
|
// src/services/codex-trust.ts
|
|
11495
|
-
import { appendFileSync, mkdirSync as
|
|
12213
|
+
import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
|
|
11496
12214
|
import { homedir as homedir3 } from "node:os";
|
|
11497
12215
|
function ensureCodexTrust(repoDir, home3) {
|
|
11498
12216
|
const path2 = codexConfigPath(home3);
|
|
@@ -11505,11 +12223,11 @@ function ensureCodexTrust(repoDir, home3) {
|
|
|
11505
12223
|
try {
|
|
11506
12224
|
let existing = "";
|
|
11507
12225
|
try {
|
|
11508
|
-
existing =
|
|
12226
|
+
existing = readFileSync8(path2, "utf8");
|
|
11509
12227
|
} catch {
|
|
11510
12228
|
}
|
|
11511
12229
|
if (existing.includes(header2)) return;
|
|
11512
|
-
|
|
12230
|
+
mkdirSync7(codexHome(home3), { recursive: true });
|
|
11513
12231
|
const lead = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
11514
12232
|
appendFileSync(path2, `${lead}
|
|
11515
12233
|
${header2}
|
|
@@ -12417,7 +13135,7 @@ var require_filesystem = __commonJS({
|
|
|
12417
13135
|
var LDD_PATH = "/usr/bin/ldd";
|
|
12418
13136
|
var SELF_PATH = "/proc/self/exe";
|
|
12419
13137
|
var MAX_LENGTH = 2048;
|
|
12420
|
-
var
|
|
13138
|
+
var readFileSync17 = (path2) => {
|
|
12421
13139
|
const fd = fs.openSync(path2, "r");
|
|
12422
13140
|
const buffer2 = Buffer.alloc(MAX_LENGTH);
|
|
12423
13141
|
const bytesRead = fs.readSync(fd, buffer2, 0, MAX_LENGTH, 0);
|
|
@@ -12442,7 +13160,7 @@ var require_filesystem = __commonJS({
|
|
|
12442
13160
|
module.exports = {
|
|
12443
13161
|
LDD_PATH,
|
|
12444
13162
|
SELF_PATH,
|
|
12445
|
-
readFileSync:
|
|
13163
|
+
readFileSync: readFileSync17,
|
|
12446
13164
|
readFile: readFile6
|
|
12447
13165
|
};
|
|
12448
13166
|
}
|
|
@@ -12491,7 +13209,7 @@ var require_detect_libc = __commonJS({
|
|
|
12491
13209
|
"use strict";
|
|
12492
13210
|
var childProcess = __require("child_process");
|
|
12493
13211
|
var { isLinux, getReport } = require_process();
|
|
12494
|
-
var { LDD_PATH, SELF_PATH, readFile: readFile6, readFileSync:
|
|
13212
|
+
var { LDD_PATH, SELF_PATH, readFile: readFile6, readFileSync: readFileSync17 } = require_filesystem();
|
|
12495
13213
|
var { interpreterPath } = require_elf();
|
|
12496
13214
|
var cachedFamilyInterpreter;
|
|
12497
13215
|
var cachedFamilyFilesystem;
|
|
@@ -12583,7 +13301,7 @@ var require_detect_libc = __commonJS({
|
|
|
12583
13301
|
}
|
|
12584
13302
|
cachedFamilyFilesystem = null;
|
|
12585
13303
|
try {
|
|
12586
|
-
const lddContent =
|
|
13304
|
+
const lddContent = readFileSync17(LDD_PATH);
|
|
12587
13305
|
cachedFamilyFilesystem = getFamilyFromLddContent(lddContent);
|
|
12588
13306
|
} catch (e) {
|
|
12589
13307
|
}
|
|
@@ -12608,7 +13326,7 @@ var require_detect_libc = __commonJS({
|
|
|
12608
13326
|
}
|
|
12609
13327
|
cachedFamilyInterpreter = null;
|
|
12610
13328
|
try {
|
|
12611
|
-
const selfContent =
|
|
13329
|
+
const selfContent = readFileSync17(SELF_PATH);
|
|
12612
13330
|
const path2 = interpreterPath(selfContent);
|
|
12613
13331
|
cachedFamilyInterpreter = familyFromInterpreterPath(path2);
|
|
12614
13332
|
} catch (e) {
|
|
@@ -12672,7 +13390,7 @@ var require_detect_libc = __commonJS({
|
|
|
12672
13390
|
}
|
|
12673
13391
|
cachedVersionFilesystem = null;
|
|
12674
13392
|
try {
|
|
12675
|
-
const lddContent =
|
|
13393
|
+
const lddContent = readFileSync17(LDD_PATH);
|
|
12676
13394
|
const versionMatch = lddContent.match(RE_GLIBC_VERSION);
|
|
12677
13395
|
if (versionMatch) {
|
|
12678
13396
|
cachedVersionFilesystem = versionMatch[1];
|
|
@@ -13590,10 +14308,10 @@ var require_truncate = __commonJS({
|
|
|
13590
14308
|
"../node_modules/.pnpm/semver@7.8.5/node_modules/semver/functions/truncate.js"(exports, module) {
|
|
13591
14309
|
"use strict";
|
|
13592
14310
|
var parse3 = require_parse();
|
|
13593
|
-
var
|
|
14311
|
+
var constants3 = require_constants();
|
|
13594
14312
|
var SemVer = require_semver();
|
|
13595
14313
|
var truncate = (version2, truncation, options2) => {
|
|
13596
|
-
if (!
|
|
14314
|
+
if (!constants3.RELEASE_TYPES.includes(truncation)) {
|
|
13597
14315
|
return null;
|
|
13598
14316
|
}
|
|
13599
14317
|
const clonedVersion = cloneInputVersion(version2, options2);
|
|
@@ -14642,7 +15360,7 @@ var require_semver2 = __commonJS({
|
|
|
14642
15360
|
"../node_modules/.pnpm/semver@7.8.5/node_modules/semver/index.js"(exports, module) {
|
|
14643
15361
|
"use strict";
|
|
14644
15362
|
var internalRe = require_re();
|
|
14645
|
-
var
|
|
15363
|
+
var constants3 = require_constants();
|
|
14646
15364
|
var SemVer = require_semver();
|
|
14647
15365
|
var identifiers = require_identifiers();
|
|
14648
15366
|
var parse3 = require_parse();
|
|
@@ -14726,8 +15444,8 @@ var require_semver2 = __commonJS({
|
|
|
14726
15444
|
re: internalRe.re,
|
|
14727
15445
|
src: internalRe.src,
|
|
14728
15446
|
tokens: internalRe.t,
|
|
14729
|
-
SEMVER_SPEC_VERSION:
|
|
14730
|
-
RELEASE_TYPES:
|
|
15447
|
+
SEMVER_SPEC_VERSION: constants3.SEMVER_SPEC_VERSION,
|
|
15448
|
+
RELEASE_TYPES: constants3.RELEASE_TYPES,
|
|
14731
15449
|
compareIdentifiers: identifiers.compareIdentifiers,
|
|
14732
15450
|
rcompareIdentifiers: identifiers.rcompareIdentifiers
|
|
14733
15451
|
};
|
|
@@ -17487,7 +18205,7 @@ __export(bootstrap_exports, {
|
|
|
17487
18205
|
reloadTelegramGateway: () => reloadTelegramGateway,
|
|
17488
18206
|
telegramSessionDeps: () => telegramSessionDeps
|
|
17489
18207
|
});
|
|
17490
|
-
import { mkdirSync as
|
|
18208
|
+
import { mkdirSync as mkdirSync8 } from "node:fs";
|
|
17491
18209
|
function parseTelegramAllowedUserIds(raw2) {
|
|
17492
18210
|
const ids = raw2.split(/[\s,]+/).filter(Boolean);
|
|
17493
18211
|
if (!ids.length) throw new Error("Telegram allowlist wajib berisi numeric user id");
|
|
@@ -17512,7 +18230,7 @@ function telegramSessionDeps(input) {
|
|
|
17512
18230
|
home: resolveHome(),
|
|
17513
18231
|
apiBase: input.apiBase,
|
|
17514
18232
|
agentToken: input.agentToken,
|
|
17515
|
-
ensureDir: (path2) =>
|
|
18233
|
+
ensureDir: (path2) => mkdirSync8(path2, { recursive: true })
|
|
17516
18234
|
};
|
|
17517
18235
|
}
|
|
17518
18236
|
async function productionFactory(input) {
|
|
@@ -19988,7 +20706,7 @@ var require_websocket = __commonJS({
|
|
|
19988
20706
|
var http = __require("http");
|
|
19989
20707
|
var net = __require("net");
|
|
19990
20708
|
var tls = __require("tls");
|
|
19991
|
-
var { randomBytes: randomBytes11, createHash:
|
|
20709
|
+
var { randomBytes: randomBytes11, createHash: createHash12 } = __require("crypto");
|
|
19992
20710
|
var { Duplex, Readable } = __require("stream");
|
|
19993
20711
|
var { URL: URL2 } = __require("url");
|
|
19994
20712
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -20656,7 +21374,7 @@ var require_websocket = __commonJS({
|
|
|
20656
21374
|
abortHandshake(websocket2, socket2, "Invalid Upgrade header");
|
|
20657
21375
|
return;
|
|
20658
21376
|
}
|
|
20659
|
-
const digest =
|
|
21377
|
+
const digest = createHash12("sha1").update(key + GUID).digest("base64");
|
|
20660
21378
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
20661
21379
|
abortHandshake(websocket2, socket2, "Invalid Sec-WebSocket-Accept header");
|
|
20662
21380
|
return;
|
|
@@ -21025,7 +21743,7 @@ var require_websocket_server = __commonJS({
|
|
|
21025
21743
|
var EventEmitter = __require("events");
|
|
21026
21744
|
var http = __require("http");
|
|
21027
21745
|
var { Duplex } = __require("stream");
|
|
21028
|
-
var { createHash:
|
|
21746
|
+
var { createHash: createHash12 } = __require("crypto");
|
|
21029
21747
|
var extension2 = require_extension();
|
|
21030
21748
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
21031
21749
|
var subprotocol2 = require_subprotocol();
|
|
@@ -21332,7 +22050,7 @@ var require_websocket_server = __commonJS({
|
|
|
21332
22050
|
);
|
|
21333
22051
|
}
|
|
21334
22052
|
if (this._state > RUNNING) return abortHandshake(socket2, 503);
|
|
21335
|
-
const digest =
|
|
22053
|
+
const digest = createHash12("sha1").update(key + GUID).digest("base64");
|
|
21336
22054
|
const headers = [
|
|
21337
22055
|
"HTTP/1.1 101 Switching Protocols",
|
|
21338
22056
|
"Upgrade: websocket",
|
|
@@ -21524,9 +22242,50 @@ async function applyFeedFrame(msg) {
|
|
|
21524
22242
|
if (msg.seq && !feedHole) await setCursor(String(msg.seq));
|
|
21525
22243
|
return true;
|
|
21526
22244
|
}
|
|
22245
|
+
async function retryDeferred(rest, stats2) {
|
|
22246
|
+
while (rest.length) {
|
|
22247
|
+
const still = [];
|
|
22248
|
+
for (const rec of rest) {
|
|
22249
|
+
try {
|
|
22250
|
+
const r = await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op ?? "upsert");
|
|
22251
|
+
if (r === "dropped") stats2.dropped++;
|
|
22252
|
+
else if (rec.op === "delete") stats2.deleted++;
|
|
22253
|
+
else stats2.pulled++;
|
|
22254
|
+
} catch {
|
|
22255
|
+
still.push(rec);
|
|
22256
|
+
}
|
|
22257
|
+
}
|
|
22258
|
+
if (still.length === rest.length) return still;
|
|
22259
|
+
rest = still;
|
|
22260
|
+
}
|
|
22261
|
+
return [];
|
|
22262
|
+
}
|
|
22263
|
+
async function bootstrapOnce(transport) {
|
|
22264
|
+
if (await getCursor() !== "0") return null;
|
|
22265
|
+
if ((await listOutbox()).length) return null;
|
|
22266
|
+
let after = null;
|
|
22267
|
+
let cursor = "0";
|
|
22268
|
+
let applied = 0;
|
|
22269
|
+
for (let page = 0; page < MAX_DRAIN_PAGES; page++) {
|
|
22270
|
+
const q = after ? `?after=${encodeURIComponent(after)}` : "";
|
|
22271
|
+
const res = await transport("GET", `/api/sync/bootstrap${q}`);
|
|
22272
|
+
if (res.status !== 200 || !Array.isArray(res.body?.records)) return null;
|
|
22273
|
+
for (const raw2 of res.body.records) {
|
|
22274
|
+
const rec = validateIncomingRecord(raw2);
|
|
22275
|
+
if (!rec.op) continue;
|
|
22276
|
+
await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op);
|
|
22277
|
+
applied++;
|
|
22278
|
+
}
|
|
22279
|
+
cursor = String(res.body.cursor ?? "0");
|
|
22280
|
+
const next = res.body.next ? String(res.body.next) : null;
|
|
22281
|
+
if (!res.body.hasMore || !next) break;
|
|
22282
|
+
after = next;
|
|
22283
|
+
}
|
|
22284
|
+
await setCursor(cursor);
|
|
22285
|
+
return applied;
|
|
22286
|
+
}
|
|
21527
22287
|
async function syncOnce(transport) {
|
|
21528
|
-
|
|
21529
|
-
const cursor = await getCursor();
|
|
22288
|
+
const stats2 = { pulled: 0, pushed: 0, conflicts: 0, deleted: 0, dropped: 0 };
|
|
21530
22289
|
const outbox = await listOutbox();
|
|
21531
22290
|
const pending2 = new Set(outbox.map((o) => `${o.entity}:${o.recordId}`));
|
|
21532
22291
|
const conflicted = /* @__PURE__ */ new Set();
|
|
@@ -21535,64 +22294,53 @@ async function syncOnce(transport) {
|
|
|
21535
22294
|
const key = `${entity}:${recordId}`;
|
|
21536
22295
|
if (!conflicted.has(key)) {
|
|
21537
22296
|
conflicted.add(key);
|
|
21538
|
-
conflicts++;
|
|
22297
|
+
stats2.conflicts++;
|
|
21539
22298
|
}
|
|
21540
22299
|
};
|
|
21541
|
-
|
|
21542
|
-
|
|
21543
|
-
|
|
21544
|
-
|
|
21545
|
-
|
|
21546
|
-
|
|
21547
|
-
|
|
21548
|
-
|
|
21549
|
-
|
|
21550
|
-
|
|
21551
|
-
|
|
21552
|
-
|
|
21553
|
-
if (
|
|
21554
|
-
await
|
|
21555
|
-
|
|
21556
|
-
|
|
21557
|
-
|
|
21558
|
-
|
|
21559
|
-
|
|
22300
|
+
let deferred = [];
|
|
22301
|
+
for (let page = 0; page < MAX_DRAIN_PAGES; page++) {
|
|
22302
|
+
const cursor = await getCursor();
|
|
22303
|
+
const pullRes = await transport("GET", `/api/sync/pull?since=${cursor}`);
|
|
22304
|
+
const rawRecords = Array.isArray(pullRes.body?.records) ? pullRes.body.records : [];
|
|
22305
|
+
const records = rawRecords.map(validateIncomingRecord);
|
|
22306
|
+
for (const rec of records) {
|
|
22307
|
+
if (!isEntity(rec.entity)) continue;
|
|
22308
|
+
if (!rec.op) {
|
|
22309
|
+
stats2.dropped++;
|
|
22310
|
+
continue;
|
|
22311
|
+
}
|
|
22312
|
+
if (rec.op === "upsert" && pending2.has(`${rec.entity}:${rec.recordId}`)) {
|
|
22313
|
+
const local = await snapshot(rec.entity, rec.recordId);
|
|
22314
|
+
if (local && JSON.stringify(local.data) !== JSON.stringify(rec.data)) {
|
|
22315
|
+
await markConflict(
|
|
22316
|
+
rec.entity,
|
|
22317
|
+
rec.recordId,
|
|
22318
|
+
{ version: local.version, data: local.data },
|
|
22319
|
+
{ version: rec.version, data: rec.data }
|
|
22320
|
+
);
|
|
22321
|
+
}
|
|
22322
|
+
continue;
|
|
21560
22323
|
}
|
|
21561
|
-
continue;
|
|
21562
|
-
}
|
|
21563
|
-
try {
|
|
21564
|
-
const r = await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op);
|
|
21565
|
-
if (r === "dropped") dropped++;
|
|
21566
|
-
else if (rec.op === "delete") deleted++;
|
|
21567
|
-
else pulled++;
|
|
21568
|
-
} catch {
|
|
21569
|
-
deferred.push(rec);
|
|
21570
|
-
}
|
|
21571
|
-
}
|
|
21572
|
-
let rest = deferred;
|
|
21573
|
-
while (rest.length) {
|
|
21574
|
-
const still = [];
|
|
21575
|
-
for (const rec of rest) {
|
|
21576
22324
|
try {
|
|
21577
|
-
const r = await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op
|
|
21578
|
-
if (r === "dropped") dropped++;
|
|
21579
|
-
else if (rec.op === "delete") deleted++;
|
|
21580
|
-
else pulled++;
|
|
22325
|
+
const r = await applyRemote(rec.entity, rec.recordId, rec.version, rec.data, rec.op);
|
|
22326
|
+
if (r === "dropped") stats2.dropped++;
|
|
22327
|
+
else if (rec.op === "delete") stats2.deleted++;
|
|
22328
|
+
else stats2.pulled++;
|
|
21581
22329
|
} catch {
|
|
21582
|
-
|
|
22330
|
+
deferred.push(rec);
|
|
21583
22331
|
}
|
|
21584
22332
|
}
|
|
21585
|
-
|
|
21586
|
-
|
|
21587
|
-
|
|
21588
|
-
|
|
21589
|
-
|
|
21590
|
-
|
|
21591
|
-
|
|
21592
|
-
|
|
22333
|
+
deferred = await retryDeferred(deferred, stats2);
|
|
22334
|
+
const next = pullRes.body?.cursor ? String(pullRes.body.cursor) : cursor;
|
|
22335
|
+
if (next !== cursor) await setCursor(next);
|
|
22336
|
+
feedHole = false;
|
|
22337
|
+
const more = pullRes.body?.hasMore ?? records.length > 0;
|
|
22338
|
+
if (!more || next === cursor) break;
|
|
22339
|
+
}
|
|
22340
|
+
for (const rec of deferred) {
|
|
22341
|
+
console.warn(`sync: record ${rec.entity}:${rec.recordId} tak bisa diterapkan \u2014 dilewati`);
|
|
22342
|
+
stats2.dropped++;
|
|
21593
22343
|
}
|
|
21594
|
-
if (pullRes.body?.cursor) await setCursor(String(pullRes.body.cursor));
|
|
21595
|
-
feedHole = false;
|
|
21596
22344
|
for (const item of outbox) {
|
|
21597
22345
|
if (item.entity === "projectRename") {
|
|
21598
22346
|
const [oldId, newId] = item.recordId.split(RENAME_SEP);
|
|
@@ -21607,9 +22355,9 @@ async function syncOnce(transport) {
|
|
|
21607
22355
|
const r2 = res2.body?.results?.[0];
|
|
21608
22356
|
if (r2?.ok) {
|
|
21609
22357
|
await clearOutbox(item.entity, item.recordId);
|
|
21610
|
-
pushed++;
|
|
22358
|
+
stats2.pushed++;
|
|
21611
22359
|
} else if (r2?.conflict) {
|
|
21612
|
-
conflicts++;
|
|
22360
|
+
stats2.conflicts++;
|
|
21613
22361
|
}
|
|
21614
22362
|
continue;
|
|
21615
22363
|
}
|
|
@@ -21635,7 +22383,7 @@ async function syncOnce(transport) {
|
|
|
21635
22383
|
});
|
|
21636
22384
|
if (res2.body?.results?.[0]?.ok) {
|
|
21637
22385
|
await clearOutbox(item.entity, item.recordId);
|
|
21638
|
-
pushed++;
|
|
22386
|
+
stats2.pushed++;
|
|
21639
22387
|
}
|
|
21640
22388
|
continue;
|
|
21641
22389
|
}
|
|
@@ -21643,6 +22391,9 @@ async function syncOnce(transport) {
|
|
|
21643
22391
|
records: [{ entity: item.entity, id: item.recordId, baseVersion: snap.version, data: snap.data }]
|
|
21644
22392
|
});
|
|
21645
22393
|
const r = res.body?.results?.[0];
|
|
22394
|
+
if (!r) {
|
|
22395
|
+
console.warn(`sync: push ${item.entity}:${item.recordId} tak dijawab hub (status ${res.status}) \u2014 tetap di outbox; periksa apakah hub lebih tua dari client ini`);
|
|
22396
|
+
}
|
|
21646
22397
|
if (r?.ok) {
|
|
21647
22398
|
if (typeof r.version === "number") {
|
|
21648
22399
|
const delegate = prisma[item.entity];
|
|
@@ -21650,7 +22401,7 @@ async function syncOnce(transport) {
|
|
|
21650
22401
|
});
|
|
21651
22402
|
}
|
|
21652
22403
|
await clearOutbox(item.entity, item.recordId);
|
|
21653
|
-
pushed++;
|
|
22404
|
+
stats2.pushed++;
|
|
21654
22405
|
} else if (r?.conflict) {
|
|
21655
22406
|
if (r.deleted) {
|
|
21656
22407
|
await applyRemote(
|
|
@@ -21661,7 +22412,7 @@ async function syncOnce(transport) {
|
|
|
21661
22412
|
"delete"
|
|
21662
22413
|
);
|
|
21663
22414
|
await clearOutbox(item.entity, item.recordId);
|
|
21664
|
-
deleted++;
|
|
22415
|
+
stats2.deleted++;
|
|
21665
22416
|
continue;
|
|
21666
22417
|
}
|
|
21667
22418
|
const server = r.server;
|
|
@@ -21678,7 +22429,7 @@ async function syncOnce(transport) {
|
|
|
21678
22429
|
}
|
|
21679
22430
|
}
|
|
21680
22431
|
}
|
|
21681
|
-
return
|
|
22432
|
+
return stats2;
|
|
21682
22433
|
}
|
|
21683
22434
|
function fetchTransport(base2, token) {
|
|
21684
22435
|
return async (method, path2, body) => {
|
|
@@ -21687,12 +22438,26 @@ function fetchTransport(base2, token) {
|
|
|
21687
22438
|
const res = await safeRequest({
|
|
21688
22439
|
url: url2,
|
|
21689
22440
|
method,
|
|
21690
|
-
headers: {
|
|
22441
|
+
headers: {
|
|
22442
|
+
authorization: `Bearer ${token}`,
|
|
22443
|
+
// SPEC-885 · ADR-0138 · hub lama mengabaikan header ini dan membalas polos; `safeRequest`
|
|
22444
|
+
// hanya men-decompress bila balasannya benar-benar ber-`content-encoding: gzip`.
|
|
22445
|
+
"accept-encoding": "gzip",
|
|
22446
|
+
...body ? { "content-type": "application/json" } : {}
|
|
22447
|
+
},
|
|
21691
22448
|
...body ? { body: Buffer.from(JSON.stringify(body)) } : {},
|
|
21692
22449
|
allowPrivate: process.env.NODE_ENV !== "production" && loopback,
|
|
21693
22450
|
connectMs: 5e3,
|
|
21694
22451
|
totalMs: 15e3,
|
|
21695
|
-
|
|
22452
|
+
// SPEC-885 · ADR-0138 · cap ini dulu 2 MB, dan halaman feed 2,51 MB di hub produksi
|
|
22453
|
+
// membuat setiap client baru MANDEK di situ selamanya — bukan lambat, mandek, dan tanpa
|
|
22454
|
+
// satu baris log. Hub yang sudah membawa anggaran byte memotong halamannya di 1 MB, jadi
|
|
22455
|
+
// cap ini tak akan tersentuh olehnya. Ia dinaikkan justru untuk hub yang BELUM naik versi,
|
|
22456
|
+
// yang tetap mengirim 500 baris apa adanya — kombinasi yang dialami tiap
|
|
22457
|
+
// `npm i -g hanoman` sebelum hub-nya diperbarui (urutan rilis hub-duluan, ADR-0135).
|
|
22458
|
+
maxResponseBytes: 8 * 1024 * 1024,
|
|
22459
|
+
acceptEncoding: "gzip",
|
|
22460
|
+
maxDecodedBytes: 16 * 1024 * 1024
|
|
21696
22461
|
});
|
|
21697
22462
|
let parsed = null;
|
|
21698
22463
|
try {
|
|
@@ -21707,22 +22472,11 @@ async function syncNow(opts) {
|
|
|
21707
22472
|
const token = effectiveStr("SYNC_DEVICE_TOKEN");
|
|
21708
22473
|
if (!base2 || !token) return null;
|
|
21709
22474
|
const transport = fetchTransport(base2, token);
|
|
21710
|
-
if (
|
|
21711
|
-
await
|
|
21712
|
-
const
|
|
21713
|
-
|
|
21714
|
-
|
|
21715
|
-
const s2 = await syncOnce(transport);
|
|
21716
|
-
total.pulled += s2.pulled;
|
|
21717
|
-
total.pushed += s2.pushed;
|
|
21718
|
-
total.conflicts += s2.conflicts;
|
|
21719
|
-
total.deleted += s2.deleted;
|
|
21720
|
-
total.dropped += s2.dropped;
|
|
21721
|
-
const now = await getCursor();
|
|
21722
|
-
if (now === seen) break;
|
|
21723
|
-
seen = now;
|
|
21724
|
-
}
|
|
21725
|
-
return total;
|
|
22475
|
+
if (opts?.full) await setCursor("0");
|
|
22476
|
+
const booted = await bootstrapOnce(transport).catch(() => null);
|
|
22477
|
+
const stats2 = await syncOnce(transport);
|
|
22478
|
+
if (booted) stats2.pulled += booted;
|
|
22479
|
+
return stats2;
|
|
21726
22480
|
}
|
|
21727
22481
|
function syncStatus() {
|
|
21728
22482
|
return {
|
|
@@ -21731,15 +22485,24 @@ function syncStatus() {
|
|
|
21731
22485
|
/* OPEN */
|
|
21732
22486
|
};
|
|
21733
22487
|
}
|
|
22488
|
+
async function syncTick(transport) {
|
|
22489
|
+
try {
|
|
22490
|
+
await syncOnce(transport);
|
|
22491
|
+
if (!pullSehat) {
|
|
22492
|
+
console.info("sync: pull pulih");
|
|
22493
|
+
pullSehat = true;
|
|
22494
|
+
}
|
|
22495
|
+
} catch (e) {
|
|
22496
|
+
if (pullSehat) {
|
|
22497
|
+
console.warn(`sync: pull gagal \u2014 ${e.message}`);
|
|
22498
|
+
pullSehat = false;
|
|
22499
|
+
}
|
|
22500
|
+
}
|
|
22501
|
+
}
|
|
21734
22502
|
async function startSyncClient(base2, token, tickMs2) {
|
|
21735
22503
|
started = true;
|
|
21736
22504
|
const transport = fetchTransport(base2, token);
|
|
21737
|
-
const tick6 =
|
|
21738
|
-
try {
|
|
21739
|
-
await syncOnce(transport);
|
|
21740
|
-
} catch {
|
|
21741
|
-
}
|
|
21742
|
-
};
|
|
22505
|
+
const tick6 = () => syncTick(transport);
|
|
21743
22506
|
const connectWs = async () => {
|
|
21744
22507
|
const { WebSocket: WebSocket2 } = await Promise.resolve().then(() => (init_wrapper(), wrapper_exports));
|
|
21745
22508
|
const wsUrl = base2.replace(/^http/, "ws").replace(/\/$/, "") + "/api/sync/ws";
|
|
@@ -21768,6 +22531,7 @@ async function startSyncClient(base2, token, tickMs2) {
|
|
|
21768
22531
|
}
|
|
21769
22532
|
});
|
|
21770
22533
|
};
|
|
22534
|
+
await bootstrapOnce(transport).catch(() => null);
|
|
21771
22535
|
await tick6();
|
|
21772
22536
|
void connectWs();
|
|
21773
22537
|
const ms = tickMs2 && tickMs2 > 0 ? tickMs2 : 15e3;
|
|
@@ -21794,7 +22558,7 @@ async function applySyncConfig() {
|
|
|
21794
22558
|
const token = effectiveStr("SYNC_DEVICE_TOKEN");
|
|
21795
22559
|
if (base2 && token) await startSyncClient(base2, token, effectiveInt("SYNC_TICK_MS"));
|
|
21796
22560
|
}
|
|
21797
|
-
var MAX_SYNC_RECORD_BYTES, feedHole,
|
|
22561
|
+
var MAX_SYNC_RECORD_BYTES, feedHole, MAX_DRAIN_PAGES, timer3, ws, started, pullSehat;
|
|
21798
22562
|
var init_sync_client = __esm({
|
|
21799
22563
|
"src/services/sync-client.ts"() {
|
|
21800
22564
|
"use strict";
|
|
@@ -21809,8 +22573,9 @@ var init_sync_client = __esm({
|
|
|
21809
22573
|
init_safe_outbound_request();
|
|
21810
22574
|
MAX_SYNC_RECORD_BYTES = 1024 * 1024;
|
|
21811
22575
|
feedHole = false;
|
|
21812
|
-
|
|
22576
|
+
MAX_DRAIN_PAGES = 500;
|
|
21813
22577
|
started = false;
|
|
22578
|
+
pullSehat = true;
|
|
21814
22579
|
}
|
|
21815
22580
|
});
|
|
21816
22581
|
|
|
@@ -25085,9 +25850,9 @@ import Fastify from "fastify";
|
|
|
25085
25850
|
import fastifyStatic from "@fastify/static";
|
|
25086
25851
|
import websocket from "@fastify/websocket";
|
|
25087
25852
|
import cookie from "@fastify/cookie";
|
|
25088
|
-
import { fileURLToPath as
|
|
25853
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
25089
25854
|
import { dirname as dirname13 } from "node:path";
|
|
25090
|
-
import { existsSync as
|
|
25855
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
25091
25856
|
|
|
25092
25857
|
// src/web-dir.ts
|
|
25093
25858
|
import { resolve } from "node:path";
|
|
@@ -25148,17 +25913,17 @@ init_stage_machine();
|
|
|
25148
25913
|
|
|
25149
25914
|
// src/services/scan.ts
|
|
25150
25915
|
init_src();
|
|
25151
|
-
import { execFile } from "node:child_process";
|
|
25916
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
25152
25917
|
import { promisify } from "node:util";
|
|
25153
25918
|
import { existsSync as existsSync6, rmSync as rmSync2 } from "node:fs";
|
|
25154
25919
|
import { resolve as resolve10 } from "node:path";
|
|
25155
25920
|
|
|
25156
25921
|
// src/services/safe-repo-path.ts
|
|
25157
|
-
import { constants } from "node:fs";
|
|
25922
|
+
import { constants as constants2 } from "node:fs";
|
|
25158
25923
|
import { lstat, mkdir as mkdir2, open as open2, realpath, rename, unlink as unlink2 } from "node:fs/promises";
|
|
25159
|
-
import { isAbsolute as
|
|
25924
|
+
import { isAbsolute as isAbsolute4, join as join7, relative, resolve as resolve9, sep as sep2 } from "node:path";
|
|
25160
25925
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
25161
|
-
import { closeSync, fstatSync, lstatSync, mkdirSync as
|
|
25926
|
+
import { closeSync, fstatSync, lstatSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync7, realpathSync as realpathSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
25162
25927
|
var PathContainmentError = class extends Error {
|
|
25163
25928
|
code = "PATH_CONTAINMENT";
|
|
25164
25929
|
};
|
|
@@ -25166,7 +25931,7 @@ var denied = (detail) => {
|
|
|
25166
25931
|
throw new PathContainmentError(`repository path ditolak: ${detail}`);
|
|
25167
25932
|
};
|
|
25168
25933
|
function components(rel) {
|
|
25169
|
-
if (!rel ||
|
|
25934
|
+
if (!rel || isAbsolute4(rel) || rel.includes("\0")) denied("path invalid");
|
|
25170
25935
|
const parts = rel.replace(/\\/g, "/").split("/");
|
|
25171
25936
|
if (parts.some((part) => !part || part === "." || part === "..")) denied("parent/empty component");
|
|
25172
25937
|
return parts;
|
|
@@ -25179,14 +25944,14 @@ async function canonicalRoot(root) {
|
|
|
25179
25944
|
}
|
|
25180
25945
|
function beneath(root, candidate) {
|
|
25181
25946
|
const rel = relative(root, candidate);
|
|
25182
|
-
return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !
|
|
25947
|
+
return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute4(rel);
|
|
25183
25948
|
}
|
|
25184
25949
|
async function ensureRepoParents(root, rel) {
|
|
25185
25950
|
const base2 = await canonicalRoot(root);
|
|
25186
25951
|
const parts = components(rel).slice(0, -1);
|
|
25187
25952
|
let current = base2;
|
|
25188
25953
|
for (const part of parts) {
|
|
25189
|
-
current =
|
|
25954
|
+
current = join7(current, part);
|
|
25190
25955
|
try {
|
|
25191
25956
|
await mkdir2(current, { mode: 448 });
|
|
25192
25957
|
} catch (error) {
|
|
@@ -25203,9 +25968,9 @@ function ensureRepoParentsSync(root, rel) {
|
|
|
25203
25968
|
const parts = components(rel).slice(0, -1);
|
|
25204
25969
|
let current = base2;
|
|
25205
25970
|
for (const part of parts) {
|
|
25206
|
-
current =
|
|
25971
|
+
current = join7(current, part);
|
|
25207
25972
|
try {
|
|
25208
|
-
|
|
25973
|
+
mkdirSync6(current, { mode: 448 });
|
|
25209
25974
|
} catch (error) {
|
|
25210
25975
|
if (error.code !== "EEXIST") throw error;
|
|
25211
25976
|
}
|
|
@@ -25219,7 +25984,7 @@ async function resolveRepoEntry(root, rel, opts = {}) {
|
|
|
25219
25984
|
const parts = components(rel);
|
|
25220
25985
|
let current = base2;
|
|
25221
25986
|
for (let i = 0; i < parts.length; i++) {
|
|
25222
|
-
current =
|
|
25987
|
+
current = join7(current, parts[i]);
|
|
25223
25988
|
const stat4 = await lstat(current).catch((error) => {
|
|
25224
25989
|
if (error.code === "ENOENT" && opts.allowMissingTail) return null;
|
|
25225
25990
|
if (error.code === "ENOENT" && opts.allowMissingFinal && i === parts.length - 1) return null;
|
|
@@ -25240,7 +26005,7 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
|
|
|
25240
26005
|
const parts = components(rel);
|
|
25241
26006
|
let current = base2;
|
|
25242
26007
|
for (let i = 0; i < parts.length; i++) {
|
|
25243
|
-
current =
|
|
26008
|
+
current = join7(current, parts[i]);
|
|
25244
26009
|
try {
|
|
25245
26010
|
const stat4 = lstatSync(current);
|
|
25246
26011
|
if (stat4.isSymbolicLink()) denied("symlink");
|
|
@@ -25256,10 +26021,10 @@ function assertSafeRepoPathSync(root, rel, allowMissingFinal = false, allowMissi
|
|
|
25256
26021
|
}
|
|
25257
26022
|
function readRepoFileSync(root, rel) {
|
|
25258
26023
|
const path2 = assertSafeRepoPathSync(root, rel);
|
|
25259
|
-
const fd = openSync(path2,
|
|
26024
|
+
const fd = openSync(path2, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
25260
26025
|
try {
|
|
25261
26026
|
if (!fstatSync(fd).isFile()) denied("bukan file regular");
|
|
25262
|
-
return
|
|
26027
|
+
return readFileSync7(fd);
|
|
25263
26028
|
} finally {
|
|
25264
26029
|
closeSync(fd);
|
|
25265
26030
|
}
|
|
@@ -25270,10 +26035,10 @@ function writeRepoFileAtomicSync(root, rel, data) {
|
|
|
25270
26035
|
const parent = resolve9(path2, "..");
|
|
25271
26036
|
const parentRel = relative(realpathSync2(root), parent);
|
|
25272
26037
|
if (parentRel) assertSafeRepoPathSync(root, parentRel);
|
|
25273
|
-
const temp =
|
|
25274
|
-
const fd = openSync(temp,
|
|
26038
|
+
const temp = join7(parent, `.hanoman-${randomUUID3()}.tmp`);
|
|
26039
|
+
const fd = openSync(temp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
|
|
25275
26040
|
try {
|
|
25276
|
-
|
|
26041
|
+
writeFileSync4(fd, data);
|
|
25277
26042
|
} finally {
|
|
25278
26043
|
closeSync(fd);
|
|
25279
26044
|
}
|
|
@@ -25297,7 +26062,7 @@ function writeRepoFileAtomicSync(root, rel, data) {
|
|
|
25297
26062
|
}
|
|
25298
26063
|
async function readRepoFile(root, rel) {
|
|
25299
26064
|
const entry = await resolveRepoEntry(root, rel);
|
|
25300
|
-
const handle = await open2(entry.path,
|
|
26065
|
+
const handle = await open2(entry.path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0)).catch(() => denied("final component tidak aman"));
|
|
25301
26066
|
try {
|
|
25302
26067
|
const stat4 = await handle.stat();
|
|
25303
26068
|
if (!stat4.isFile()) denied("bukan file regular");
|
|
@@ -25317,8 +26082,8 @@ async function writeRepoFileAtomic(root, rel, data) {
|
|
|
25317
26082
|
await mkdir2(entry.parent, { recursive: false, mode: 448 }).catch((error) => {
|
|
25318
26083
|
if (error.code !== "EEXIST") throw error;
|
|
25319
26084
|
});
|
|
25320
|
-
const temp =
|
|
25321
|
-
const handle = await open2(temp,
|
|
26085
|
+
const temp = join7(entry.parent, `.hanoman-${randomUUID3()}.tmp`);
|
|
26086
|
+
const handle = await open2(temp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
|
|
25322
26087
|
try {
|
|
25323
26088
|
await handle.writeFile(data);
|
|
25324
26089
|
await handle.sync();
|
|
@@ -25339,7 +26104,7 @@ async function writeRepoFileAtomic(root, rel, data) {
|
|
|
25339
26104
|
}
|
|
25340
26105
|
|
|
25341
26106
|
// src/services/scan.ts
|
|
25342
|
-
var exec = promisify(
|
|
26107
|
+
var exec = promisify(execFile2);
|
|
25343
26108
|
async function listRepoDocs(repoDir) {
|
|
25344
26109
|
try {
|
|
25345
26110
|
const { stdout } = await exec(
|
|
@@ -25459,7 +26224,18 @@ function sessionOf(projectId, sessions) {
|
|
|
25459
26224
|
commit: `\u2192 hanoman/${s2.id}`
|
|
25460
26225
|
};
|
|
25461
26226
|
}
|
|
25462
|
-
async function
|
|
26227
|
+
async function loadDeviceIndex() {
|
|
26228
|
+
const rows = await prisma.deviceToken.findMany({ select: { id: true, name: true, revokedAt: true } });
|
|
26229
|
+
return new Map(rows.map((d) => [d.id, { name: d.name, revoked: d.revokedAt !== null }]));
|
|
26230
|
+
}
|
|
26231
|
+
function handledByView(raw2, devices) {
|
|
26232
|
+
return handledByOf(raw2).map((e) => {
|
|
26233
|
+
const d = devices.get(e.deviceId);
|
|
26234
|
+
return { deviceId: e.deviceId, name: d?.name ?? e.name, revoked: d?.revoked ?? false };
|
|
26235
|
+
});
|
|
26236
|
+
}
|
|
26237
|
+
async function toProjectView(p, sessions, devices) {
|
|
26238
|
+
const deviceIndex = devices ?? await loadDeviceIndex();
|
|
25463
26239
|
const specs = await prisma.spec.findMany({ where: { projectId: p.id } });
|
|
25464
26240
|
const { coverage } = await scanRepoDocs(await resolveRepoDir(p.id));
|
|
25465
26241
|
const binding = await getBinding(p.id);
|
|
@@ -25489,6 +26265,9 @@ async function toProjectView(p, sessions) {
|
|
|
25489
26265
|
schedulerOptIn: p.schedulerOptIn,
|
|
25490
26266
|
// SPEC-409 · ADR-0091 · opt-in hanoman-lead (lokal per-instance, cermin schedulerOptIn).
|
|
25491
26267
|
leadOptIn: p.leadOptIn,
|
|
26268
|
+
// SPEC-880 · ADR-0135 · penanda "ditangani oleh" (DISYNC — beda dari repoDir/binding di atas
|
|
26269
|
+
// yang fakta mesin ini saja).
|
|
26270
|
+
handledBy: handledByView(p.handledBy, deviceIndex),
|
|
25492
26271
|
// SPEC-486 · ADR-0103 · kebijakan auto-merge (lokal per-instance, cermin schedulerOptIn).
|
|
25493
26272
|
autoMerge: autoMergeOf(p.autoMerge)
|
|
25494
26273
|
};
|
|
@@ -25539,9 +26318,9 @@ async function listPendingDeletes() {
|
|
|
25539
26318
|
}
|
|
25540
26319
|
|
|
25541
26320
|
// src/services/branches.ts
|
|
25542
|
-
import { execFile as
|
|
26321
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
25543
26322
|
import { promisify as promisify2 } from "node:util";
|
|
25544
|
-
var exec2 = promisify2(
|
|
26323
|
+
var exec2 = promisify2(execFile3);
|
|
25545
26324
|
var GIT = { maxBuffer: 1 << 24, encoding: "utf8" };
|
|
25546
26325
|
async function refs(repoDir, glob) {
|
|
25547
26326
|
if (!repoDir) return [];
|
|
@@ -25596,6 +26375,17 @@ async function checkAutoMerge(repoDir, raw2) {
|
|
|
25596
26375
|
return known.includes(p.branch) ? { ok: true } : { ok: false, code: 400, error: `branch "${p.branch}" tidak ada di ${p.dest === "origin" ? "origin" : "repo lokal"} project` };
|
|
25597
26376
|
}
|
|
25598
26377
|
|
|
26378
|
+
// src/services/handled-by.ts
|
|
26379
|
+
init_db();
|
|
26380
|
+
async function checkHandledBy(list2) {
|
|
26381
|
+
const known = await prisma.deviceToken.findMany({ select: { id: true } });
|
|
26382
|
+
if (!known.length) return { ok: true };
|
|
26383
|
+
const ids = new Set(known.map((d) => d.id));
|
|
26384
|
+
const asing = list2.filter((e) => !ids.has(e.deviceId)).map((e) => e.deviceId);
|
|
26385
|
+
if (asing.length) return { ok: false, code: 400, error: `device tak dikenal: ${asing.join(", ")}` };
|
|
26386
|
+
return { ok: true };
|
|
26387
|
+
}
|
|
26388
|
+
|
|
25599
26389
|
// src/routes/projects.ts
|
|
25600
26390
|
import { Prisma } from "@prisma/client";
|
|
25601
26391
|
init_pty();
|
|
@@ -25613,13 +26403,16 @@ function paginate(items, page, limit) {
|
|
|
25613
26403
|
init_src2();
|
|
25614
26404
|
async function projects_default(app2) {
|
|
25615
26405
|
app2.get("/projects", async (req) => {
|
|
25616
|
-
const { q, page, limit } = req.query;
|
|
26406
|
+
const { q, handledBy, page, limit } = req.query;
|
|
25617
26407
|
const ps = await prisma.project.findMany({ orderBy: { createdAt: "desc" } });
|
|
25618
26408
|
const sessions = listSessions();
|
|
25619
|
-
const
|
|
26409
|
+
const devices = await loadDeviceIndex();
|
|
26410
|
+
const views = await Promise.all(ps.map((p) => toProjectView(p, sessions, devices)));
|
|
25620
26411
|
const needle = (q ?? "").trim().toLowerCase();
|
|
25621
26412
|
const filtered = needle ? views.filter((v) => `${v.name} ${v.desc} ${v.stack}`.toLowerCase().includes(needle)) : views;
|
|
25622
|
-
|
|
26413
|
+
const device = (handledBy ?? "").trim();
|
|
26414
|
+
const scoped = device ? filtered.filter((v) => v.handledBy.some((h) => h.deviceId === device)) : filtered;
|
|
26415
|
+
return paginate(scoped, page, limit);
|
|
25623
26416
|
});
|
|
25624
26417
|
app2.get("/projects/:id", async (req, reply) => {
|
|
25625
26418
|
const { id } = req.params;
|
|
@@ -25634,6 +26427,10 @@ async function projects_default(app2) {
|
|
|
25634
26427
|
const id = (b.name || b.repoDir?.split("/").pop() || "repo").trim().toLowerCase().replace(/\s+/g, "-");
|
|
25635
26428
|
if (await prisma.project.findUnique({ where: { id } }))
|
|
25636
26429
|
return reply.code(409).send({ error: `project "${id}" sudah ada` });
|
|
26430
|
+
if (b.handledBy?.length) {
|
|
26431
|
+
const gate = await checkHandledBy(b.handledBy);
|
|
26432
|
+
if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
|
|
26433
|
+
}
|
|
25637
26434
|
if (b.kind === "from-scratch" && b.repoDir) {
|
|
25638
26435
|
try {
|
|
25639
26436
|
realGit.initRepo(b.repoDir);
|
|
@@ -25649,7 +26446,8 @@ async function projects_default(app2) {
|
|
|
25649
26446
|
kind: b.kind,
|
|
25650
26447
|
repoDir: b.repoDir ?? null,
|
|
25651
26448
|
gitRemote: b.gitRemote ?? null,
|
|
25652
|
-
stack: ""
|
|
26449
|
+
stack: "",
|
|
26450
|
+
...b.handledBy?.length ? { handledBy: b.handledBy } : {}
|
|
25653
26451
|
}
|
|
25654
26452
|
});
|
|
25655
26453
|
await notifySynced("project", created.id);
|
|
@@ -25664,8 +26462,13 @@ async function projects_default(app2) {
|
|
|
25664
26462
|
const gate = await checkAutoMerge(await resolveRepoDir(id), parsed.data.autoMerge);
|
|
25665
26463
|
if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
|
|
25666
26464
|
}
|
|
26465
|
+
if (parsed.data.handledBy?.length) {
|
|
26466
|
+
const gate = await checkHandledBy(parsed.data.handledBy);
|
|
26467
|
+
if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
|
|
26468
|
+
}
|
|
25667
26469
|
const data = { ...parsed.data };
|
|
25668
26470
|
if ("autoMerge" in data && data.autoMerge === null) data.autoMerge = Prisma.DbNull;
|
|
26471
|
+
if ("handledBy" in data && !data.handledBy?.length) data.handledBy = Prisma.DbNull;
|
|
25669
26472
|
const updated = await prisma.project.update({ where: { id }, data });
|
|
25670
26473
|
await notifySynced("project", id);
|
|
25671
26474
|
return toProjectView(updated, listSessions());
|
|
@@ -25736,13 +26539,13 @@ init_src2();
|
|
|
25736
26539
|
import { existsSync as existsSync7 } from "node:fs";
|
|
25737
26540
|
|
|
25738
26541
|
// src/services/integrate.ts
|
|
25739
|
-
import { execFile as
|
|
26542
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
25740
26543
|
import { promisify as promisify3 } from "node:util";
|
|
25741
26544
|
import { rmSync as rmSync3 } from "node:fs";
|
|
25742
|
-
import { join as
|
|
26545
|
+
import { join as join8 } from "node:path";
|
|
25743
26546
|
var sanitize = (id) => id.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
25744
26547
|
var sourceBranch = (specId) => `hanoman/${sanitize(specId)}`;
|
|
25745
|
-
var exec3 = promisify3(
|
|
26548
|
+
var exec3 = promisify3(execFile4);
|
|
25746
26549
|
var GIT2 = { timeout: 6e4, maxBuffer: 1 << 24, encoding: "utf8" };
|
|
25747
26550
|
var sh = (cwd, args) => exec3("git", args, { cwd, ...GIT2 }).then(
|
|
25748
26551
|
(r) => ({ status: 0, stdout: r.stdout, stderr: r.stderr }),
|
|
@@ -25783,7 +26586,7 @@ async function integrateBranch(repoDir, src, op, target2) {
|
|
|
25783
26586
|
const tgt = await resolveTarget(repoDir, target2);
|
|
25784
26587
|
if (!tgt) return { status: "error", code: 400, error: `target "${target2}" tidak dikenal` };
|
|
25785
26588
|
await sh(repoDir, ["fetch", "origin"]);
|
|
25786
|
-
const wt =
|
|
26589
|
+
const wt = join8(repoDir, ".worktrees", `merge-${sanitize(src.mergeId)}`);
|
|
25787
26590
|
await reclaim(repoDir, wt);
|
|
25788
26591
|
const baseRef = op === "merge" ? tgt.ref : source;
|
|
25789
26592
|
const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`]);
|
|
@@ -25791,9 +26594,9 @@ async function integrateBranch(repoDir, src, op, target2) {
|
|
|
25791
26594
|
return { status: "error", code: 500, error: "gagal membuat worktree integrasi" };
|
|
25792
26595
|
const applyRef = op === "merge" ? source : tgt.ref;
|
|
25793
26596
|
const cmd = op === "merge" ? ["merge", "--no-edit", applyRef] : ["rebase", applyRef];
|
|
25794
|
-
const
|
|
26597
|
+
const run3 = await sh(wt, cmd);
|
|
25795
26598
|
const finalize = op === "rebase" ? { kind: "force-push", branch: src.branch } : tgt.dest === "local" ? { kind: "branch-f", branch: tgt.name, checkout: await worktreeForBranch(repoDir, tgt.name) } : { kind: "push", branch: tgt.name };
|
|
25796
|
-
if (
|
|
26599
|
+
if (run3.status === 0) {
|
|
25797
26600
|
const fin = await runFinalize(wt, repoDir, finalize);
|
|
25798
26601
|
await sh(repoDir, ["worktree", "remove", "--force", wt]);
|
|
25799
26602
|
return fin.ok ? { status: "clean", detail: fin.detail } : { status: "error", code: 409, error: fin.error };
|
|
@@ -25844,16 +26647,16 @@ async function mergeIntoCurrent(repoDir, source, opts = {}) {
|
|
|
25844
26647
|
const src = await resolveGraphSource(repoDir, source);
|
|
25845
26648
|
if (!src) return { status: "error", code: 400, error: `source "${source}" tak dikenal` };
|
|
25846
26649
|
await sh(repoDir, ["fetch", "origin"]);
|
|
25847
|
-
const wt =
|
|
26650
|
+
const wt = join8(repoDir, ".worktrees", `merge-${sanitize(current)}`);
|
|
25848
26651
|
await reclaim(repoDir, wt);
|
|
25849
26652
|
const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
|
|
25850
26653
|
if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
|
|
25851
26654
|
return { status: "error", code: 500, error: "gagal membuat worktree merge" };
|
|
25852
26655
|
const srcSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `${src}^{commit}`]);
|
|
25853
26656
|
const cmd = ["merge", "--no-edit", ...opts.ff ? [`--${opts.ff}`] : [], "--end-of-options", srcSha];
|
|
25854
|
-
const
|
|
26657
|
+
const run3 = await sh(wt, cmd);
|
|
25855
26658
|
const finalize = { kind: "branch-f", branch: current, checkout: await worktreeForBranch(repoDir, current) };
|
|
25856
|
-
if (
|
|
26659
|
+
if (run3.status === 0) {
|
|
25857
26660
|
const fin = await runFinalize(wt, repoDir, finalize);
|
|
25858
26661
|
if (fin.ok && opts.deleteBranch) await deleteMergedBranch(repoDir, opts.deleteBranch);
|
|
25859
26662
|
await sh(repoDir, ["worktree", "remove", "--force", wt]);
|
|
@@ -25863,7 +26666,7 @@ async function mergeIntoCurrent(repoDir, source, opts = {}) {
|
|
|
25863
26666
|
if (!conflicted) {
|
|
25864
26667
|
await sh(wt, ["merge", "--abort"]);
|
|
25865
26668
|
await sh(repoDir, ["worktree", "remove", "--force", wt]);
|
|
25866
|
-
return { status: "error", code: 409, error:
|
|
26669
|
+
return { status: "error", code: 409, error: run3.stderr.trim() || "merge gagal \u2014 tak bisa fast-forward?" };
|
|
25867
26670
|
}
|
|
25868
26671
|
return { status: "conflict", worktree: wt, source: src, target: `local:${current}`, finalize: finalizeInstruction("merge", finalize) };
|
|
25869
26672
|
}
|
|
@@ -25882,13 +26685,13 @@ async function replayCurrent(repoDir, source, cmd) {
|
|
|
25882
26685
|
const current = await out(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
25883
26686
|
if (!current || current === "HEAD")
|
|
25884
26687
|
return { status: "error", code: 409, error: "HEAD detached \u2014 checkout sebuah branch dulu" };
|
|
25885
|
-
const wt =
|
|
26688
|
+
const wt = join8(repoDir, ".worktrees", `merge-${sanitize(current)}`);
|
|
25886
26689
|
await reclaim(repoDir, wt);
|
|
25887
26690
|
const baseSha = await out(repoDir, ["rev-parse", "--verify", "--end-of-options", `refs/heads/${current}^{commit}`]);
|
|
25888
26691
|
if (!await ok(repoDir, ["worktree", "add", "--detach", "-q", wt, baseSha]))
|
|
25889
26692
|
return { status: "error", code: 500, error: "gagal membuat worktree rebase" };
|
|
25890
|
-
const
|
|
25891
|
-
if (
|
|
26693
|
+
const run3 = await sh(wt, cmd);
|
|
26694
|
+
if (run3.status === 0) {
|
|
25892
26695
|
const head = await out(wt, ["rev-parse", "HEAD"]);
|
|
25893
26696
|
const fin = await landRewrite(repoDir, current, head);
|
|
25894
26697
|
await sh(repoDir, ["worktree", "remove", "--force", wt]);
|
|
@@ -25898,7 +26701,7 @@ async function replayCurrent(repoDir, source, cmd) {
|
|
|
25898
26701
|
if (!conflicted) {
|
|
25899
26702
|
await sh(wt, ["rebase", "--abort"]);
|
|
25900
26703
|
await sh(repoDir, ["worktree", "remove", "--force", wt]);
|
|
25901
|
-
return { status: "error", code: 409, error:
|
|
26704
|
+
return { status: "error", code: 409, error: run3.stderr.trim() || "operasi rebase gagal" };
|
|
25902
26705
|
}
|
|
25903
26706
|
return { status: "conflict", worktree: wt, source, target: `local:${current}`, finalize: rewriteInstruction(repoDir, current) };
|
|
25904
26707
|
}
|
|
@@ -25927,19 +26730,19 @@ init_codex_trust();
|
|
|
25927
26730
|
init_db();
|
|
25928
26731
|
|
|
25929
26732
|
// src/services/spec-review.ts
|
|
25930
|
-
import { execFile as
|
|
26733
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
25931
26734
|
import { promisify as promisify4 } from "node:util";
|
|
25932
26735
|
import { mkdtemp, copyFile, rm as rm2 } from "node:fs/promises";
|
|
25933
26736
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
25934
|
-
import { join as
|
|
25935
|
-
var exec4 = promisify4(
|
|
26737
|
+
import { join as join9, resolve as resolve11 } from "node:path";
|
|
26738
|
+
var exec4 = promisify4(execFile5);
|
|
25936
26739
|
var GIT3 = { maxBuffer: 1 << 24 };
|
|
25937
26740
|
var MAX = 256 * 1024;
|
|
25938
|
-
var worktreeDir = (repoDir, specId) =>
|
|
26741
|
+
var worktreeDir = (repoDir, specId) => join9(repoDir, ".worktrees", specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_"));
|
|
25939
26742
|
async function withTempIndex(wt, fn2) {
|
|
25940
26743
|
const idx = (await exec4("git", ["rev-parse", "--git-path", "index"], { cwd: wt, ...GIT3 })).stdout.trim();
|
|
25941
|
-
const dir2 = await mkdtemp(
|
|
25942
|
-
const tmp =
|
|
26744
|
+
const dir2 = await mkdtemp(join9(tmpdir2(), "hanoman-idx-"));
|
|
26745
|
+
const tmp = join9(dir2, "index");
|
|
25943
26746
|
await copyFile(resolve11(wt, idx), tmp);
|
|
25944
26747
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
25945
26748
|
try {
|
|
@@ -26210,7 +27013,7 @@ import { extname } from "node:path";
|
|
|
26210
27013
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
26211
27014
|
import { spawn as spawn2 } from "node:child_process";
|
|
26212
27015
|
import { mkdir as mkdir3, rename as rename2, unlink as unlink3, writeFile as writeFile2 } from "node:fs/promises";
|
|
26213
|
-
import { basename as basename3, isAbsolute as
|
|
27016
|
+
import { basename as basename3, isAbsolute as isAbsolute5, join as join10 } from "node:path";
|
|
26214
27017
|
|
|
26215
27018
|
// ../node_modules/.pnpm/strtok3@10.3.5/node_modules/strtok3/lib/stream/Errors.js
|
|
26216
27019
|
var defaultMessages = "End-Of-Stream";
|
|
@@ -33572,6 +34375,7 @@ utility_default(constructor_default);
|
|
|
33572
34375
|
var dist_default = constructor_default;
|
|
33573
34376
|
|
|
33574
34377
|
// src/services/upload-pipeline.ts
|
|
34378
|
+
init_src2();
|
|
33575
34379
|
init_db();
|
|
33576
34380
|
init_uploads();
|
|
33577
34381
|
var UPLOAD_LIMITS = {
|
|
@@ -33617,10 +34421,12 @@ async function defaultUsage(projectId) {
|
|
|
33617
34421
|
function scannerFromEnv(path2) {
|
|
33618
34422
|
const command = process.env.HANOMAN_UPLOAD_SCANNER?.trim();
|
|
33619
34423
|
if (!command) {
|
|
33620
|
-
if (process.env
|
|
34424
|
+
if (resolveHardening(process.env))
|
|
34425
|
+
return Promise.reject(new UploadError("UPLOAD_SCAN", "scanner required"));
|
|
34426
|
+
console.warn("upload: HANOMAN_UPLOAD_SCANNER tak disetel \u2014 lampiran diterima tanpa dipindai");
|
|
33621
34427
|
return Promise.resolve();
|
|
33622
34428
|
}
|
|
33623
|
-
if (!
|
|
34429
|
+
if (!isAbsolute5(command)) return Promise.reject(new UploadError("UPLOAD_SCAN", "scanner path must be absolute"));
|
|
33624
34430
|
return new Promise((resolve21, reject2) => {
|
|
33625
34431
|
const child = spawn2(command, [path2], { shell: false, stdio: "ignore" });
|
|
33626
34432
|
const timer9 = setTimeout(() => {
|
|
@@ -33644,16 +34450,16 @@ function safeFilename(input, extension2) {
|
|
|
33644
34450
|
}
|
|
33645
34451
|
async function commitToStorage(buffer2, extension2, deps, beforePromote) {
|
|
33646
34452
|
const storageDir = deps.storageDir ?? uploadDir();
|
|
33647
|
-
const quarantineDir =
|
|
34453
|
+
const quarantineDir = join10(storageDir, ".quarantine");
|
|
33648
34454
|
await mkdir3(quarantineDir, { recursive: true, mode: 448 });
|
|
33649
34455
|
await mkdir3(storageDir, { recursive: true, mode: 448 });
|
|
33650
|
-
const quarantine =
|
|
34456
|
+
const quarantine = join10(quarantineDir, `${randomUUID5()}.upload`);
|
|
33651
34457
|
const storageKey = `${randomUUID5()}${extension2}`;
|
|
33652
34458
|
await writeFile2(quarantine, buffer2, { mode: 384, flag: "wx" });
|
|
33653
34459
|
try {
|
|
33654
34460
|
await timeout2((deps.scanner ?? scannerFromEnv)(quarantine), UPLOAD_LIMITS.scanMs, "UPLOAD_SCAN");
|
|
33655
34461
|
await beforePromote?.();
|
|
33656
|
-
await rename2(quarantine,
|
|
34462
|
+
await rename2(quarantine, join10(storageDir, storageKey));
|
|
33657
34463
|
} catch (error) {
|
|
33658
34464
|
await unlink3(quarantine).catch(() => {
|
|
33659
34465
|
});
|
|
@@ -33854,10 +34660,10 @@ async function dropSpecAttachments(specId) {
|
|
|
33854
34660
|
// src/services/spec-attachment-dir.ts
|
|
33855
34661
|
init_db();
|
|
33856
34662
|
import { mkdir as mkdir4, readdir, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
|
|
33857
|
-
import { join as
|
|
34663
|
+
import { join as join11 } from "node:path";
|
|
33858
34664
|
init_session_id();
|
|
33859
34665
|
init_uploads();
|
|
33860
|
-
var specAttachmentsDir = (repoDir, sessionId2) =>
|
|
34666
|
+
var specAttachmentsDir = (repoDir, sessionId2) => join11(repoDir, ".worktrees", ".attachments", sessionId2);
|
|
33861
34667
|
var INDEX = "INDEX.md";
|
|
33862
34668
|
var humanSize = (n2) => n2 >= 1024 * 1024 ? `${(n2 / 1024 / 1024).toFixed(1)} MB` : n2 >= 1024 ? `${Math.round(n2 / 1024)} KB` : `${n2} B`;
|
|
33863
34669
|
function uniqueName(taken, filename) {
|
|
@@ -33907,19 +34713,19 @@ async function syncSpecAttachmentsDir(specId, projectId) {
|
|
|
33907
34713
|
for (const a of rows) {
|
|
33908
34714
|
let bytes;
|
|
33909
34715
|
try {
|
|
33910
|
-
bytes = await readFile3(
|
|
34716
|
+
bytes = await readFile3(join11(uploadDir(), a.storageKey));
|
|
33911
34717
|
} catch {
|
|
33912
34718
|
continue;
|
|
33913
34719
|
}
|
|
33914
34720
|
const filename = uniqueName(taken, a.filename);
|
|
33915
|
-
const path2 =
|
|
34721
|
+
const path2 = join11(dir2, filename);
|
|
33916
34722
|
await writeFile3(path2, bytes, { mode: 384 });
|
|
33917
34723
|
items.push({ filename, mimeType: a.mimeType, size: a.size, path: path2 });
|
|
33918
34724
|
}
|
|
33919
|
-
await writeFile3(
|
|
34725
|
+
await writeFile3(join11(dir2, INDEX), renderIndex(specId, items), { mode: 384 });
|
|
33920
34726
|
const keep = /* @__PURE__ */ new Set([INDEX, ...items.map((a) => a.filename)]);
|
|
33921
34727
|
for (const name2 of await readdir(dir2)) {
|
|
33922
|
-
if (!keep.has(name2)) await rm3(
|
|
34728
|
+
if (!keep.has(name2)) await rm3(join11(dir2, name2), { recursive: true, force: true }).catch(() => {
|
|
33923
34729
|
});
|
|
33924
34730
|
}
|
|
33925
34731
|
return items;
|
|
@@ -37235,14 +38041,14 @@ async function docs_default(app2) {
|
|
|
37235
38041
|
// src/routes/ide.ts
|
|
37236
38042
|
init_src2();
|
|
37237
38043
|
import { basename as basename5, resolve as resolve16 } from "node:path";
|
|
37238
|
-
import { execFile as
|
|
38044
|
+
import { execFile as execFile11, spawn as spawn3 } from "node:child_process";
|
|
37239
38045
|
import { promisify as promisify10 } from "node:util";
|
|
37240
38046
|
|
|
37241
38047
|
// src/services/git-remotes.ts
|
|
37242
|
-
import { execFile as
|
|
38048
|
+
import { execFile as execFile6 } from "node:child_process";
|
|
37243
38049
|
import { promisify as promisify5 } from "node:util";
|
|
37244
38050
|
import { existsSync as existsSync8 } from "node:fs";
|
|
37245
|
-
var exec5 = promisify5(
|
|
38051
|
+
var exec5 = promisify5(execFile6);
|
|
37246
38052
|
var GIT4 = { maxBuffer: 1 << 24, encoding: "utf8" };
|
|
37247
38053
|
function parseRemote(url2) {
|
|
37248
38054
|
let m = /^git@([^:]+):(.+?)(?:\.git)?$/.exec(url2);
|
|
@@ -37295,15 +38101,15 @@ init_settings2();
|
|
|
37295
38101
|
init_codex_trust();
|
|
37296
38102
|
|
|
37297
38103
|
// src/services/branch-cleanup.ts
|
|
37298
|
-
import { execFile as
|
|
38104
|
+
import { execFile as execFile8 } from "node:child_process";
|
|
37299
38105
|
import { promisify as promisify7 } from "node:util";
|
|
37300
38106
|
|
|
37301
38107
|
// src/services/git-ide.ts
|
|
37302
|
-
import { execFile as
|
|
38108
|
+
import { execFile as execFile7 } from "node:child_process";
|
|
37303
38109
|
import { promisify as promisify6 } from "node:util";
|
|
37304
38110
|
import { existsSync as existsSync9 } from "node:fs";
|
|
37305
38111
|
import { resolve as resolve12 } from "node:path";
|
|
37306
|
-
var exec6 = promisify6(
|
|
38112
|
+
var exec6 = promisify6(execFile7);
|
|
37307
38113
|
var GIT5 = { maxBuffer: 1 << 24 };
|
|
37308
38114
|
var MAX2 = 256 * 1024;
|
|
37309
38115
|
var splitZ2 = (s2) => s2.split("\0").filter(Boolean);
|
|
@@ -37467,16 +38273,16 @@ async function compareCommits(repoDir, from, to) {
|
|
|
37467
38273
|
}
|
|
37468
38274
|
async function searchCommits(repoDir, q, by = "all") {
|
|
37469
38275
|
if (!repoDir || !existsSync9(repoDir) || !q) return [];
|
|
37470
|
-
const
|
|
38276
|
+
const run3 = async (extra) => {
|
|
37471
38277
|
try {
|
|
37472
38278
|
return (await exec6("git", ["log", "--all", "--format=%H", ...extra], { cwd: repoDir, ...GIT5 })).stdout.split("\n").filter(Boolean);
|
|
37473
38279
|
} catch {
|
|
37474
38280
|
return [];
|
|
37475
38281
|
}
|
|
37476
38282
|
};
|
|
37477
|
-
const byMessage = () =>
|
|
37478
|
-
const byAuthor = () =>
|
|
37479
|
-
const byHash = async () => (await
|
|
38283
|
+
const byMessage = () => run3(["-i", "-F", `--grep=${q}`]);
|
|
38284
|
+
const byAuthor = () => run3(["-i", `--author=${q}`]);
|
|
38285
|
+
const byHash = async () => (await run3([])).filter((h2) => h2.toLowerCase().startsWith(q.toLowerCase()));
|
|
37480
38286
|
if (by === "message") return byMessage();
|
|
37481
38287
|
if (by === "author") return byAuthor();
|
|
37482
38288
|
if (by === "hash") return byHash();
|
|
@@ -37748,7 +38554,7 @@ async function workingFileDiff(repoDir, path2, staged) {
|
|
|
37748
38554
|
}
|
|
37749
38555
|
|
|
37750
38556
|
// src/services/branch-cleanup.ts
|
|
37751
|
-
var exec7 = promisify7(
|
|
38557
|
+
var exec7 = promisify7(execFile8);
|
|
37752
38558
|
var GIT6 = { timeout: 6e4, maxBuffer: 1 << 24, encoding: "utf8" };
|
|
37753
38559
|
var LOCK_REASON = {
|
|
37754
38560
|
current: "branch aktif (HEAD)",
|
|
@@ -37916,10 +38722,10 @@ async function deleteBranches(repoDir, names, opts) {
|
|
|
37916
38722
|
}
|
|
37917
38723
|
|
|
37918
38724
|
// src/services/worktree-list.ts
|
|
37919
|
-
import { execFile as
|
|
38725
|
+
import { execFile as execFile9 } from "node:child_process";
|
|
37920
38726
|
import { realpathSync as realpathSync4 } from "node:fs";
|
|
37921
38727
|
import { stat } from "node:fs/promises";
|
|
37922
|
-
import { basename as basename4, join as
|
|
38728
|
+
import { basename as basename4, join as join12, resolve as resolve14, sep as sep4 } from "node:path";
|
|
37923
38729
|
import { promisify as promisify8 } from "node:util";
|
|
37924
38730
|
|
|
37925
38731
|
// src/services/session-worktree.ts
|
|
@@ -37932,7 +38738,7 @@ function ownsWorktree(repoDir, cwd) {
|
|
|
37932
38738
|
}
|
|
37933
38739
|
|
|
37934
38740
|
// src/services/worktree-list.ts
|
|
37935
|
-
var exec8 = promisify8(
|
|
38741
|
+
var exec8 = promisify8(execFile9);
|
|
37936
38742
|
var GIT7 = { timeout: 6e4, maxBuffer: 1 << 24, encoding: "utf8" };
|
|
37937
38743
|
async function out3(cwd, args) {
|
|
37938
38744
|
try {
|
|
@@ -37975,7 +38781,7 @@ var real = (p) => {
|
|
|
37975
38781
|
};
|
|
37976
38782
|
async function bornAt(path2) {
|
|
37977
38783
|
try {
|
|
37978
|
-
const st = await stat(
|
|
38784
|
+
const st = await stat(join12(path2, ".git"));
|
|
37979
38785
|
const ms = st.birthtimeMs > 0 ? st.birthtimeMs : st.mtimeMs;
|
|
37980
38786
|
return new Date(ms).toISOString();
|
|
37981
38787
|
} catch {
|
|
@@ -38095,14 +38901,14 @@ init_stage_machine();
|
|
|
38095
38901
|
// src/services/worktree-reaper.ts
|
|
38096
38902
|
init_src2();
|
|
38097
38903
|
init_db();
|
|
38098
|
-
import { execFile as
|
|
38904
|
+
import { execFile as execFile10 } from "node:child_process";
|
|
38099
38905
|
import { readdir as readdir2, rm as rm4 } from "node:fs/promises";
|
|
38100
38906
|
import { promisify as promisify9 } from "node:util";
|
|
38101
|
-
import { join as
|
|
38907
|
+
import { join as join13, resolve as resolve15 } from "node:path";
|
|
38102
38908
|
init_notifications();
|
|
38103
38909
|
var TICK_MS = 6e4;
|
|
38104
38910
|
var trashDirOf = (repoDir) => resolve15(repoDir, ".worktrees", ".trash");
|
|
38105
|
-
var exec9 = promisify9(
|
|
38911
|
+
var exec9 = promisify9(execFile10);
|
|
38106
38912
|
var prodReaperDeps = {
|
|
38107
38913
|
rm: (path2) => rm4(path2, { recursive: true, force: true }),
|
|
38108
38914
|
prune: async (repoDir) => {
|
|
@@ -38158,7 +38964,7 @@ async function sweepRepo(repoDir, projectId, deps = prodReaperDeps) {
|
|
|
38158
38964
|
}
|
|
38159
38965
|
let removed = 0;
|
|
38160
38966
|
for (const entry of entries) {
|
|
38161
|
-
const path2 =
|
|
38967
|
+
const path2 = join13(dir2, entry);
|
|
38162
38968
|
const known = pending.get(path2);
|
|
38163
38969
|
const row = known ?? { path: path2, repoDir, projectId, entry, sessionId: sessionIdOf(entry), since: Date.now() };
|
|
38164
38970
|
pending.set(path2, row);
|
|
@@ -38248,7 +39054,7 @@ init_session_id();
|
|
|
38248
39054
|
// src/services/repo-fs.ts
|
|
38249
39055
|
import { lstat as lstat2, rename as rename3, rm as rm5 } from "node:fs/promises";
|
|
38250
39056
|
import { createWriteStream } from "node:fs";
|
|
38251
|
-
import { join as
|
|
39057
|
+
import { join as join14 } from "node:path";
|
|
38252
39058
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
38253
39059
|
import { pipeline } from "node:stream/promises";
|
|
38254
39060
|
var EntryExistsError = class extends Error {
|
|
@@ -38309,7 +39115,7 @@ async function saveUpload(repoDir, rel, source, opts = {}) {
|
|
|
38309
39115
|
if (current && !opts.overwrite) return { status: "exists" };
|
|
38310
39116
|
if (current && !current.isFile())
|
|
38311
39117
|
throw new PathContainmentError("repository path ditolak: target bukan file regular");
|
|
38312
|
-
const temp =
|
|
39118
|
+
const temp = join14(entry.parent, `.hanoman-${randomUUID6()}.tmp`);
|
|
38313
39119
|
try {
|
|
38314
39120
|
await pipeline(source, createWriteStream(temp, { flags: "wx", mode: 384 }));
|
|
38315
39121
|
if (opts.isTruncated?.()) {
|
|
@@ -38349,7 +39155,7 @@ async function worktreeInputs(id) {
|
|
|
38349
39155
|
sessions: new Map(listSessions().filter((s2) => s2.projectId === id && !s2.exited).map((s2) => [resolve16(s2.cwd), { id: s2.id, specId: s2.specId ?? null }]))
|
|
38350
39156
|
};
|
|
38351
39157
|
}
|
|
38352
|
-
var execAsync = promisify10(
|
|
39158
|
+
var execAsync = promisify10(execFile11);
|
|
38353
39159
|
function entryError(reply, e) {
|
|
38354
39160
|
if (e instanceof EntryMissingError) return reply.code(404).send({ error: "not found" });
|
|
38355
39161
|
if (e instanceof EntryExistsError) return reply.code(409).send({ error: "sudah ada" });
|
|
@@ -38813,14 +39619,14 @@ async function finishGraphOp(reply, id, repoDir, r, verb) {
|
|
|
38813
39619
|
// src/routes/fs.ts
|
|
38814
39620
|
import { readdir as readdir3 } from "node:fs/promises";
|
|
38815
39621
|
import { homedir as homedir4 } from "node:os";
|
|
38816
|
-
import { resolve as resolve17, dirname as dirname9, join as
|
|
39622
|
+
import { resolve as resolve17, dirname as dirname9, join as join15 } from "node:path";
|
|
38817
39623
|
async function fs_default(app2) {
|
|
38818
39624
|
app2.get("/fs/browse", async (req, reply) => {
|
|
38819
39625
|
const q = req.query.path;
|
|
38820
39626
|
const dir2 = q && q.trim() ? resolve17(q.trim()) : homedir4();
|
|
38821
39627
|
try {
|
|
38822
39628
|
const ents = await readdir3(dir2, { withFileTypes: true });
|
|
38823
|
-
const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path:
|
|
39629
|
+
const entries = ents.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, path: join15(dir2, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
38824
39630
|
const parent = dirname9(dir2);
|
|
38825
39631
|
return { path: dir2, parent: parent === dir2 ? null : parent, entries };
|
|
38826
39632
|
} catch {
|
|
@@ -39021,6 +39827,7 @@ function setBounded(map, key, value, maxKeys) {
|
|
|
39021
39827
|
}
|
|
39022
39828
|
|
|
39023
39829
|
// src/services/auth.ts
|
|
39830
|
+
init_src2();
|
|
39024
39831
|
init_db();
|
|
39025
39832
|
import { randomBytes as randomBytes3, scrypt as scryptCb, timingSafeEqual as timingSafeEqual2, createHash as createHash6 } from "node:crypto";
|
|
39026
39833
|
import { promisify as promisify11 } from "node:util";
|
|
@@ -39087,11 +39894,13 @@ function clearLoginFails(ip) {
|
|
|
39087
39894
|
fails.clear(ip);
|
|
39088
39895
|
loginStates.delete(ip);
|
|
39089
39896
|
}
|
|
39090
|
-
function cookieOpts() {
|
|
39897
|
+
function cookieOpts(req) {
|
|
39898
|
+
const forwarded = String(req.headers["x-forwarded-proto"] ?? "").split(",")[0]?.trim();
|
|
39899
|
+
const https = req.protocol === "https" || forwarded === "https";
|
|
39091
39900
|
return {
|
|
39092
39901
|
httpOnly: true,
|
|
39093
39902
|
sameSite: "strict",
|
|
39094
|
-
secure: process.env
|
|
39903
|
+
secure: https || resolveHardening(process.env),
|
|
39095
39904
|
path: "/",
|
|
39096
39905
|
maxAge: SESSION_TTL_MS / 1e3
|
|
39097
39906
|
};
|
|
@@ -39217,6 +40026,41 @@ async function revalidateWsPrincipal(req, principal) {
|
|
|
39217
40026
|
}
|
|
39218
40027
|
return req.agent?.id === principal.id;
|
|
39219
40028
|
}
|
|
40029
|
+
var WS_REVALIDATE_EVERY_MS = 1e3;
|
|
40030
|
+
function createPrincipalWatch(opts) {
|
|
40031
|
+
const now = opts.now ?? Date.now;
|
|
40032
|
+
const everyMs = opts.everyMs ?? WS_REVALIDATE_EVERY_MS;
|
|
40033
|
+
let revoked = false;
|
|
40034
|
+
let disposed = false;
|
|
40035
|
+
let inFlight3 = false;
|
|
40036
|
+
let attemptedAt = null;
|
|
40037
|
+
const run3 = () => {
|
|
40038
|
+
if (revoked || disposed || inFlight3) return;
|
|
40039
|
+
inFlight3 = true;
|
|
40040
|
+
attemptedAt = now();
|
|
40041
|
+
opts.check().then((ok2) => {
|
|
40042
|
+
inFlight3 = false;
|
|
40043
|
+
if (disposed || revoked || ok2) return;
|
|
40044
|
+
revoked = true;
|
|
40045
|
+
opts.onRevoked();
|
|
40046
|
+
}, () => {
|
|
40047
|
+
inFlight3 = false;
|
|
40048
|
+
});
|
|
40049
|
+
};
|
|
40050
|
+
return {
|
|
40051
|
+
admit() {
|
|
40052
|
+
if (revoked) return false;
|
|
40053
|
+
if (attemptedAt === null || now() - attemptedAt >= everyMs) run3();
|
|
40054
|
+
return true;
|
|
40055
|
+
},
|
|
40056
|
+
refresh() {
|
|
40057
|
+
run3();
|
|
40058
|
+
},
|
|
40059
|
+
dispose() {
|
|
40060
|
+
disposed = true;
|
|
40061
|
+
}
|
|
40062
|
+
};
|
|
40063
|
+
}
|
|
39220
40064
|
function bearerToken(req) {
|
|
39221
40065
|
const value = req.headers.authorization;
|
|
39222
40066
|
return typeof value === "string" && value.startsWith("Bearer ") ? value.slice(7) : null;
|
|
@@ -39260,14 +40104,14 @@ function installCommand(m, agent, shell = shellBin()) {
|
|
|
39260
40104
|
}
|
|
39261
40105
|
|
|
39262
40106
|
// src/services/terminal-diag.ts
|
|
39263
|
-
import { appendFileSync as appendFileSync2, mkdirSync as
|
|
39264
|
-
import { join as
|
|
40107
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, statSync as statSync3, rmSync as rmSync4 } from "node:fs";
|
|
40108
|
+
import { join as join16 } from "node:path";
|
|
39265
40109
|
var DIAG_MAX_BYTES = 2 * 1024 * 1024;
|
|
39266
40110
|
var KINDS = /* @__PURE__ */ new Set(["key", "comp", "data", "ack", "pred"]);
|
|
39267
40111
|
var ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
39268
40112
|
function diagFile(home3, sessionId2) {
|
|
39269
40113
|
if (!ID.test(sessionId2)) throw new Error(`id sesi tak sah untuk diag: ${sessionId2}`);
|
|
39270
|
-
return
|
|
40114
|
+
return join16(home3, "diag", `${sessionId2}.jsonl`);
|
|
39271
40115
|
}
|
|
39272
40116
|
function usable(ev) {
|
|
39273
40117
|
if (!ev || typeof ev !== "object") return false;
|
|
@@ -39278,7 +40122,7 @@ function appendDiag(home3, sessionId2, events2) {
|
|
|
39278
40122
|
const file = diagFile(home3, sessionId2);
|
|
39279
40123
|
const rows = events2.filter(usable);
|
|
39280
40124
|
if (!rows.length) return;
|
|
39281
|
-
|
|
40125
|
+
mkdirSync9(join16(home3, "diag"), { recursive: true });
|
|
39282
40126
|
try {
|
|
39283
40127
|
if (statSync3(file).size > DIAG_MAX_BYTES) rmSync4(file);
|
|
39284
40128
|
} catch {
|
|
@@ -39291,7 +40135,103 @@ init_notifications();
|
|
|
39291
40135
|
init_stage_machine();
|
|
39292
40136
|
init_pty();
|
|
39293
40137
|
init_uploads();
|
|
40138
|
+
|
|
40139
|
+
// src/services/session-dialog.ts
|
|
40140
|
+
init_tui_dialog();
|
|
40141
|
+
init_pty();
|
|
40142
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
40143
|
+
var DIALOG_CHUNK_MS = 50;
|
|
40144
|
+
var screenHashOf = (paneText2) => createHash8("sha256").update(dialogKey(paneText2)).digest("hex").slice(0, 16);
|
|
40145
|
+
var answerable = (paneText2) => {
|
|
40146
|
+
const s2 = readDialogScreen(paneText2);
|
|
40147
|
+
if (s2?.kind !== "question") return null;
|
|
40148
|
+
if (!(s2.multi && s2.submit.present) && s2.freeIndex === null && !s2.notes) return null;
|
|
40149
|
+
return s2;
|
|
40150
|
+
};
|
|
40151
|
+
function readSessionDialog(io) {
|
|
40152
|
+
const text = io.capture();
|
|
40153
|
+
const s2 = answerable(text);
|
|
40154
|
+
if (!s2) return null;
|
|
40155
|
+
return {
|
|
40156
|
+
dialog: {
|
|
40157
|
+
title: s2.title,
|
|
40158
|
+
multi: s2.multi,
|
|
40159
|
+
freeIndex: s2.freeIndex,
|
|
40160
|
+
notes: s2.notes,
|
|
40161
|
+
options: s2.rows.filter((r) => !r.free && !r.chat).map((r) => ({ n: r.n, label: r.label, checked: r.checked })),
|
|
40162
|
+
tabs: s2.tabs
|
|
40163
|
+
},
|
|
40164
|
+
screenHash: screenHashOf(text)
|
|
40165
|
+
};
|
|
40166
|
+
}
|
|
40167
|
+
async function answerSessionDialog(io, input, chunkMs = DIALOG_CHUNK_MS) {
|
|
40168
|
+
const text = io.capture();
|
|
40169
|
+
const s2 = answerable(text);
|
|
40170
|
+
if (!s2) return { ok: false, reason: "stale" };
|
|
40171
|
+
if (screenHashOf(text) !== input.screenHash) return { ok: false, reason: "stale" };
|
|
40172
|
+
const optionLabel = (n2) => s2.rows.find((r) => r.n === n2 && !r.free && !r.chat)?.label ?? null;
|
|
40173
|
+
if (s2.multi && s2.submit.present) {
|
|
40174
|
+
if (input.choice !== void 0) return { ok: false, reason: "shape" };
|
|
40175
|
+
const pick2 = input.choices ?? [];
|
|
40176
|
+
if (pick2.some((n2) => optionLabel(n2) === null)) return { ok: false, reason: "shape" };
|
|
40177
|
+
if (input.text && s2.freeIndex === null) return { ok: false, reason: "shape" };
|
|
40178
|
+
const ok3 = await answerMultiSelectDialog(
|
|
40179
|
+
io,
|
|
40180
|
+
{ pick: pick2, line: input.text ?? "", freeIndex: s2.freeIndex },
|
|
40181
|
+
chunkMs
|
|
40182
|
+
);
|
|
40183
|
+
return ok3 ? { ok: true } : { ok: false, reason: "not-landed" };
|
|
40184
|
+
}
|
|
40185
|
+
if (input.choices !== void 0) return { ok: false, reason: "shape" };
|
|
40186
|
+
const line = input.text ?? (input.choice !== void 0 ? optionLabel(input.choice) : null);
|
|
40187
|
+
if (!line) return { ok: false, reason: "shape" };
|
|
40188
|
+
const ok2 = s2.freeIndex !== null ? await answerChoiceDialog(io, s2.freeIndex, line, chunkMs) : await answerNotesDialog(io, line, chunkMs);
|
|
40189
|
+
return ok2 ? { ok: true } : { ok: false, reason: "not-landed" };
|
|
40190
|
+
}
|
|
40191
|
+
var answering = /* @__PURE__ */ new Set();
|
|
40192
|
+
function beginAnswer(sessionId2) {
|
|
40193
|
+
if (answering.has(sessionId2)) return false;
|
|
40194
|
+
answering.add(sessionId2);
|
|
40195
|
+
return true;
|
|
40196
|
+
}
|
|
40197
|
+
function endAnswer(sessionId2) {
|
|
40198
|
+
answering.delete(sessionId2);
|
|
40199
|
+
}
|
|
40200
|
+
var paneIOFactory = paneIO;
|
|
40201
|
+
var sessionPaneIO = (id) => paneIOFactory(id);
|
|
40202
|
+
|
|
40203
|
+
// src/services/lead/deciding.ts
|
|
40204
|
+
var deciding = /* @__PURE__ */ new Set();
|
|
40205
|
+
function markDeciding(sessionId2) {
|
|
40206
|
+
deciding.add(sessionId2);
|
|
40207
|
+
}
|
|
40208
|
+
function clearDeciding(sessionId2) {
|
|
40209
|
+
deciding.delete(sessionId2);
|
|
40210
|
+
}
|
|
40211
|
+
function isDeciding(sessionId2) {
|
|
40212
|
+
return deciding.has(sessionId2);
|
|
40213
|
+
}
|
|
40214
|
+
function decidingIds() {
|
|
40215
|
+
return [...deciding];
|
|
40216
|
+
}
|
|
40217
|
+
var queued = /* @__PURE__ */ new Set();
|
|
40218
|
+
function markQueued(sessionId2) {
|
|
40219
|
+
queued.add(sessionId2);
|
|
40220
|
+
}
|
|
40221
|
+
function clearQueued(sessionId2) {
|
|
40222
|
+
queued.delete(sessionId2);
|
|
40223
|
+
}
|
|
40224
|
+
function queuedIds() {
|
|
40225
|
+
return [...queued];
|
|
40226
|
+
}
|
|
40227
|
+
|
|
40228
|
+
// src/routes/terminal.ts
|
|
39294
40229
|
var ATTACHMENT_MIME = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
40230
|
+
var DIALOG_ANSWER_ERROR = {
|
|
40231
|
+
stale: "layar dialog sudah berubah \u2014 muat ulang pertanyaannya",
|
|
40232
|
+
shape: "bentuk jawaban tak cocok dengan dialog di layar",
|
|
40233
|
+
"not-landed": "jawaban tak mendarat di pane \u2014 sesi tidak digerakkan"
|
|
40234
|
+
};
|
|
39295
40235
|
var TERMINAL_WS_MESSAGES_PER_MINUTE = 6e3;
|
|
39296
40236
|
var ensureWorktree = (repoDir, wt, branchFrom) => {
|
|
39297
40237
|
if (realGit.worktreeAlive(wt)) return true;
|
|
@@ -39515,6 +40455,33 @@ async function terminal_default(app2, opts) {
|
|
|
39515
40455
|
if (!ok2) return reply.code(404).send({ error: "live session not found" });
|
|
39516
40456
|
return reply.code(202).send({ accepted: true });
|
|
39517
40457
|
});
|
|
40458
|
+
app2.get("/terminal/sessions/:id/dialog", async (req, reply) => {
|
|
40459
|
+
const { id } = req.params;
|
|
40460
|
+
const s2 = getSession(id);
|
|
40461
|
+
if (!s2) return reply.code(404).send({ error: "not found" });
|
|
40462
|
+
if (s2.exited) return reply.code(204).send();
|
|
40463
|
+
const found = readSessionDialog(sessionPaneIO(id));
|
|
40464
|
+
if (!found) return reply.code(204).send();
|
|
40465
|
+
return found;
|
|
40466
|
+
});
|
|
40467
|
+
app2.post("/terminal/sessions/:id/dialog/answer", async (req, reply) => {
|
|
40468
|
+
const parsed = zSessionDialogAnswer.safeParse(req.body);
|
|
40469
|
+
if (!parsed.success) return reply.code(400).send({ error: "invalid body" });
|
|
40470
|
+
const { id } = req.params;
|
|
40471
|
+
const s2 = getSession(id);
|
|
40472
|
+
if (!s2 || s2.exited) return reply.code(404).send({ error: "live session not found" });
|
|
40473
|
+
if (isDeciding(id))
|
|
40474
|
+
return reply.code(409).send({ error: "hanoman-lead sedang menyusun keputusan untuk sesi ini", reason: "deciding" });
|
|
40475
|
+
if (!beginAnswer(id))
|
|
40476
|
+
return reply.code(409).send({ error: "jawaban lain sedang dikirim ke sesi ini", reason: "answering" });
|
|
40477
|
+
try {
|
|
40478
|
+
const r = await answerSessionDialog(sessionPaneIO(id), parsed.data);
|
|
40479
|
+
if (!r.ok) return reply.code(409).send({ error: DIALOG_ANSWER_ERROR[r.reason], reason: r.reason });
|
|
40480
|
+
return reply.code(202).send({ accepted: true });
|
|
40481
|
+
} finally {
|
|
40482
|
+
endAnswer(id);
|
|
40483
|
+
}
|
|
40484
|
+
});
|
|
39518
40485
|
const sessionWorktree = async (id) => {
|
|
39519
40486
|
const s2 = getSession(id);
|
|
39520
40487
|
if (!s2) return { ok: false, code: 404, msg: "not found" };
|
|
@@ -39614,17 +40581,17 @@ async function terminal_default(app2, opts) {
|
|
|
39614
40581
|
const guard = new WsMessageGuard({ perWindow: TERMINAL_WS_MESSAGES_PER_MINUTE });
|
|
39615
40582
|
const client = { send: (m) => socket2.send(m), close: () => socket2.close() };
|
|
39616
40583
|
attach(id, client);
|
|
39617
|
-
|
|
40584
|
+
const watch = createPrincipalWatch({
|
|
40585
|
+
check: () => revalidateWsPrincipal(req, principal),
|
|
40586
|
+
onRevoked: () => socket2.close(1008, "session revoked")
|
|
40587
|
+
});
|
|
40588
|
+
socket2.on("message", (raw2) => {
|
|
39618
40589
|
const verdict = guard.accept(raw2);
|
|
39619
40590
|
if (!verdict.ok) {
|
|
39620
40591
|
socket2.close(verdict.code, verdict.reason);
|
|
39621
40592
|
return;
|
|
39622
40593
|
}
|
|
39623
|
-
|
|
39624
|
-
if (!valid) {
|
|
39625
|
-
socket2.close(1008, "session revoked");
|
|
39626
|
-
return;
|
|
39627
|
-
}
|
|
40594
|
+
if (!watch.admit()) return;
|
|
39628
40595
|
let m;
|
|
39629
40596
|
try {
|
|
39630
40597
|
m = JSON.parse(raw2.toString());
|
|
@@ -39642,14 +40609,11 @@ async function terminal_default(app2, opts) {
|
|
|
39642
40609
|
}
|
|
39643
40610
|
}
|
|
39644
40611
|
});
|
|
39645
|
-
const revalidate = setInterval(() =>
|
|
39646
|
-
void revalidateWsPrincipal(req, principal).then((ok2) => {
|
|
39647
|
-
if (!ok2) socket2.close(1008, "session revoked");
|
|
39648
|
-
});
|
|
39649
|
-
}, 6e4);
|
|
40612
|
+
const revalidate = setInterval(() => watch.refresh(), 6e4);
|
|
39650
40613
|
revalidate.unref?.();
|
|
39651
40614
|
socket2.on("close", () => {
|
|
39652
40615
|
clearInterval(revalidate);
|
|
40616
|
+
watch.dispose();
|
|
39653
40617
|
release();
|
|
39654
40618
|
detach(id, client);
|
|
39655
40619
|
});
|
|
@@ -39746,7 +40710,7 @@ async function terminalWorkspaceRoutes(app2) {
|
|
|
39746
40710
|
// src/routes/vps.ts
|
|
39747
40711
|
init_db();
|
|
39748
40712
|
init_src();
|
|
39749
|
-
import { readFileSync as
|
|
40713
|
+
import { readFileSync as readFileSync13, existsSync as existsSync14 } from "node:fs";
|
|
39750
40714
|
import { homedir as homedir6 } from "node:os";
|
|
39751
40715
|
|
|
39752
40716
|
// src/vps/catalog/catalog.data.ts
|
|
@@ -39828,15 +40792,130 @@ var SECTIONS = RAW_SECTIONS.map((s2) => ({
|
|
|
39828
40792
|
var _map = new Map(CATALOG.map((c) => [c.id, c]));
|
|
39829
40793
|
var byId = (id) => _map.get(id);
|
|
39830
40794
|
|
|
39831
|
-
// src/
|
|
39832
|
-
|
|
40795
|
+
// src/vps/catalog/components.ts
|
|
40796
|
+
var BOTH = ["lab", "production"];
|
|
40797
|
+
var COMPONENTS = [
|
|
40798
|
+
{
|
|
40799
|
+
id: "base",
|
|
40800
|
+
label: "Paket dasar (curl, git, tmux, toolchain node-pty)",
|
|
40801
|
+
section: "dasar",
|
|
40802
|
+
requires: { lab: [], production: [] },
|
|
40803
|
+
profiles: BOTH,
|
|
40804
|
+
interactiveLogin: false,
|
|
40805
|
+
needsDomain: false
|
|
40806
|
+
},
|
|
40807
|
+
{
|
|
40808
|
+
id: "node",
|
|
40809
|
+
label: "Node.js 22 LTS",
|
|
40810
|
+
section: "dasar",
|
|
40811
|
+
requires: { lab: ["base"], production: ["base"] },
|
|
40812
|
+
profiles: BOTH,
|
|
40813
|
+
interactiveLogin: false,
|
|
40814
|
+
needsDomain: false
|
|
40815
|
+
},
|
|
40816
|
+
{
|
|
40817
|
+
id: "hanoman",
|
|
40818
|
+
label: "hanoman (npm global + user service + systemd)",
|
|
40819
|
+
section: "hanoman",
|
|
40820
|
+
requires: { lab: ["node"], production: ["node", "podman"] },
|
|
40821
|
+
profiles: BOTH,
|
|
40822
|
+
interactiveLogin: false,
|
|
40823
|
+
needsDomain: false
|
|
40824
|
+
},
|
|
40825
|
+
{
|
|
40826
|
+
id: "caddy",
|
|
40827
|
+
label: "Caddy + TLS otomatis",
|
|
40828
|
+
section: "ingress",
|
|
40829
|
+
requires: { lab: [], production: [] },
|
|
40830
|
+
profiles: BOTH,
|
|
40831
|
+
interactiveLogin: false,
|
|
40832
|
+
needsDomain: true
|
|
40833
|
+
},
|
|
40834
|
+
{
|
|
40835
|
+
id: "podman",
|
|
40836
|
+
label: "Podman rootless + network egress",
|
|
40837
|
+
section: "sandbox",
|
|
40838
|
+
requires: { lab: ["base"], production: ["base"] },
|
|
40839
|
+
profiles: BOTH,
|
|
40840
|
+
interactiveLogin: false,
|
|
40841
|
+
needsDomain: false
|
|
40842
|
+
},
|
|
40843
|
+
{
|
|
40844
|
+
id: "agent-image",
|
|
40845
|
+
label: "Image agen hanoman-agent:latest",
|
|
40846
|
+
section: "sandbox",
|
|
40847
|
+
requires: { lab: [], production: ["podman"] },
|
|
40848
|
+
profiles: ["production"],
|
|
40849
|
+
interactiveLogin: false,
|
|
40850
|
+
needsDomain: false
|
|
40851
|
+
},
|
|
40852
|
+
{
|
|
40853
|
+
id: "claude",
|
|
40854
|
+
label: "Claude Code CLI",
|
|
40855
|
+
section: "agen",
|
|
40856
|
+
requires: { lab: ["node"], production: ["agent-image"] },
|
|
40857
|
+
profiles: BOTH,
|
|
40858
|
+
interactiveLogin: true,
|
|
40859
|
+
needsDomain: false
|
|
40860
|
+
},
|
|
40861
|
+
{
|
|
40862
|
+
id: "codex",
|
|
40863
|
+
label: "Codex CLI",
|
|
40864
|
+
section: "agen",
|
|
40865
|
+
requires: { lab: ["node"], production: ["agent-image"] },
|
|
40866
|
+
profiles: BOTH,
|
|
40867
|
+
interactiveLogin: true,
|
|
40868
|
+
needsDomain: false
|
|
40869
|
+
},
|
|
40870
|
+
{
|
|
40871
|
+
id: "gh",
|
|
40872
|
+
label: "GitHub CLI",
|
|
40873
|
+
section: "agen",
|
|
40874
|
+
requires: { lab: ["base"], production: ["base"] },
|
|
40875
|
+
profiles: BOTH,
|
|
40876
|
+
interactiveLogin: true,
|
|
40877
|
+
needsDomain: false
|
|
40878
|
+
}
|
|
40879
|
+
];
|
|
40880
|
+
var BY_ID = new Map(COMPONENTS.map((c) => [c.id, c]));
|
|
40881
|
+
var componentById = (id) => BY_ID.get(id);
|
|
40882
|
+
function resolveComponents(ids, profile) {
|
|
40883
|
+
if (ids.length === 0) return { ok: false, error: "tak ada komponen yang dipilih" };
|
|
40884
|
+
const out4 = [];
|
|
40885
|
+
const seen = /* @__PURE__ */ new Set();
|
|
40886
|
+
const stack = /* @__PURE__ */ new Set();
|
|
40887
|
+
const visit = (id) => {
|
|
40888
|
+
const c = componentById(id);
|
|
40889
|
+
if (!c) return `komponen tak dikenal: ${id}`;
|
|
40890
|
+
if (!c.profiles.includes(profile)) return `komponen ${id} tak tersedia di profil ${profile}`;
|
|
40891
|
+
if (seen.has(id)) return null;
|
|
40892
|
+
if (stack.has(id)) return `siklus dependensi pada ${id}`;
|
|
40893
|
+
stack.add(id);
|
|
40894
|
+
for (const dep of c.requires[profile]) {
|
|
40895
|
+
const err = visit(dep);
|
|
40896
|
+
if (err) return err;
|
|
40897
|
+
}
|
|
40898
|
+
stack.delete(id);
|
|
40899
|
+
seen.add(id);
|
|
40900
|
+
out4.push(id);
|
|
40901
|
+
return null;
|
|
40902
|
+
};
|
|
40903
|
+
for (const id of ids) {
|
|
40904
|
+
const err = visit(id);
|
|
40905
|
+
if (err) return { ok: false, error: err };
|
|
40906
|
+
}
|
|
40907
|
+
return { ok: true, items: out4 };
|
|
40908
|
+
}
|
|
40909
|
+
|
|
40910
|
+
// src/services/vps-provision.ts
|
|
40911
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
39833
40912
|
|
|
39834
40913
|
// src/services/vps-ssh.ts
|
|
39835
40914
|
init_config2();
|
|
39836
40915
|
import { spawn as spawn4 } from "node:child_process";
|
|
39837
|
-
import { mkdtempSync, rmSync as rmSync5, writeFileSync as
|
|
40916
|
+
import { mkdtempSync, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
39838
40917
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
39839
|
-
import { dirname as dirname10, join as
|
|
40918
|
+
import { dirname as dirname10, join as join17 } from "node:path";
|
|
39840
40919
|
var sshBin = () => effectiveStr("HANOMAN_SSH_BIN") ?? "ssh";
|
|
39841
40920
|
function consoleArgv(t) {
|
|
39842
40921
|
return [
|
|
@@ -39851,9 +40930,9 @@ function consoleArgv(t) {
|
|
|
39851
40930
|
];
|
|
39852
40931
|
}
|
|
39853
40932
|
function askpassScript() {
|
|
39854
|
-
const dir2 = mkdtempSync(
|
|
39855
|
-
const path2 =
|
|
39856
|
-
|
|
40933
|
+
const dir2 = mkdtempSync(join17(tmpdir3(), "hanoman-askpass-"));
|
|
40934
|
+
const path2 = join17(dir2, "askpass.sh");
|
|
40935
|
+
writeFileSync5(path2, `#!/bin/sh
|
|
39857
40936
|
printf '%s' "$HANOMAN_SSH_PASSWORD"
|
|
39858
40937
|
`, { mode: 448 });
|
|
39859
40938
|
return path2;
|
|
@@ -39912,16 +40991,17 @@ function sshExec(t, remoteCmd, opts = {}) {
|
|
|
39912
40991
|
|
|
39913
40992
|
// src/services/vps-audit.ts
|
|
39914
40993
|
init_db();
|
|
39915
|
-
import { readFileSync as
|
|
39916
|
-
import { join as
|
|
40994
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
|
|
40995
|
+
import { join as join19 } from "node:path";
|
|
40996
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
39917
40997
|
|
|
39918
40998
|
// src/runner/deps.ts
|
|
39919
40999
|
import { existsSync as existsSync11 } from "node:fs";
|
|
39920
|
-
import { dirname as dirname11, join as
|
|
41000
|
+
import { dirname as dirname11, join as join18 } from "node:path";
|
|
39921
41001
|
function repoRootFrom(startDir) {
|
|
39922
41002
|
let dir2 = startDir;
|
|
39923
41003
|
for (let i = 0; i < 8; i++) {
|
|
39924
|
-
if (existsSync11(
|
|
41004
|
+
if (existsSync11(join18(dir2, "pnpm-workspace.yaml"))) return dir2;
|
|
39925
41005
|
const parent = dirname11(dir2);
|
|
39926
41006
|
if (parent === dir2) break;
|
|
39927
41007
|
dir2 = parent;
|
|
@@ -40027,7 +41107,12 @@ function parseHealth(out4) {
|
|
|
40027
41107
|
return { uptime: h.uptime ?? "", disk: h.disk ?? "", mem: h.mem ?? "", load: h.load ?? "" };
|
|
40028
41108
|
}
|
|
40029
41109
|
var target = (v) => ({ host: v.host, port: v.port, user: v.user, keyPath: v.keyPath });
|
|
40030
|
-
var
|
|
41110
|
+
var packagedScript = (f) => fileURLToPath3(new URL(`../scripts/vps/${f}`, import.meta.url));
|
|
41111
|
+
var moduleDir = () => fileURLToPath3(new URL(".", import.meta.url));
|
|
41112
|
+
var scriptPath = (f) => {
|
|
41113
|
+
const packed = packagedScript(f);
|
|
41114
|
+
return existsSync12(packed) ? packed : join19(repoRoot(moduleDir()), "server", "scripts", "vps", f);
|
|
41115
|
+
};
|
|
40031
41116
|
async function itemStatesOf(vpsId) {
|
|
40032
41117
|
const rows = await prisma.vpsItemState.findMany({ where: { vpsId } });
|
|
40033
41118
|
return Object.fromEntries(rows.map((s2) => [s2.itemId, { na: s2.na, attested: s2.attested }]));
|
|
@@ -40036,7 +41121,7 @@ async function runAudit(v) {
|
|
|
40036
41121
|
const r = await sshExec(
|
|
40037
41122
|
target(v),
|
|
40038
41123
|
"sudo -n bash -s",
|
|
40039
|
-
{ stdin:
|
|
41124
|
+
{ stdin: readFileSync9(scriptPath("audit.sh"), "utf8"), timeoutMs: 6e4 }
|
|
40040
41125
|
);
|
|
40041
41126
|
const audit = parseAudit(r.out);
|
|
40042
41127
|
if (r.code !== 0 || audit.length === 0) return { ok: false, out: r.out };
|
|
@@ -40072,19 +41157,78 @@ async function runAudit(v) {
|
|
|
40072
41157
|
await notifySynced("vps", v.id);
|
|
40073
41158
|
return { ok: true, audit, hardened, scoreTotal: scored.total, scoreBySection: scored.bySection, drift };
|
|
40074
41159
|
}
|
|
41160
|
+
var PUBLISH_HEARTBEAT_MS = 36e5;
|
|
41161
|
+
function shouldPublishHealth(prev, next, now = Date.now()) {
|
|
41162
|
+
if (JSON.stringify(prev?.health ?? null) !== JSON.stringify(next ?? null)) return true;
|
|
41163
|
+
if (!prev?.lastPublishedAt) return true;
|
|
41164
|
+
return now - prev.lastPublishedAt.getTime() >= PUBLISH_HEARTBEAT_MS;
|
|
41165
|
+
}
|
|
40075
41166
|
async function runHealth(v) {
|
|
40076
41167
|
const r = await sshExec(target(v), HEALTH_CMD, { timeoutMs: 6e4 });
|
|
40077
41168
|
const health = parseHealth(r.out);
|
|
40078
41169
|
if (r.code !== 0 || !health) return false;
|
|
41170
|
+
const prev = await prisma.vps.findUnique({
|
|
41171
|
+
where: { id: v.id },
|
|
41172
|
+
select: { health: true, lastPublishedAt: true }
|
|
41173
|
+
});
|
|
41174
|
+
const publish = shouldPublishHealth(prev, health);
|
|
40079
41175
|
await prisma.vps.update({ where: { id: v.id }, data: {
|
|
40080
41176
|
health,
|
|
40081
|
-
lastSeenAt: /* @__PURE__ */ new Date()
|
|
41177
|
+
lastSeenAt: /* @__PURE__ */ new Date(),
|
|
41178
|
+
...publish ? { lastPublishedAt: /* @__PURE__ */ new Date() } : {}
|
|
40082
41179
|
} });
|
|
40083
|
-
await notifySynced("vps", v.id);
|
|
41180
|
+
if (publish) await notifySynced("vps", v.id);
|
|
40084
41181
|
return true;
|
|
40085
41182
|
}
|
|
40086
41183
|
|
|
41184
|
+
// src/services/vps-provision.ts
|
|
41185
|
+
var PROVISION_TIMEOUT_MS = 9e5;
|
|
41186
|
+
var PROBE_TIMEOUT_MS = 6e4;
|
|
41187
|
+
var STATUSES = ["ok", "partial", "absent"];
|
|
41188
|
+
function parseComponents(out4) {
|
|
41189
|
+
return out4.split("\n").flatMap((line) => {
|
|
41190
|
+
const m = line.match(/^COMP (\S+) (\S+)(?: (.*))?$/);
|
|
41191
|
+
if (!m) return [];
|
|
41192
|
+
const [, id, status] = m;
|
|
41193
|
+
if (!componentById(id)) return [];
|
|
41194
|
+
if (!STATUSES.includes(status)) return [];
|
|
41195
|
+
return [{ id, status, detail: (m[3] ?? "").trim() }];
|
|
41196
|
+
});
|
|
41197
|
+
}
|
|
41198
|
+
function parseProvisionSteps(out4) {
|
|
41199
|
+
return out4.split("\n").flatMap((line) => {
|
|
41200
|
+
const m = line.match(/^STEP (\S+) (would|ok|fail|skip)(?: (.*))?$/);
|
|
41201
|
+
return m ? [{ item: m[1], status: m[2], detail: (m[3] ?? "").trim() }] : [];
|
|
41202
|
+
});
|
|
41203
|
+
}
|
|
41204
|
+
var script = () => readFileSync10(scriptPath("provision.sh"), "utf8");
|
|
41205
|
+
async function probeComponents(v) {
|
|
41206
|
+
const r = await sshExec(v, "env MODE=probe bash -s", { stdin: script(), timeoutMs: PROBE_TIMEOUT_MS });
|
|
41207
|
+
const components2 = parseComponents(r.out);
|
|
41208
|
+
if (r.code !== 0 || components2.length === 0) return { ok: false, out: r.out };
|
|
41209
|
+
return { ok: true, components: components2 };
|
|
41210
|
+
}
|
|
41211
|
+
async function provision(v, items, opts) {
|
|
41212
|
+
const env = [
|
|
41213
|
+
"MODE=apply",
|
|
41214
|
+
`ITEMS=${items.join(",")}`,
|
|
41215
|
+
`PROFILE=${opts.profile}`,
|
|
41216
|
+
...opts.domain ? [`DOMAIN=${opts.domain}`] : [],
|
|
41217
|
+
...opts.dryRun ? ["DRY_RUN=1"] : []
|
|
41218
|
+
].join(" ");
|
|
41219
|
+
const r = await sshExec(v, `sudo -n env ${env} bash -s`, { stdin: script(), timeoutMs: PROVISION_TIMEOUT_MS });
|
|
41220
|
+
return { ok: r.code === 0, steps: parseProvisionSteps(r.out), out: r.out };
|
|
41221
|
+
}
|
|
41222
|
+
async function readSetupToken(v, home3 = "/var/lib/hanoman") {
|
|
41223
|
+
const r = await sshExec(v, `sudo -n cat ${home3}/setup.token`, { timeoutMs: 15e3 });
|
|
41224
|
+
if (r.code !== 0) return null;
|
|
41225
|
+
const [token, expires] = r.out.trim().split("\n");
|
|
41226
|
+
if (!token || !expires) return null;
|
|
41227
|
+
return Date.parse(expires) > Date.now() ? token : null;
|
|
41228
|
+
}
|
|
41229
|
+
|
|
40087
41230
|
// src/services/vps-remediate.ts
|
|
41231
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
40088
41232
|
function parseSteps(out4) {
|
|
40089
41233
|
return out4.split("\n").flatMap((line) => {
|
|
40090
41234
|
const m = line.match(/^STEP (\S+) (would|ok|fail)(?: (.*))?$/);
|
|
@@ -40095,7 +41239,7 @@ async function remediate(v, items, dryRun) {
|
|
|
40095
41239
|
const r = await sshExec(
|
|
40096
41240
|
v,
|
|
40097
41241
|
`sudo -n env ITEMS=${items.join(",")} SSH_PORT=${v.port} DRY_RUN=${dryRun ? "1" : ""} bash -s`,
|
|
40098
|
-
{ stdin:
|
|
41242
|
+
{ stdin: readFileSync11(scriptPath("remediate.sh"), "utf8"), timeoutMs: 3e5 }
|
|
40099
41243
|
);
|
|
40100
41244
|
return { ok: r.code === 0, steps: parseSteps(r.out), out: r.out };
|
|
40101
41245
|
}
|
|
@@ -40174,39 +41318,39 @@ async function buildChecklist(vpsId) {
|
|
|
40174
41318
|
// src/services/vps-key.ts
|
|
40175
41319
|
init_src2();
|
|
40176
41320
|
init_config2();
|
|
40177
|
-
import { execFileSync as
|
|
40178
|
-
import { chmodSync as
|
|
41321
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
41322
|
+
import { chmodSync as chmodSync4, copyFileSync, existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync12, unlinkSync as unlinkSync2 } from "node:fs";
|
|
40179
41323
|
import { homedir as homedir5 } from "node:os";
|
|
40180
|
-
import { join as
|
|
41324
|
+
import { join as join20 } from "node:path";
|
|
40181
41325
|
var keyDir = () => effectiveStr("HANOMAN_SSH_KEY_DIR") ?? resolveDataDirs().sshKeys;
|
|
40182
41326
|
var KEY_FILES = ["id_ed25519", "id_ed25519.pub"];
|
|
40183
41327
|
function adoptLegacyKey(dir2) {
|
|
40184
41328
|
if (effectiveStr("HANOMAN_SSH_KEY_DIR")) return;
|
|
40185
|
-
const legacy =
|
|
41329
|
+
const legacy = join20(homedir5(), ".hanoman");
|
|
40186
41330
|
if (legacy === dir2) return;
|
|
40187
|
-
if (!KEY_FILES.every((f) =>
|
|
40188
|
-
|
|
41331
|
+
if (!KEY_FILES.every((f) => existsSync13(join20(legacy, f)))) return;
|
|
41332
|
+
mkdirSync10(dir2, { recursive: true, mode: 448 });
|
|
40189
41333
|
for (const f of KEY_FILES) {
|
|
40190
|
-
copyFileSync(
|
|
40191
|
-
|
|
41334
|
+
copyFileSync(join20(legacy, f), join20(dir2, f));
|
|
41335
|
+
chmodSync4(join20(dir2, f), 384);
|
|
40192
41336
|
}
|
|
40193
|
-
for (const f of KEY_FILES) unlinkSync2(
|
|
41337
|
+
for (const f of KEY_FILES) unlinkSync2(join20(legacy, f));
|
|
40194
41338
|
console.log(`vps: key SSH dipindah dari ${legacy} ke ${dir2} (SPEC-846 \u2014 satu batas backup)`);
|
|
40195
41339
|
}
|
|
40196
41340
|
function ensureHanomanKey() {
|
|
40197
41341
|
const dir2 = keyDir();
|
|
40198
|
-
const privPath =
|
|
41342
|
+
const privPath = join20(dir2, KEY_FILES[0]);
|
|
40199
41343
|
const pubPath = `${privPath}.pub`;
|
|
40200
|
-
if (!
|
|
40201
|
-
if (!
|
|
40202
|
-
|
|
40203
|
-
|
|
41344
|
+
if (!existsSync13(privPath)) adoptLegacyKey(dir2);
|
|
41345
|
+
if (!existsSync13(privPath)) {
|
|
41346
|
+
mkdirSync10(dir2, { recursive: true, mode: 448 });
|
|
41347
|
+
execFileSync3(
|
|
40204
41348
|
"ssh-keygen",
|
|
40205
41349
|
["-t", "ed25519", "-N", "", "-C", "hanoman", "-f", privPath],
|
|
40206
41350
|
{ stdio: ["ignore", "ignore", "pipe"] }
|
|
40207
41351
|
);
|
|
40208
41352
|
}
|
|
40209
|
-
return { privPath, pubPath, pub:
|
|
41353
|
+
return { privPath, pubPath, pub: readFileSync12(pubPath, "utf8").trim() };
|
|
40210
41354
|
}
|
|
40211
41355
|
|
|
40212
41356
|
// src/services/vps-bootstrap.ts
|
|
@@ -40225,7 +41369,7 @@ async function bootstrapKey(t, password) {
|
|
|
40225
41369
|
init_pty();
|
|
40226
41370
|
init_settings2();
|
|
40227
41371
|
function keyMissing(v) {
|
|
40228
|
-
return !!v.keyPath && !
|
|
41372
|
+
return !!v.keyPath && !existsSync14(v.keyPath);
|
|
40229
41373
|
}
|
|
40230
41374
|
async function vps_default(app2) {
|
|
40231
41375
|
app2.get("/vps", async () => prisma.vps.findMany({ orderBy: { createdAt: "asc" } }));
|
|
@@ -40373,6 +41517,79 @@ async function vps_default(app2) {
|
|
|
40373
41517
|
scoreBySection: audit.ok ? audit.scoreBySection : null
|
|
40374
41518
|
};
|
|
40375
41519
|
});
|
|
41520
|
+
app2.get("/vps/components", async () => ({ components: COMPONENTS }));
|
|
41521
|
+
function planItems(items, profile, domain) {
|
|
41522
|
+
const r = resolveComponents(items, profile);
|
|
41523
|
+
if (!r.ok) return r;
|
|
41524
|
+
const needsDomain = r.items.some((id) => componentById(id)?.needsDomain);
|
|
41525
|
+
if (needsDomain && !domain) return { ok: false, error: "domain wajib untuk komponen ingress" };
|
|
41526
|
+
return r;
|
|
41527
|
+
}
|
|
41528
|
+
async function runProbe(v) {
|
|
41529
|
+
const r = await probeComponents(v);
|
|
41530
|
+
if (!r.ok) return r;
|
|
41531
|
+
const map = {};
|
|
41532
|
+
for (const c of r.components) map[c.id] = { status: c.status, detail: c.detail };
|
|
41533
|
+
const checkedAt = /* @__PURE__ */ new Date();
|
|
41534
|
+
await prisma.vps.update({ where: { id: v.id }, data: { components: map, componentsCheckedAt: checkedAt } });
|
|
41535
|
+
return { ok: true, components: r.components, checkedAt };
|
|
41536
|
+
}
|
|
41537
|
+
app2.post("/vps/:id/probe", async (req, reply) => {
|
|
41538
|
+
const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
|
|
41539
|
+
if (!v) return reply.code(404).send({ error: "not found" });
|
|
41540
|
+
if (keyMissing(v)) return reply.code(409).send({ error: "key VPS tidak ada di mesin ini", keyMissing: true });
|
|
41541
|
+
const r = await runProbe(v);
|
|
41542
|
+
if (!r.ok) return reply.code(502).send({ error: "probe gagal lewat ssh", out: r.out });
|
|
41543
|
+
return { components: r.components, checkedAt: r.checkedAt.toISOString() };
|
|
41544
|
+
});
|
|
41545
|
+
app2.post("/vps/:id/provision/preview", async (req, reply) => {
|
|
41546
|
+
const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
|
|
41547
|
+
if (!v) return reply.code(404).send({ error: "not found" });
|
|
41548
|
+
if (keyMissing(v)) return reply.code(409).send({ error: "key VPS tidak ada di mesin ini", keyMissing: true });
|
|
41549
|
+
const p = zProvision.safeParse(req.body);
|
|
41550
|
+
if (!p.success) return reply.code(400).send({ error: "invalid body" });
|
|
41551
|
+
const plan = planItems(p.data.items, p.data.profile, p.data.domain);
|
|
41552
|
+
if (!plan.ok) return reply.code(400).send({ error: plan.error });
|
|
41553
|
+
const r = await provision(v, plan.items, { profile: p.data.profile, domain: p.data.domain, dryRun: true });
|
|
41554
|
+
if (!r.ok) return reply.code(502).send({ error: "preview gagal lewat ssh", out: r.out });
|
|
41555
|
+
return { steps: r.steps };
|
|
41556
|
+
});
|
|
41557
|
+
app2.post("/vps/:id/provision", async (req, reply) => {
|
|
41558
|
+
const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
|
|
41559
|
+
if (!v) return reply.code(404).send({ error: "not found" });
|
|
41560
|
+
if (keyMissing(v)) return reply.code(409).send({ error: "key VPS tidak ada di mesin ini", keyMissing: true });
|
|
41561
|
+
const p = zProvision.safeParse(req.body);
|
|
41562
|
+
if (!p.success) return reply.code(400).send({ error: "invalid body" });
|
|
41563
|
+
const plan = planItems(p.data.items, p.data.profile, p.data.domain);
|
|
41564
|
+
if (!plan.ok) return reply.code(400).send({ error: plan.error });
|
|
41565
|
+
if (!p.data.confirm) {
|
|
41566
|
+
const dry = await provision(v, plan.items, { profile: p.data.profile, domain: p.data.domain, dryRun: true });
|
|
41567
|
+
if (!dry.ok) return reply.code(502).send({ error: "dry-run gagal lewat ssh", out: dry.out });
|
|
41568
|
+
return reply.code(409).send({ error: "confirm-required", steps: dry.steps });
|
|
41569
|
+
}
|
|
41570
|
+
const installed = v.components?.hanoman?.status === "ok";
|
|
41571
|
+
if (installed && v.provisionProfile && v.provisionProfile !== p.data.profile && !p.data.force) {
|
|
41572
|
+
return reply.code(409).send({ error: "profile-mismatch", current: v.provisionProfile });
|
|
41573
|
+
}
|
|
41574
|
+
const r = await provision(v, plan.items, { profile: p.data.profile, domain: p.data.domain, dryRun: false });
|
|
41575
|
+
if (!r.ok) return reply.code(502).send({ error: "provision gagal lewat ssh", transcript: r.out, steps: r.steps });
|
|
41576
|
+
const probe = await runProbe(v);
|
|
41577
|
+
if (!probe.ok) return reply.code(502).send({ error: "probe pasca-provision gagal", transcript: r.out, steps: r.steps });
|
|
41578
|
+
await prisma.vps.update({ where: { id: v.id }, data: { provisionProfile: p.data.profile } });
|
|
41579
|
+
let setup = null;
|
|
41580
|
+
const hanomanOk = probe.components.some((c) => c.id === "hanoman" && c.status === "ok");
|
|
41581
|
+
if (hanomanOk) {
|
|
41582
|
+
const token = await readSetupToken(v);
|
|
41583
|
+
if (token) {
|
|
41584
|
+
const base2 = p.data.domain ? `https://${p.data.domain}` : `http://${v.host}:8787`;
|
|
41585
|
+
setup = {
|
|
41586
|
+
url: `${base2}/setup?token=${encodeURIComponent(token)}`,
|
|
41587
|
+
expiresAt: new Date(Date.now() + 15 * 6e4).toISOString()
|
|
41588
|
+
};
|
|
41589
|
+
}
|
|
41590
|
+
}
|
|
41591
|
+
return { steps: r.steps, components: probe.components, checkedAt: probe.checkedAt.toISOString(), setup };
|
|
41592
|
+
});
|
|
40376
41593
|
app2.post("/vps/:id/harden", async (req, reply) => {
|
|
40377
41594
|
const v = await prisma.vps.findUnique({ where: { id: req.params.id } });
|
|
40378
41595
|
if (!v) return reply.code(404).send({ error: "not found" });
|
|
@@ -40380,7 +41597,7 @@ async function vps_default(app2) {
|
|
|
40380
41597
|
const r = await sshExec(
|
|
40381
41598
|
v,
|
|
40382
41599
|
`sudo -n env SSH_PORT=${v.port} SSH_USER=${v.user} bash -s`,
|
|
40383
|
-
{ stdin:
|
|
41600
|
+
{ stdin: readFileSync13(scriptPath("harden.sh"), "utf8"), timeoutMs: 3e5 }
|
|
40384
41601
|
);
|
|
40385
41602
|
if (r.code !== 0) return reply.code(502).send({ error: "harden gagal lewat ssh", transcript: r.out });
|
|
40386
41603
|
const verify = await sshExec(v, "true", { timeoutMs: 3e4 });
|
|
@@ -40431,10 +41648,10 @@ async function vps_default(app2) {
|
|
|
40431
41648
|
|
|
40432
41649
|
// src/services/limits.ts
|
|
40433
41650
|
init_config2();
|
|
40434
|
-
import { execFileSync as
|
|
40435
|
-
import { readFileSync as
|
|
41651
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
41652
|
+
import { readFileSync as readFileSync14 } from "node:fs";
|
|
40436
41653
|
import { homedir as homedir7 } from "node:os";
|
|
40437
|
-
import { join as
|
|
41654
|
+
import { join as join21 } from "node:path";
|
|
40438
41655
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
40439
41656
|
var TTL_MS2 = 3e4;
|
|
40440
41657
|
var lastOk = null;
|
|
@@ -40447,12 +41664,12 @@ var LABELS = {
|
|
|
40447
41664
|
var humanize = (k) => k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
40448
41665
|
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
40449
41666
|
function credsFile() {
|
|
40450
|
-
return
|
|
41667
|
+
return join21(effectiveStr("CLAUDE_CONFIG_DIR") ?? join21(homedir7(), ".claude"), ".credentials.json");
|
|
40451
41668
|
}
|
|
40452
41669
|
function readAccessToken() {
|
|
40453
41670
|
if (process.platform === "darwin" && !effectiveStr("CLAUDE_CONFIG_DIR")) {
|
|
40454
41671
|
try {
|
|
40455
|
-
const blob =
|
|
41672
|
+
const blob = execFileSync4(
|
|
40456
41673
|
"security",
|
|
40457
41674
|
["find-generic-password", "-s", "Claude Code-credentials", "-w"],
|
|
40458
41675
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
@@ -40463,7 +41680,7 @@ function readAccessToken() {
|
|
|
40463
41680
|
}
|
|
40464
41681
|
}
|
|
40465
41682
|
try {
|
|
40466
|
-
return JSON.parse(
|
|
41683
|
+
return JSON.parse(readFileSync14(credsFile(), "utf8"))?.claudeAiOauth?.accessToken ?? null;
|
|
40467
41684
|
} catch {
|
|
40468
41685
|
return null;
|
|
40469
41686
|
}
|
|
@@ -40521,14 +41738,14 @@ async function getLimits() {
|
|
|
40521
41738
|
// src/services/codex-limits.ts
|
|
40522
41739
|
import { open as open3, readdir as readdir4, stat as stat2 } from "node:fs/promises";
|
|
40523
41740
|
import { homedir as homedir8 } from "node:os";
|
|
40524
|
-
import { join as
|
|
41741
|
+
import { join as join22 } from "node:path";
|
|
40525
41742
|
var TTL_MS3 = 3e4;
|
|
40526
41743
|
var STALE_AFTER_MS = 12 * 36e5;
|
|
40527
41744
|
var TAIL_BYTES = 512 * 1024;
|
|
40528
41745
|
var MAX_FILES = 8;
|
|
40529
41746
|
var cache3 = null;
|
|
40530
41747
|
var freshUntil2 = 0;
|
|
40531
|
-
var codexSessionsDir = () =>
|
|
41748
|
+
var codexSessionsDir = () => join22(process.env.CODEX_HOME ?? join22(homedir8(), ".codex"), "sessions");
|
|
40532
41749
|
var UNAVAILABLE = { status: "unavailable", windows: [], fetchedAt: null, plan: null };
|
|
40533
41750
|
function windowLabel(minutes) {
|
|
40534
41751
|
if (minutes === 300) return "Sesi 5 jam";
|
|
@@ -40561,7 +41778,7 @@ async function recentRollouts(dir2) {
|
|
|
40561
41778
|
return [];
|
|
40562
41779
|
}
|
|
40563
41780
|
const stamped = await Promise.all(entries.map(async (rel) => {
|
|
40564
|
-
const full =
|
|
41781
|
+
const full = join22(dir2, rel);
|
|
40565
41782
|
try {
|
|
40566
41783
|
return { full, mtime: (await stat2(full)).mtimeMs };
|
|
40567
41784
|
} catch {
|
|
@@ -40641,9 +41858,9 @@ async function limits(app2) {
|
|
|
40641
41858
|
// src/services/codex-version.ts
|
|
40642
41859
|
init_src();
|
|
40643
41860
|
init_config2();
|
|
40644
|
-
import { execFile as
|
|
41861
|
+
import { execFile as execFile12 } from "node:child_process";
|
|
40645
41862
|
import { promisify as promisify12 } from "node:util";
|
|
40646
|
-
var
|
|
41863
|
+
var run2 = promisify12(execFile12);
|
|
40647
41864
|
var CODEX_MIN_CLIENT = "0.144.0";
|
|
40648
41865
|
var TTL_MS4 = 5 * 6e4;
|
|
40649
41866
|
var cache4 = null;
|
|
@@ -40656,7 +41873,7 @@ async function getCodexVersion(now = Date.now()) {
|
|
|
40656
41873
|
if (cache4 && now - cache4.at < TTL_MS4) return cache4.version;
|
|
40657
41874
|
let version2 = null;
|
|
40658
41875
|
try {
|
|
40659
|
-
const { stdout } = await
|
|
41876
|
+
const { stdout } = await run2(codexBin2(), ["--version"], { timeout: 5e3 });
|
|
40660
41877
|
version2 = parseCodexVersion(stdout);
|
|
40661
41878
|
} catch {
|
|
40662
41879
|
}
|
|
@@ -40688,9 +41905,9 @@ init_src();
|
|
|
40688
41905
|
// src/services/update.ts
|
|
40689
41906
|
init_src();
|
|
40690
41907
|
init_config2();
|
|
40691
|
-
import { readFileSync as
|
|
41908
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
40692
41909
|
import { resolve as resolve18, dirname as dirname12 } from "node:path";
|
|
40693
|
-
import { fileURLToPath as
|
|
41910
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
40694
41911
|
var UPDATE_COMMAND = "npm i -g hanoman@latest --prefer-online";
|
|
40695
41912
|
function composeUpdate(x) {
|
|
40696
41913
|
const available = x.registryStatus === "ok" && x.latestVersion != null && compareSemver(x.latestVersion, x.currentVersion) > 0;
|
|
@@ -40714,10 +41931,10 @@ var lastFetchAt = 0;
|
|
|
40714
41931
|
var lastLatest = null;
|
|
40715
41932
|
var lastStatus = "unavailable";
|
|
40716
41933
|
function runningVersion() {
|
|
40717
|
-
const here = dirname12(
|
|
41934
|
+
const here = dirname12(fileURLToPath4(import.meta.url));
|
|
40718
41935
|
for (const p of [resolve18(here, "build-info.json"), resolve18(here, "../package.json")]) {
|
|
40719
41936
|
try {
|
|
40720
|
-
const v = JSON.parse(
|
|
41937
|
+
const v = JSON.parse(readFileSync15(p, "utf8")).version;
|
|
40721
41938
|
if (typeof v === "string" && v) return v;
|
|
40722
41939
|
} catch {
|
|
40723
41940
|
}
|
|
@@ -40794,33 +42011,6 @@ async function update(app2) {
|
|
|
40794
42011
|
// src/services/events.ts
|
|
40795
42012
|
init_pty();
|
|
40796
42013
|
init_notifications();
|
|
40797
|
-
|
|
40798
|
-
// src/services/lead/deciding.ts
|
|
40799
|
-
var deciding = /* @__PURE__ */ new Set();
|
|
40800
|
-
function markDeciding(sessionId2) {
|
|
40801
|
-
deciding.add(sessionId2);
|
|
40802
|
-
}
|
|
40803
|
-
function clearDeciding(sessionId2) {
|
|
40804
|
-
deciding.delete(sessionId2);
|
|
40805
|
-
}
|
|
40806
|
-
function isDeciding(sessionId2) {
|
|
40807
|
-
return deciding.has(sessionId2);
|
|
40808
|
-
}
|
|
40809
|
-
function decidingIds() {
|
|
40810
|
-
return [...deciding];
|
|
40811
|
-
}
|
|
40812
|
-
var queued = /* @__PURE__ */ new Set();
|
|
40813
|
-
function markQueued(sessionId2) {
|
|
40814
|
-
queued.add(sessionId2);
|
|
40815
|
-
}
|
|
40816
|
-
function clearQueued(sessionId2) {
|
|
40817
|
-
queued.delete(sessionId2);
|
|
40818
|
-
}
|
|
40819
|
-
function queuedIds() {
|
|
40820
|
-
return [...queued];
|
|
40821
|
-
}
|
|
40822
|
-
|
|
40823
|
-
// src/services/events.ts
|
|
40824
42014
|
init_db();
|
|
40825
42015
|
init_config2();
|
|
40826
42016
|
var clients = /* @__PURE__ */ new Set();
|
|
@@ -40832,7 +42022,9 @@ var GROUPS = [
|
|
|
40832
42022
|
// service itu sengaja tak tahu apa-apa soal lead maupun DB.
|
|
40833
42023
|
{ everyTicks: 1, last: "", build: async () => ({
|
|
40834
42024
|
t: "sessions",
|
|
40835
|
-
|
|
42025
|
+
// Asinkron: grup ini di-recompute tiap detik, dan `execFileSync` tmux memblokir seluruh event
|
|
42026
|
+
// loop — termasuk frame ketikan terminal — selama spawn (terukur sampai 916 ms saat mesin sibuk).
|
|
42027
|
+
sessions: (await listSessionsAsync()).map((s2) => isDeciding(s2.id) ? { ...s2, deciding: true } : s2)
|
|
40836
42028
|
}) },
|
|
40837
42029
|
{ everyTicks: 1, last: "", build: async () => ({ t: "specs", specs: await liveSpecs() }) },
|
|
40838
42030
|
{ everyTicks: 3, last: "", build: async () => ({ t: "notifications", ...await notificationsFeed() }) },
|
|
@@ -41033,6 +42225,7 @@ async function bindings_default(app2) {
|
|
|
41033
42225
|
// src/routes/sync.ts
|
|
41034
42226
|
init_zod();
|
|
41035
42227
|
init_db();
|
|
42228
|
+
import { gzipSync } from "node:zlib";
|
|
41036
42229
|
|
|
41037
42230
|
// src/services/device-auth.ts
|
|
41038
42231
|
function bearerToken2(req) {
|
|
@@ -41088,10 +42281,21 @@ var zPush = external_exports.object({
|
|
|
41088
42281
|
}))
|
|
41089
42282
|
});
|
|
41090
42283
|
var AUTHORED = ["spec", "sessionResult"];
|
|
42284
|
+
function maybeGzip(req, reply, payload) {
|
|
42285
|
+
reply.header("vary", "accept-encoding");
|
|
42286
|
+
if (!/\bgzip\b/.test(String(req.headers["accept-encoding"] ?? ""))) return payload;
|
|
42287
|
+
reply.header("content-type", "application/json; charset=utf-8");
|
|
42288
|
+
reply.header("content-encoding", "gzip");
|
|
42289
|
+
return gzipSync(Buffer.from(JSON.stringify(payload)));
|
|
42290
|
+
}
|
|
41091
42291
|
async function sync_default(app2) {
|
|
41092
|
-
app2.get("/sync/pull", { preHandler: requireDeviceToken }, async (req) => {
|
|
42292
|
+
app2.get("/sync/pull", { preHandler: requireDeviceToken }, async (req, reply) => {
|
|
41093
42293
|
const since = req.query.since ?? "0";
|
|
41094
|
-
return pull(since);
|
|
42294
|
+
return maybeGzip(req, reply, await pull(since));
|
|
42295
|
+
});
|
|
42296
|
+
app2.get("/sync/bootstrap", { preHandler: requireDeviceToken }, async (req, reply) => {
|
|
42297
|
+
const after = req.query.after ?? null;
|
|
42298
|
+
return maybeGzip(req, reply, await bootstrapSnapshot(after));
|
|
41095
42299
|
});
|
|
41096
42300
|
app2.get("/sync/attachments/:storageKey", { preHandler: requireDeviceToken }, async (req, reply) => {
|
|
41097
42301
|
const { storageKey } = req.params;
|
|
@@ -41114,8 +42318,13 @@ async function sync_default(app2) {
|
|
|
41114
42318
|
}
|
|
41115
42319
|
const data = { ...rec.data };
|
|
41116
42320
|
if (AUTHORED.includes(rec.entity) && !data.author && user) data.author = user.email;
|
|
41117
|
-
|
|
41118
|
-
|
|
42321
|
+
try {
|
|
42322
|
+
const r = await applyPush(rec.entity, rec.id, rec.baseVersion, data, req.device.id, rec.op ?? "upsert");
|
|
42323
|
+
results.push({ id: rec.id, ...r });
|
|
42324
|
+
} catch (e) {
|
|
42325
|
+
req.log.warn({ entity: rec.entity, recordId: rec.id, err: e }, "sync push record ditolak");
|
|
42326
|
+
results.push({ id: rec.id, ok: false, error: e.message });
|
|
42327
|
+
}
|
|
41119
42328
|
}
|
|
41120
42329
|
return { results };
|
|
41121
42330
|
});
|
|
@@ -41232,7 +42441,7 @@ init_config2();
|
|
|
41232
42441
|
init_src2();
|
|
41233
42442
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
41234
42443
|
import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile4, unlink as unlink4, readdir as readdir5, stat as stat3 } from "node:fs/promises";
|
|
41235
|
-
import { join as
|
|
42444
|
+
import { join as join23, resolve as resolve19, basename as basename6 } from "node:path";
|
|
41236
42445
|
var MAX_TRANSCRIPT_BYTES = 1024 * 1024;
|
|
41237
42446
|
function transcriptDir() {
|
|
41238
42447
|
return resolve19(effectiveStr("HANOMAN_TRANSCRIPT_DIR")?.trim() || resolveDataDirs().transcripts);
|
|
@@ -41253,13 +42462,13 @@ async function saveTranscript(text) {
|
|
|
41253
42462
|
const dir2 = transcriptDir();
|
|
41254
42463
|
await mkdir5(dir2, { recursive: true, mode: 448 });
|
|
41255
42464
|
const key = `${randomUUID7()}.log`;
|
|
41256
|
-
await writeFile4(
|
|
42465
|
+
await writeFile4(join23(dir2, key), body, { encoding: "utf8", mode: 384 });
|
|
41257
42466
|
return { key, bytes: Buffer.byteLength(body, "utf8"), truncated };
|
|
41258
42467
|
}
|
|
41259
42468
|
async function readTranscript(key) {
|
|
41260
42469
|
if (!key) return null;
|
|
41261
42470
|
try {
|
|
41262
|
-
return await readFile4(
|
|
42471
|
+
return await readFile4(join23(transcriptDir(), basename6(key)), "utf8");
|
|
41263
42472
|
} catch {
|
|
41264
42473
|
return null;
|
|
41265
42474
|
}
|
|
@@ -41267,7 +42476,7 @@ async function readTranscript(key) {
|
|
|
41267
42476
|
async function deleteTranscript(key) {
|
|
41268
42477
|
if (!key) return;
|
|
41269
42478
|
try {
|
|
41270
|
-
await unlink4(
|
|
42479
|
+
await unlink4(join23(transcriptDir(), basename6(key)));
|
|
41271
42480
|
} catch (e) {
|
|
41272
42481
|
if (e.code !== "ENOENT") throw e;
|
|
41273
42482
|
}
|
|
@@ -41285,7 +42494,7 @@ async function listTranscripts() {
|
|
|
41285
42494
|
for (const name2 of names) {
|
|
41286
42495
|
if (!name2.endsWith(".log")) continue;
|
|
41287
42496
|
try {
|
|
41288
|
-
rows.push({ key: name2, mtimeMs: (await stat3(
|
|
42497
|
+
rows.push({ key: name2, mtimeMs: (await stat3(join23(dir2, name2))).mtimeMs });
|
|
41289
42498
|
} catch {
|
|
41290
42499
|
}
|
|
41291
42500
|
}
|
|
@@ -41598,9 +42807,9 @@ init_db();
|
|
|
41598
42807
|
init_db();
|
|
41599
42808
|
init_uploads();
|
|
41600
42809
|
init_src();
|
|
41601
|
-
import { createHash as
|
|
42810
|
+
import { createHash as createHash9, randomBytes as randomBytes6 } from "node:crypto";
|
|
41602
42811
|
function hashAccessKey(key) {
|
|
41603
|
-
return
|
|
42812
|
+
return createHash9("sha256").update(key).digest("hex");
|
|
41604
42813
|
}
|
|
41605
42814
|
function generateAccessKey() {
|
|
41606
42815
|
const key = "hnm_tkt_" + randomBytes6(24).toString("hex");
|
|
@@ -42162,11 +43371,11 @@ async function expireStale(now) {
|
|
|
42162
43371
|
const stale = await prisma.schedulerCronRun.findMany({
|
|
42163
43372
|
where: { status: "queued", dueAt: { lt: new Date(now - GRACE_MS) } }
|
|
42164
43373
|
});
|
|
42165
|
-
for (const
|
|
42166
|
-
const note =
|
|
42167
|
-
await prisma.schedulerCronRun.update({ where: { id:
|
|
42168
|
-
const cron = await prisma.schedulerCron.findUnique({ where: { id:
|
|
42169
|
-
await recordCronRun(
|
|
43374
|
+
for (const run3 of stale) {
|
|
43375
|
+
const note = run3.note ?? "tak terluncurkan sampai batas keterlambatan";
|
|
43376
|
+
await prisma.schedulerCronRun.update({ where: { id: run3.id }, data: { status: "skipped", note } });
|
|
43377
|
+
const cron = await prisma.schedulerCron.findUnique({ where: { id: run3.cronId } });
|
|
43378
|
+
await recordCronRun(run3.cronId, cron?.name ?? run3.cronId, run3.projectId, run3.dueAt, "skipped", note);
|
|
42170
43379
|
}
|
|
42171
43380
|
}
|
|
42172
43381
|
function queuedCronRuns() {
|
|
@@ -42403,10 +43612,10 @@ async function scheduler_default(app2) {
|
|
|
42403
43612
|
const pending2 = await prisma.schedulerCronRun.findFirst({ where: { cronId: id, status: "queued" } });
|
|
42404
43613
|
if (pending2) return reply.code(409).send({ error: "masih ada run yang menunggu dijalankan", runId: pending2.id });
|
|
42405
43614
|
try {
|
|
42406
|
-
const
|
|
43615
|
+
const run3 = await prisma.schedulerCronRun.create({
|
|
42407
43616
|
data: { cronId: id, projectId: cron.projectId, dueAt: /* @__PURE__ */ new Date(), manual: true }
|
|
42408
43617
|
});
|
|
42409
|
-
return reply.code(201).send(runView(
|
|
43618
|
+
return reply.code(201).send(runView(run3));
|
|
42410
43619
|
} catch {
|
|
42411
43620
|
return reply.code(409).send({ error: "run untuk jatuh tempo ini sudah ada" });
|
|
42412
43621
|
}
|
|
@@ -42554,8 +43763,8 @@ function leadPrompt(q, c) {
|
|
|
42554
43763
|
|
|
42555
43764
|
// src/services/lead/verdict.ts
|
|
42556
43765
|
init_src();
|
|
42557
|
-
import { existsSync as
|
|
42558
|
-
import { isAbsolute as
|
|
43766
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
43767
|
+
import { isAbsolute as isAbsolute6, resolve as resolvePath, relative as relative2 } from "node:path";
|
|
42559
43768
|
function extractJsonBlock(raw2) {
|
|
42560
43769
|
const re = /```json\s*\n([\s\S]*?)```/g;
|
|
42561
43770
|
let last = null;
|
|
@@ -42588,11 +43797,11 @@ function keepExistingRefs(refs2, repoDir) {
|
|
|
42588
43797
|
continue;
|
|
42589
43798
|
}
|
|
42590
43799
|
if (!repoDir) continue;
|
|
42591
|
-
if (
|
|
43800
|
+
if (isAbsolute6(ref)) continue;
|
|
42592
43801
|
const full = resolvePath(repoDir, ref);
|
|
42593
43802
|
const rel = relative2(repoDir, full);
|
|
42594
43803
|
if (rel.startsWith("..")) continue;
|
|
42595
|
-
if (
|
|
43804
|
+
if (existsSync15(full)) out4.push(ref);
|
|
42596
43805
|
}
|
|
42597
43806
|
return [...new Set(out4)];
|
|
42598
43807
|
}
|
|
@@ -42740,14 +43949,15 @@ function toDecisionView(r) {
|
|
|
42740
43949
|
}
|
|
42741
43950
|
|
|
42742
43951
|
// src/services/lead/brain.ts
|
|
43952
|
+
init_src2();
|
|
42743
43953
|
init_config2();
|
|
42744
43954
|
init_pty();
|
|
42745
43955
|
init_session_sandbox();
|
|
42746
|
-
import { execFile as
|
|
43956
|
+
import { execFile as execFile13 } from "node:child_process";
|
|
42747
43957
|
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
42748
|
-
import { mkdirSync as
|
|
43958
|
+
import { mkdirSync as mkdirSync11, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
42749
43959
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
42750
|
-
import { join as
|
|
43960
|
+
import { join as join24 } from "node:path";
|
|
42751
43961
|
var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
|
|
42752
43962
|
function leadArgv(o) {
|
|
42753
43963
|
if (o.agent === "codex") {
|
|
@@ -42772,15 +43982,15 @@ var shellQuote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
|
42772
43982
|
function leadProcess(prompt, o, env = process.env) {
|
|
42773
43983
|
const file = binFor(o.agent);
|
|
42774
43984
|
const directArgs = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
|
|
42775
|
-
const mode = env.HANOMAN_SESSION_SANDBOX ?? (env
|
|
43985
|
+
const mode = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
|
|
42776
43986
|
if (mode === "off") return { file, args: directArgs, cwd: o.cwd, cleanup: () => {
|
|
42777
43987
|
} };
|
|
42778
|
-
const promptDir =
|
|
42779
|
-
|
|
42780
|
-
const promptFile =
|
|
42781
|
-
|
|
42782
|
-
const workspace = o.cwd ??
|
|
42783
|
-
if (!o.cwd)
|
|
43988
|
+
const promptDir = join24(tmpdir4(), "hanoman-prompts");
|
|
43989
|
+
mkdirSync11(promptDir, { recursive: true, mode: 448 });
|
|
43990
|
+
const promptFile = join24(promptDir, `oneshot-${randomUUID9()}`);
|
|
43991
|
+
writeFileSync6(promptFile, prompt, { flag: "wx", mode: 384 });
|
|
43992
|
+
const workspace = o.cwd ?? join24(tmpdir4(), `hanoman-oneshot-${randomUUID9()}`);
|
|
43993
|
+
if (!o.cwd) mkdirSync11(workspace, { recursive: false, mode: 448 });
|
|
42784
43994
|
try {
|
|
42785
43995
|
const argsWithoutPrompt = directArgs.slice(0, -1);
|
|
42786
43996
|
const command = [file, ...argsWithoutPrompt].map(shellQuote).join(" ") + ` "$(cat ${shellQuote(promptFile)})"`;
|
|
@@ -42822,7 +44032,7 @@ function leadFailureReason(agent, timeoutMs, err, stdout, stderr) {
|
|
|
42822
44032
|
function think(prompt, o) {
|
|
42823
44033
|
const process2 = leadProcess(prompt, o);
|
|
42824
44034
|
return new Promise((resolve21, reject2) => {
|
|
42825
|
-
const child =
|
|
44035
|
+
const child = execFile13(process2.file, process2.args, {
|
|
42826
44036
|
cwd: process2.cwd,
|
|
42827
44037
|
timeout: o.timeoutMs,
|
|
42828
44038
|
maxBuffer: 16 * 1024 * 1024,
|
|
@@ -43385,7 +44595,7 @@ init_src();
|
|
|
43385
44595
|
init_pty();
|
|
43386
44596
|
init_tui_dialog();
|
|
43387
44597
|
init_notifications();
|
|
43388
|
-
import { writeFileSync as
|
|
44598
|
+
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
43389
44599
|
|
|
43390
44600
|
// src/services/lead/pane.ts
|
|
43391
44601
|
init_tui_dialog();
|
|
@@ -43471,7 +44681,7 @@ var prodDetectDeps = {
|
|
|
43471
44681
|
send: (id, text, choices) => sendToPane(id, text, 50, choices),
|
|
43472
44682
|
clearMarker: (file) => {
|
|
43473
44683
|
try {
|
|
43474
|
-
|
|
44684
|
+
writeFileSync7(file, "");
|
|
43475
44685
|
} catch {
|
|
43476
44686
|
}
|
|
43477
44687
|
},
|
|
@@ -44236,9 +45446,9 @@ init_db();
|
|
|
44236
45446
|
|
|
44237
45447
|
// src/services/changelog/collect.ts
|
|
44238
45448
|
init_db();
|
|
44239
|
-
import { execFile as
|
|
45449
|
+
import { execFile as execFile14 } from "node:child_process";
|
|
44240
45450
|
import { promisify as promisify13 } from "node:util";
|
|
44241
|
-
import { existsSync as
|
|
45451
|
+
import { existsSync as existsSync16 } from "node:fs";
|
|
44242
45452
|
|
|
44243
45453
|
// src/services/changelog/scrub.ts
|
|
44244
45454
|
var MERGE = /^merge\s+(branch|pull request|remote-tracking branch|tag)\b/i;
|
|
@@ -44299,13 +45509,13 @@ async function collectBacklog(projectId, from, to) {
|
|
|
44299
45509
|
notes.push(`${stampless} item selesai tanpa stempel waktu (selesai sebelum stempel ini ada) dan tak ikut dihitung.`);
|
|
44300
45510
|
return { ok: true, input: { mode: "backlog", title: `${from} \u2013 ${to}`, items, notes } };
|
|
44301
45511
|
}
|
|
44302
|
-
var exec10 = promisify13(
|
|
45512
|
+
var exec10 = promisify13(execFile14);
|
|
44303
45513
|
var GIT8 = { maxBuffer: 1 << 24, encoding: "utf8" };
|
|
44304
45514
|
var US2 = "";
|
|
44305
45515
|
var RS = "";
|
|
44306
45516
|
var NO_REPO = "project ini belum ditautkan ke repo di mesin ini";
|
|
44307
45517
|
var NO_TAG = "repo project ini belum punya tag rilis";
|
|
44308
|
-
var usable2 = (repoDir) => !!repoDir &&
|
|
45518
|
+
var usable2 = (repoDir) => !!repoDir && existsSync16(repoDir);
|
|
44309
45519
|
async function listTags(repoDir) {
|
|
44310
45520
|
if (!usable2(repoDir)) return { tags: [], head: null, reason: NO_REPO };
|
|
44311
45521
|
try {
|
|
@@ -44459,12 +45669,12 @@ async function generateChangelog(projectId, req, deps = {}) {
|
|
|
44459
45669
|
const input = got.input;
|
|
44460
45670
|
const prompt = changelogPrompt(input, CHANGELOG_TIMEOUT_MS);
|
|
44461
45671
|
const { agent, model, effort } = await changelogAgentDefaults();
|
|
44462
|
-
const
|
|
45672
|
+
const run3 = deps.think ?? think;
|
|
44463
45673
|
let body = "";
|
|
44464
45674
|
let generator = "agent";
|
|
44465
45675
|
const warnings = [...input.notes];
|
|
44466
45676
|
try {
|
|
44467
|
-
const raw2 = await
|
|
45677
|
+
const raw2 = await run3(prompt, { agent, model, effort, timeoutMs: CHANGELOG_TIMEOUT_MS });
|
|
44468
45678
|
body = scrubOutput(raw2 ?? "");
|
|
44469
45679
|
if (!body.trim()) throw new Error("agen tak memulangkan teks apa pun");
|
|
44470
45680
|
} catch (e) {
|
|
@@ -44566,13 +45776,13 @@ init_db();
|
|
|
44566
45776
|
|
|
44567
45777
|
// src/services/agent-tool-catalog.ts
|
|
44568
45778
|
init_src();
|
|
44569
|
-
import { readFileSync as
|
|
45779
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
44570
45780
|
import { homedir as homedir9 } from "node:os";
|
|
44571
|
-
import { join as
|
|
45781
|
+
import { join as join25 } from "node:path";
|
|
44572
45782
|
var home = () => process.env.HOME || homedir9();
|
|
44573
45783
|
var readJson2 = (path2) => {
|
|
44574
45784
|
try {
|
|
44575
|
-
return JSON.parse(
|
|
45785
|
+
return JSON.parse(readFileSync16(path2, "utf8"));
|
|
44576
45786
|
} catch {
|
|
44577
45787
|
return null;
|
|
44578
45788
|
}
|
|
@@ -44585,7 +45795,7 @@ var serversOf = (node) => {
|
|
|
44585
45795
|
var codexServers = () => {
|
|
44586
45796
|
let text;
|
|
44587
45797
|
try {
|
|
44588
|
-
text =
|
|
45798
|
+
text = readFileSync16(join25(home(), ".codex", "config.toml"), "utf8");
|
|
44589
45799
|
} catch {
|
|
44590
45800
|
return [];
|
|
44591
45801
|
}
|
|
@@ -44598,12 +45808,12 @@ var codexServers = () => {
|
|
|
44598
45808
|
};
|
|
44599
45809
|
function mcpServerNames(repoDir) {
|
|
44600
45810
|
const names = [];
|
|
44601
|
-
const claudeJson = readJson2(
|
|
45811
|
+
const claudeJson = readJson2(join25(home(), ".claude.json"));
|
|
44602
45812
|
names.push(...serversOf(claudeJson));
|
|
44603
45813
|
if (repoDir) {
|
|
44604
45814
|
const projects = claudeJson?.projects;
|
|
44605
45815
|
if (projects && typeof projects === "object") names.push(...serversOf(projects[repoDir]));
|
|
44606
|
-
names.push(...serversOf(readJson2(
|
|
45816
|
+
names.push(...serversOf(readJson2(join25(repoDir, ".mcp.json"))));
|
|
44607
45817
|
}
|
|
44608
45818
|
names.push(...codexServers());
|
|
44609
45819
|
return [...new Set(names.filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
|
@@ -44613,6 +45823,71 @@ function agentToolCatalog(repoDir) {
|
|
|
44613
45823
|
}
|
|
44614
45824
|
var agentToolIds = (repoDir) => agentToolCatalog(repoDir).map((t) => t.id);
|
|
44615
45825
|
|
|
45826
|
+
// src/routes/custom-agents.ts
|
|
45827
|
+
init_settings2();
|
|
45828
|
+
|
|
45829
|
+
// src/services/builtin-agents.ts
|
|
45830
|
+
init_src();
|
|
45831
|
+
init_db();
|
|
45832
|
+
init_settings2();
|
|
45833
|
+
init_tombstone();
|
|
45834
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
45835
|
+
var fingerprint = (name2, description, instructions, tools) => createHash10("sha256").update([name2, description, instructions, [...tools].join(",")].join(" ")).digest("hex").slice(0, 16);
|
|
45836
|
+
var builtinFingerprint = (a) => fingerprint(a.name, a.description, a.instructions, a.tools);
|
|
45837
|
+
var rowFingerprint = (r) => fingerprint(r.name, r.description, r.instructions, toolsOf(r.tools) ?? []);
|
|
45838
|
+
async function seedBuiltinAgents() {
|
|
45839
|
+
try {
|
|
45840
|
+
const setting = await getSetting();
|
|
45841
|
+
const stamps = { ...setting.builtinAgents };
|
|
45842
|
+
let changed = false;
|
|
45843
|
+
for (const a of BUILTIN_AGENTS) {
|
|
45844
|
+
const id = customAgentId(null, a.name);
|
|
45845
|
+
const fp = builtinFingerprint(a);
|
|
45846
|
+
const row = await prisma.customAgent.findUnique({ where: { id } });
|
|
45847
|
+
if (!row) {
|
|
45848
|
+
if (await findTombstone("customAgent", id)) continue;
|
|
45849
|
+
await prisma.customAgent.create({ data: {
|
|
45850
|
+
id,
|
|
45851
|
+
projectId: null,
|
|
45852
|
+
name: a.name,
|
|
45853
|
+
description: a.description,
|
|
45854
|
+
instructions: a.instructions,
|
|
45855
|
+
tools: [...a.tools],
|
|
45856
|
+
model: null,
|
|
45857
|
+
mentions: [],
|
|
45858
|
+
runtime: null,
|
|
45859
|
+
enabled: a.enabledByDefault
|
|
45860
|
+
} });
|
|
45861
|
+
await notifySynced("customAgent", id);
|
|
45862
|
+
stamps[a.name] = fp;
|
|
45863
|
+
changed = true;
|
|
45864
|
+
continue;
|
|
45865
|
+
}
|
|
45866
|
+
const stamped = stamps[a.name];
|
|
45867
|
+
if (!stamped || stamped === fp) continue;
|
|
45868
|
+
if (stamped !== rowFingerprint(row)) continue;
|
|
45869
|
+
await prisma.customAgent.update({ where: { id }, data: {
|
|
45870
|
+
description: a.description,
|
|
45871
|
+
instructions: a.instructions,
|
|
45872
|
+
tools: [...a.tools]
|
|
45873
|
+
// `enabled` TIDAK di sini. Sengaja.
|
|
45874
|
+
} });
|
|
45875
|
+
await notifySynced("customAgent", id);
|
|
45876
|
+
stamps[a.name] = fp;
|
|
45877
|
+
changed = true;
|
|
45878
|
+
}
|
|
45879
|
+
if (changed) {
|
|
45880
|
+
const data = { ...setting, builtinAgents: stamps };
|
|
45881
|
+
await prisma.setting.upsert({
|
|
45882
|
+
where: { id: 1 },
|
|
45883
|
+
update: { data },
|
|
45884
|
+
create: { id: 1, data }
|
|
45885
|
+
});
|
|
45886
|
+
}
|
|
45887
|
+
} catch {
|
|
45888
|
+
}
|
|
45889
|
+
}
|
|
45890
|
+
|
|
44616
45891
|
// src/services/custom-agents.ts
|
|
44617
45892
|
init_db();
|
|
44618
45893
|
init_src();
|
|
@@ -44684,25 +45959,35 @@ function unknownMentions(row, all) {
|
|
|
44684
45959
|
return mentionsOf(row.mentions).filter((m) => !visible.has(m));
|
|
44685
45960
|
}
|
|
44686
45961
|
async function installCustomAgents() {
|
|
45962
|
+
await seedBuiltinAgents();
|
|
44687
45963
|
await loadCustomAgents();
|
|
44688
45964
|
registerCustomAgentSource((projectId, agent) => agentDefsFor(projectId, agent));
|
|
44689
45965
|
}
|
|
44690
45966
|
|
|
44691
45967
|
// src/routes/custom-agents.ts
|
|
44692
45968
|
var rowsOf = async () => await prisma.customAgent.findMany();
|
|
44693
|
-
var
|
|
44694
|
-
|
|
44695
|
-
projectId
|
|
44696
|
-
|
|
44697
|
-
|
|
44698
|
-
|
|
44699
|
-
|
|
44700
|
-
|
|
44701
|
-
|
|
44702
|
-
|
|
44703
|
-
|
|
44704
|
-
|
|
44705
|
-
|
|
45969
|
+
var stampsOf = async () => (await getSetting()).builtinAgents;
|
|
45970
|
+
var view9 = (r, projectId, stamps = {}) => {
|
|
45971
|
+
const builtin = r.projectId === null && BUILTIN_AGENT_NAMES.includes(r.name);
|
|
45972
|
+
return {
|
|
45973
|
+
id: r.id,
|
|
45974
|
+
projectId: r.projectId,
|
|
45975
|
+
name: r.name,
|
|
45976
|
+
description: r.description,
|
|
45977
|
+
instructions: r.instructions,
|
|
45978
|
+
tools: toolsOf(r.tools),
|
|
45979
|
+
model: r.model,
|
|
45980
|
+
mentions: mentionsOf(r.mentions),
|
|
45981
|
+
runtime: runtimeOf(r.runtime),
|
|
45982
|
+
enabled: r.enabled,
|
|
45983
|
+
builtin,
|
|
45984
|
+
// Sidik jari yang tak tercatat (baris menyeberang sync dari mesin lain, seed di sini belum
|
|
45985
|
+
// pernah menyentuhnya) dibaca sebagai "disunting" — lebih baik menandai berlebih daripada
|
|
45986
|
+
// menjanjikan "asli bawaan" untuk isi yang tak bisa kita buktikan.
|
|
45987
|
+
builtinEdited: builtin ? stamps[r.name] !== rowFingerprint(r) : false,
|
|
45988
|
+
...projectId ? { inherited: r.projectId === null } : {}
|
|
45989
|
+
};
|
|
45990
|
+
};
|
|
44706
45991
|
var repoDirOf2 = async (projectId) => projectId ? await resolveRepoDir(projectId) : null;
|
|
44707
45992
|
function toolsProblem(tools, catalogIds) {
|
|
44708
45993
|
if (!tools || tools.length === 0) return null;
|
|
@@ -44735,7 +46020,8 @@ async function custom_agents_default(app2) {
|
|
|
44735
46020
|
const byName = /* @__PURE__ */ new Map();
|
|
44736
46021
|
for (const r of rows) if (r.projectId === null) byName.set(r.name, r);
|
|
44737
46022
|
for (const r of rows) if (r.projectId !== null) byName.set(r.name, r);
|
|
44738
|
-
|
|
46023
|
+
const stamps = await stampsOf();
|
|
46024
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view9(r, projectId, stamps));
|
|
44739
46025
|
});
|
|
44740
46026
|
app2.post("/custom-agents", async (req, reply) => {
|
|
44741
46027
|
const parsed = zCreateCustomAgent.safeParse(req.body);
|
|
@@ -44782,7 +46068,7 @@ async function custom_agents_default(app2) {
|
|
|
44782
46068
|
} });
|
|
44783
46069
|
await loadCustomAgents();
|
|
44784
46070
|
await notifySynced("customAgent", id);
|
|
44785
|
-
return reply.code(201).send(view9(row));
|
|
46071
|
+
return reply.code(201).send(view9(row, void 0, await stampsOf()));
|
|
44786
46072
|
});
|
|
44787
46073
|
app2.patch("/custom-agents/:id", async (req, reply) => {
|
|
44788
46074
|
const { id } = req.params;
|
|
@@ -44829,7 +46115,7 @@ async function custom_agents_default(app2) {
|
|
|
44829
46115
|
} });
|
|
44830
46116
|
await loadCustomAgents();
|
|
44831
46117
|
await notifySynced("customAgent", id);
|
|
44832
|
-
return view9(row);
|
|
46118
|
+
return view9(row, void 0, await stampsOf());
|
|
44833
46119
|
});
|
|
44834
46120
|
app2.delete("/custom-agents/:id", async (req, reply) => {
|
|
44835
46121
|
const { id } = req.params;
|
|
@@ -44902,7 +46188,7 @@ async function resolveGithubRepo(projectId) {
|
|
|
44902
46188
|
|
|
44903
46189
|
// src/services/github-fetch.ts
|
|
44904
46190
|
init_config2();
|
|
44905
|
-
import { execFile as
|
|
46191
|
+
import { execFile as execFile15 } from "node:child_process";
|
|
44906
46192
|
var labelNames = (l) => (l ?? []).map((x) => typeof x === "string" ? x : x.name ?? "").filter(Boolean);
|
|
44907
46193
|
var norm2 = (s2) => s2.toLowerCase() === "closed" ? "closed" : "open";
|
|
44908
46194
|
function issueFromGh(raw2) {
|
|
@@ -44943,7 +46229,7 @@ function issuesFromRest(raw2) {
|
|
|
44943
46229
|
var GH_FIELDS = "number,title,body,author,labels,url,state,createdAt,updatedAt";
|
|
44944
46230
|
var API2 = "https://api.github.com";
|
|
44945
46231
|
var defaultRunGh = (args, env) => new Promise((resolve21, reject2) => {
|
|
44946
|
-
|
|
46232
|
+
execFile15(
|
|
44947
46233
|
args[0],
|
|
44948
46234
|
args.slice(1),
|
|
44949
46235
|
{ env, maxBuffer: 1 << 26, encoding: "utf8", timeout: 6e4 },
|
|
@@ -45289,22 +46575,22 @@ async function verifyTelegramAgentToken(token, deps = {}) {
|
|
|
45289
46575
|
return missing.length ? { ok: false, missing, reason: `AgentToken kurang ${missing.length} capability.` } : { ok: true };
|
|
45290
46576
|
}
|
|
45291
46577
|
async function telegramInboundReadiness(deps = {}) {
|
|
45292
|
-
const
|
|
46578
|
+
const polling2 = deps.polling ?? telegramRuntimeStatus().running;
|
|
45293
46579
|
const empty = [];
|
|
45294
46580
|
const token = deps.agentToken ?? effectiveStr("HANOMAN_TELEGRAM_AGENT_TOKEN")?.trim();
|
|
45295
46581
|
if (!token) {
|
|
45296
|
-
return { ok: false, reason: "AgentToken gateway belum diisi.", missingCapabilities: [...TELEGRAM_REQUIRED_CAPABILITIES], polling };
|
|
46582
|
+
return { ok: false, reason: "AgentToken gateway belum diisi.", missingCapabilities: [...TELEGRAM_REQUIRED_CAPABILITIES], polling: polling2 };
|
|
45297
46583
|
}
|
|
45298
46584
|
const setting = await (deps.getSetting ?? getSetting)();
|
|
45299
46585
|
if (!setting.agentAccessEnabled) {
|
|
45300
|
-
return { ok: false, reason: "Akses agent mati \u2014 nyalakan master switch di Akses AI Agent.", missingCapabilities: empty, polling };
|
|
46586
|
+
return { ok: false, reason: "Akses agent mati \u2014 nyalakan master switch di Akses AI Agent.", missingCapabilities: empty, polling: polling2 };
|
|
45301
46587
|
}
|
|
45302
46588
|
const gate = await verifyTelegramAgentToken(token, deps);
|
|
45303
|
-
if (!gate.ok) return { ok: false, reason: gate.reason, missingCapabilities: gate.missing, polling };
|
|
45304
|
-
if (!
|
|
45305
|
-
return { ok: false, reason: "Kredensial sudah sah tapi gateway belum polling \u2014 nyalakan \u201CGateway aktif\u201D.", missingCapabilities: empty, polling };
|
|
46589
|
+
if (!gate.ok) return { ok: false, reason: gate.reason, missingCapabilities: gate.missing, polling: polling2 };
|
|
46590
|
+
if (!polling2) {
|
|
46591
|
+
return { ok: false, reason: "Kredensial sudah sah tapi gateway belum polling \u2014 nyalakan \u201CGateway aktif\u201D.", missingCapabilities: empty, polling: polling2 };
|
|
45306
46592
|
}
|
|
45307
|
-
return { ok: true, reason: null, missingCapabilities: empty, polling };
|
|
46593
|
+
return { ok: true, reason: null, missingCapabilities: empty, polling: polling2 };
|
|
45308
46594
|
}
|
|
45309
46595
|
var KEYS = TELEGRAM_CONFIG_KEYS;
|
|
45310
46596
|
function telegramCredentialView() {
|
|
@@ -45975,7 +47261,7 @@ init_settings2();
|
|
|
45975
47261
|
// src/services/portal-chat/turn.ts
|
|
45976
47262
|
init_src();
|
|
45977
47263
|
init_db();
|
|
45978
|
-
import { execFile as
|
|
47264
|
+
import { execFile as execFile16 } from "node:child_process";
|
|
45979
47265
|
|
|
45980
47266
|
// src/services/portal-chat/guard-input.ts
|
|
45981
47267
|
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
@@ -46087,9 +47373,9 @@ ${baru}`;
|
|
|
46087
47373
|
// src/services/portal-chat/workspace.ts
|
|
46088
47374
|
init_src();
|
|
46089
47375
|
init_db();
|
|
46090
|
-
import { mkdirSync as
|
|
47376
|
+
import { mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync2, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
46091
47377
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
46092
|
-
import { join as
|
|
47378
|
+
import { join as join26 } from "node:path";
|
|
46093
47379
|
var STAGE_LABEL = {
|
|
46094
47380
|
brainstorming: "Dirumuskan",
|
|
46095
47381
|
objective: "Dirumuskan",
|
|
@@ -46144,7 +47430,7 @@ ${c.body}
|
|
|
46144
47430
|
${baris.join("\n")}`;
|
|
46145
47431
|
}
|
|
46146
47432
|
async function buildChatWorkspace(projectId) {
|
|
46147
|
-
const dir2 = mkdtempSync2(
|
|
47433
|
+
const dir2 = mkdtempSync2(join26(tmpdir5(), "hanoman-portal-chat-"));
|
|
46148
47434
|
try {
|
|
46149
47435
|
const project = await prisma.project.findUnique({
|
|
46150
47436
|
where: { id: projectId },
|
|
@@ -46165,7 +47451,7 @@ async function buildChatWorkspace(projectId) {
|
|
|
46165
47451
|
});
|
|
46166
47452
|
const files = [];
|
|
46167
47453
|
const tulis = (rel, isi) => {
|
|
46168
|
-
|
|
47454
|
+
writeFileSync8(join26(dir2, rel), isi, { mode: 384 });
|
|
46169
47455
|
files.push(rel);
|
|
46170
47456
|
};
|
|
46171
47457
|
tulis("project.md", renderProjectDoc(project));
|
|
@@ -46174,11 +47460,11 @@ async function buildChatWorkspace(projectId) {
|
|
|
46174
47460
|
tulis("catatan-rilis.md", renderChangelogDoc(changelogs));
|
|
46175
47461
|
const prds = await listPrds(projectId);
|
|
46176
47462
|
if (prds.length) {
|
|
46177
|
-
|
|
47463
|
+
mkdirSync12(join26(dir2, "dokumen"), { mode: 448 });
|
|
46178
47464
|
for (const prd of prds) {
|
|
46179
47465
|
const isi = await readPrd(projectId, prd.path);
|
|
46180
47466
|
if (!isi) continue;
|
|
46181
|
-
tulis(
|
|
47467
|
+
tulis(join26("dokumen", `${prd.slug.replaceAll("/", "-")}.md`), isi);
|
|
46182
47468
|
}
|
|
46183
47469
|
}
|
|
46184
47470
|
return { dir: dir2, files, cleanup: () => rmSync7(dir2, { recursive: true, force: true }) };
|
|
@@ -46190,6 +47476,7 @@ async function buildChatWorkspace(projectId) {
|
|
|
46190
47476
|
|
|
46191
47477
|
// src/services/portal-chat/argv.ts
|
|
46192
47478
|
init_src();
|
|
47479
|
+
init_src2();
|
|
46193
47480
|
init_config2();
|
|
46194
47481
|
init_session_sandbox();
|
|
46195
47482
|
var PORTAL_CHAT_TOOLS = "Read,Glob,Grep";
|
|
@@ -46220,10 +47507,10 @@ var shellQuote2 = (v) => `'${v.replace(/'/g, `'"'"'`)}'`;
|
|
|
46220
47507
|
function portalChatProcess(o, env = process.env) {
|
|
46221
47508
|
const file = effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
|
|
46222
47509
|
const args = portalChatArgv(o);
|
|
46223
|
-
const mode = env.HANOMAN_SESSION_SANDBOX ?? (env
|
|
47510
|
+
const mode = env.HANOMAN_SESSION_SANDBOX ?? (resolveHardening(env) ? "required" : "off");
|
|
46224
47511
|
if (mode === "off") {
|
|
46225
|
-
if (env
|
|
46226
|
-
throw new Error("chat portal menolak jalan: sandbox sesi wajib
|
|
47512
|
+
if (resolveHardening(env))
|
|
47513
|
+
throw new Error("chat portal menolak jalan: sandbox sesi wajib saat hardening menyala");
|
|
46227
47514
|
return { file, args, cwd: o.workspace };
|
|
46228
47515
|
}
|
|
46229
47516
|
const command = [file, ...args].map(shellQuote2).join(" ");
|
|
@@ -46261,7 +47548,7 @@ function chatFailureReason(err, stdout, stderr, timeoutMs) {
|
|
|
46261
47548
|
}
|
|
46262
47549
|
function runProcess(p, timeoutMs) {
|
|
46263
47550
|
return new Promise((resolve21, reject2) => {
|
|
46264
|
-
const child =
|
|
47551
|
+
const child = execFile16(p.file, p.args, {
|
|
46265
47552
|
cwd: p.cwd,
|
|
46266
47553
|
timeout: timeoutMs,
|
|
46267
47554
|
maxBuffer: 16 * 1024 * 1024,
|
|
@@ -46688,15 +47975,15 @@ init_src();
|
|
|
46688
47975
|
init_db();
|
|
46689
47976
|
|
|
46690
47977
|
// src/services/bootstrap.ts
|
|
46691
|
-
import { createHash as
|
|
47978
|
+
import { createHash as createHash11, randomBytes as randomBytes10, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
46692
47979
|
import { chmod, lstat as lstat3, mkdir as mkdir6, readFile as readFile5, unlink as unlink5, writeFile as writeFile5 } from "node:fs/promises";
|
|
46693
|
-
import { join as
|
|
47980
|
+
import { join as join27 } from "node:path";
|
|
46694
47981
|
var TTL_MS5 = 15 * 6e4;
|
|
46695
47982
|
var SETUP_TOKEN_FILE = "setup.token";
|
|
46696
47983
|
var BootstrapError = class extends Error {
|
|
46697
47984
|
code = "BOOTSTRAP_PROOF";
|
|
46698
47985
|
};
|
|
46699
|
-
var tokenPath = (home3) =>
|
|
47986
|
+
var tokenPath = (home3) => join27(home3, SETUP_TOKEN_FILE);
|
|
46700
47987
|
async function readStored(home3) {
|
|
46701
47988
|
try {
|
|
46702
47989
|
const info = await lstat3(tokenPath(home3));
|
|
@@ -46741,8 +48028,8 @@ ${new Date(expiresAt).toISOString()}
|
|
|
46741
48028
|
}
|
|
46742
48029
|
async function verifySetupToken(candidate, home3, now = Date.now()) {
|
|
46743
48030
|
const stored = await readStored(home3);
|
|
46744
|
-
const got =
|
|
46745
|
-
const want =
|
|
48031
|
+
const got = createHash11("sha256").update(candidate).digest();
|
|
48032
|
+
const want = createHash11("sha256").update(stored.token).digest();
|
|
46746
48033
|
if (stored.expiresAt <= now || !timingSafeEqual3(got, want)) throw new BootstrapError("invalid setup proof");
|
|
46747
48034
|
}
|
|
46748
48035
|
async function consumeSetupToken(home3) {
|
|
@@ -46754,9 +48041,9 @@ async function consumeSetupToken(home3) {
|
|
|
46754
48041
|
// src/routes/auth.ts
|
|
46755
48042
|
var BOOTSTRAP_USER_ID = "bootstrap-admin";
|
|
46756
48043
|
var view12 = (u) => ({ id: u.id, email: u.email, role: u.role, createdAt: u.createdAt.toISOString() });
|
|
46757
|
-
async function issue(reply, userId) {
|
|
48044
|
+
async function issue(req, reply, userId) {
|
|
46758
48045
|
const token = await createSession2(userId);
|
|
46759
|
-
reply.setCookie(COOKIE_NAME, token, cookieOpts());
|
|
48046
|
+
reply.setCookie(COOKIE_NAME, token, cookieOpts(req));
|
|
46760
48047
|
}
|
|
46761
48048
|
async function auth_default(app2, opts) {
|
|
46762
48049
|
const setupAttempts = new BoundedRateLimiter({ windowMs: 6e4, limit: 10, maxKeys: 4096 });
|
|
@@ -46792,7 +48079,7 @@ async function auth_default(app2, opts) {
|
|
|
46792
48079
|
}
|
|
46793
48080
|
if (opts.bootstrapRequired) await consumeSetupToken(opts.home);
|
|
46794
48081
|
setupAttempts.clear(req.ip);
|
|
46795
|
-
await issue(reply, user.id);
|
|
48082
|
+
await issue(req, reply, user.id);
|
|
46796
48083
|
return { user: view12(user) };
|
|
46797
48084
|
});
|
|
46798
48085
|
app2.post("/auth/login", async (req, reply) => {
|
|
@@ -46805,7 +48092,7 @@ async function auth_default(app2, opts) {
|
|
|
46805
48092
|
return reply.code(401).send({ error: "email atau password salah" });
|
|
46806
48093
|
}
|
|
46807
48094
|
clearLoginFails(req.ip);
|
|
46808
|
-
await issue(reply, user.id);
|
|
48095
|
+
await issue(req, reply, user.id);
|
|
46809
48096
|
return { user: view12(user) };
|
|
46810
48097
|
});
|
|
46811
48098
|
app2.post("/auth/logout", async (req, reply) => {
|
|
@@ -46847,11 +48134,93 @@ async function auth_default(app2, opts) {
|
|
|
46847
48134
|
data: { passwordHash: await hashPassword(p.data.newPassword) }
|
|
46848
48135
|
});
|
|
46849
48136
|
await deleteUserSessions(user.id);
|
|
46850
|
-
await issue(reply, user.id);
|
|
48137
|
+
await issue(req, reply, user.id);
|
|
46851
48138
|
return { user: view12(user) };
|
|
46852
48139
|
});
|
|
46853
48140
|
}
|
|
46854
48141
|
|
|
48142
|
+
// src/routes/setup.ts
|
|
48143
|
+
init_src2();
|
|
48144
|
+
init_src();
|
|
48145
|
+
init_db();
|
|
48146
|
+
|
|
48147
|
+
// src/services/setup-config.ts
|
|
48148
|
+
init_src2();
|
|
48149
|
+
var SETUP_ALLOWED_KEYS = [
|
|
48150
|
+
"HANOMAN_DEPLOYMENT",
|
|
48151
|
+
"HANOMAN_HARDENING",
|
|
48152
|
+
"HANOMAN_SETUP_DONE",
|
|
48153
|
+
"HANOMAN_SESSION_SANDBOX",
|
|
48154
|
+
"HANOMAN_SESSION_NETWORK",
|
|
48155
|
+
"HANOMAN_EGRESS_PROXY",
|
|
48156
|
+
"HANOMAN_AGENT_CREDENTIAL_DIR",
|
|
48157
|
+
"HANOMAN_CONTROL_ORIGINS",
|
|
48158
|
+
"HANOMAN_PUBLIC_ORIGINS",
|
|
48159
|
+
"HANOMAN_SINGLE_ORIGIN",
|
|
48160
|
+
"HANOMAN_TRUST_PROXY",
|
|
48161
|
+
"HANOMAN_UPLOAD_SCANNER"
|
|
48162
|
+
];
|
|
48163
|
+
function setupDone(home3) {
|
|
48164
|
+
return readConfigEnv(home3).HANOMAN_SETUP_DONE === "1";
|
|
48165
|
+
}
|
|
48166
|
+
function hardeningLocked(home3, env) {
|
|
48167
|
+
return resolveHardening(env) && !resolveHardening(readConfigEnv(home3));
|
|
48168
|
+
}
|
|
48169
|
+
function applySetup(home3, input) {
|
|
48170
|
+
const existing = readConfigEnv(home3);
|
|
48171
|
+
const next = {};
|
|
48172
|
+
for (const key of SETUP_ALLOWED_KEYS) if (existing[key]) next[key] = existing[key];
|
|
48173
|
+
next.HANOMAN_DEPLOYMENT = input.deployment;
|
|
48174
|
+
next.HANOMAN_SETUP_DONE = "1";
|
|
48175
|
+
if (input.hardening) next.HANOMAN_HARDENING = "1";
|
|
48176
|
+
else delete next.HANOMAN_HARDENING;
|
|
48177
|
+
writeConfigEnv(home3, next);
|
|
48178
|
+
return next;
|
|
48179
|
+
}
|
|
48180
|
+
|
|
48181
|
+
// src/services/restart.ts
|
|
48182
|
+
init_src();
|
|
48183
|
+
function requestConfigRestart(delayMs = 50) {
|
|
48184
|
+
setTimeout(() => process.exit(CONFIG_RESTART_EXIT), delayMs);
|
|
48185
|
+
}
|
|
48186
|
+
|
|
48187
|
+
// src/routes/setup.ts
|
|
48188
|
+
async function setup_default(app2, opts) {
|
|
48189
|
+
const attempts = new BoundedRateLimiter({ windowMs: 6e4, limit: 10, maxKeys: 4096 });
|
|
48190
|
+
const env = opts.env;
|
|
48191
|
+
const status = async () => ({
|
|
48192
|
+
// Dua syarat, bukan satu: belum ada user DAN wizard belum pernah dijawab.
|
|
48193
|
+
needed: await prisma.user.count() === 0 && !setupDone(opts.home),
|
|
48194
|
+
deployment: resolveDeployment(env),
|
|
48195
|
+
hardening: resolveHardening(env),
|
|
48196
|
+
hardeningLocked: hardeningLocked(opts.home, env),
|
|
48197
|
+
supervised: env.HANOMAN_SUPERVISOR === "1",
|
|
48198
|
+
setupTokenRequired: resolveHardening(env),
|
|
48199
|
+
prerequisites: prerequisites(env, collectProbeFacts(env))
|
|
48200
|
+
});
|
|
48201
|
+
app2.get("/setup/status", async () => status());
|
|
48202
|
+
app2.post("/setup", async (req, reply) => {
|
|
48203
|
+
if (attempts.hit(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
|
|
48204
|
+
const p = zSetupApply.safeParse(req.body);
|
|
48205
|
+
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
48206
|
+
if (hardeningLocked(opts.home, env) && !p.data.hardening)
|
|
48207
|
+
return reply.code(409).send({ error: "hardening-locked" });
|
|
48208
|
+
if (p.data.hardening) {
|
|
48209
|
+
const rows = prerequisites(env, collectProbeFacts(env));
|
|
48210
|
+
if (!allReady(rows))
|
|
48211
|
+
return reply.code(400).send({
|
|
48212
|
+
error: "prerequisites-missing",
|
|
48213
|
+
missing: rows.filter((r) => !r.ok).map((r) => r.id)
|
|
48214
|
+
});
|
|
48215
|
+
}
|
|
48216
|
+
applySetup(opts.home, { deployment: p.data.deployment, hardening: p.data.hardening });
|
|
48217
|
+
attempts.clear(req.ip);
|
|
48218
|
+
if (env.HANOMAN_SUPERVISOR !== "1") return { restart: "manual" };
|
|
48219
|
+
reply.raw.on("finish", () => requestConfigRestart());
|
|
48220
|
+
return { restart: "self" };
|
|
48221
|
+
});
|
|
48222
|
+
}
|
|
48223
|
+
|
|
46855
48224
|
// src/routes/agent-tokens.ts
|
|
46856
48225
|
init_src();
|
|
46857
48226
|
init_agent_token();
|
|
@@ -47138,6 +48507,7 @@ function trustProxyFromEnv(env) {
|
|
|
47138
48507
|
|
|
47139
48508
|
// src/app.ts
|
|
47140
48509
|
init_src2();
|
|
48510
|
+
init_db();
|
|
47141
48511
|
var PUBLIC = /* @__PURE__ */ new Set([
|
|
47142
48512
|
"GET /api/health",
|
|
47143
48513
|
"GET /api/auth/status",
|
|
@@ -47149,8 +48519,12 @@ var PUBLIC = /* @__PURE__ */ new Set([
|
|
|
47149
48519
|
// justru menjelaskan arti 403 itu.
|
|
47150
48520
|
"GET /api/agent-integration.md"
|
|
47151
48521
|
]);
|
|
48522
|
+
var SETUP_PUBLIC = /* @__PURE__ */ new Set([
|
|
48523
|
+
"GET /api/setup/status",
|
|
48524
|
+
"POST /api/setup"
|
|
48525
|
+
]);
|
|
47152
48526
|
function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {}) {
|
|
47153
|
-
const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname13(
|
|
48527
|
+
const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname13(fileURLToPath5(import.meta.url)), env, existsSync17);
|
|
47154
48528
|
const ingress = loadIngressPolicy(env);
|
|
47155
48529
|
const app2 = Fastify({ logger: false, trustProxy: trustProxyFromEnv(env) });
|
|
47156
48530
|
app2.addHook("onRequest", async (req, reply) => {
|
|
@@ -47191,6 +48565,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
47191
48565
|
if (user) req.user = user;
|
|
47192
48566
|
const path2 = req.url.split("?")[0] ?? req.url;
|
|
47193
48567
|
if (PUBLIC.has(`${req.method} ${path2}`)) return;
|
|
48568
|
+
if (SETUP_PUBLIC.has(`${req.method} ${path2}`) && await prisma.user.count() === 0) return;
|
|
47194
48569
|
if (user?.role === "client" && !clientRouteAllowed(req.method, path2))
|
|
47195
48570
|
return reply.code(403).send({ error: "portal klien: baca-saja" });
|
|
47196
48571
|
if (path2.startsWith("/api/sync") && path2 !== "/api/sync/now" && path2 !== "/api/sync/pending" && !path2.startsWith("/api/sync/conflicts")) return;
|
|
@@ -47216,7 +48591,8 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
47216
48591
|
setActor(actorFromRequest({ user: req.user ?? null, agent: req.agent ?? null }));
|
|
47217
48592
|
});
|
|
47218
48593
|
api.addHook("onResponse", auditTelegramGatewayResponse);
|
|
47219
|
-
await api.register(auth_default, { bootstrapRequired: env
|
|
48594
|
+
await api.register(auth_default, { bootstrapRequired: resolveHardening(env), home: resolveHome(env) });
|
|
48595
|
+
await api.register(setup_default, { home: resolveHome(env), env });
|
|
47220
48596
|
await api.register(health_default);
|
|
47221
48597
|
await api.register(agent_doc_default, { file: docFile });
|
|
47222
48598
|
await api.register(projects_default);
|
|
@@ -47258,7 +48634,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
47258
48634
|
await api.register(client_accounts_default);
|
|
47259
48635
|
}, { prefix: "/api" });
|
|
47260
48636
|
if (shouldServeWeb(env)) {
|
|
47261
|
-
const dist = pickWebDir(dirname13(
|
|
48637
|
+
const dist = pickWebDir(dirname13(fileURLToPath5(import.meta.url)), env, existsSync17);
|
|
47262
48638
|
if (dist) {
|
|
47263
48639
|
app2.register(fastifyStatic, { root: dist });
|
|
47264
48640
|
app2.setNotFoundHandler((req, reply) => req.url.startsWith("/api") ? reply.code(404).send({ error: "not found" }) : reply.sendFile("index.html"));
|
|
@@ -47317,12 +48693,12 @@ var cronLiveNote = (sessionId2) => `sesi cron sebelumnya masih berjalan (${sessi
|
|
|
47317
48693
|
var CRON_GONE_NOTE = "cron sudah dihapus atau dinonaktifkan";
|
|
47318
48694
|
var CRON_OPTOUT_NOTE = "project belum di-opt-in scheduler";
|
|
47319
48695
|
async function drainCronRuns(slots, deps) {
|
|
47320
|
-
for (const
|
|
47321
|
-
const cron = await prisma.schedulerCron.findUnique({ where: { id:
|
|
48696
|
+
for (const run3 of await queuedCronRuns()) {
|
|
48697
|
+
const cron = await prisma.schedulerCron.findUnique({ where: { id: run3.cronId } });
|
|
47322
48698
|
const close = async (status, note) => {
|
|
47323
|
-
if (status === "skipped") await markCronSkipped(
|
|
47324
|
-
else await markCronFailed(
|
|
47325
|
-
await recordCronRun(
|
|
48699
|
+
if (status === "skipped") await markCronSkipped(run3.id, note);
|
|
48700
|
+
else await markCronFailed(run3.id, note);
|
|
48701
|
+
await recordCronRun(run3.cronId, cron?.name ?? run3.cronId, run3.projectId, run3.dueAt, status, note);
|
|
47326
48702
|
};
|
|
47327
48703
|
if (!cron || !cron.enabled) {
|
|
47328
48704
|
await close("skipped", CRON_GONE_NOTE);
|
|
@@ -47342,7 +48718,7 @@ async function drainCronRuns(slots, deps) {
|
|
|
47342
48718
|
continue;
|
|
47343
48719
|
}
|
|
47344
48720
|
if (slots <= 0) {
|
|
47345
|
-
await noteCronRun(
|
|
48721
|
+
await noteCronRun(run3.id, CAP_FULL_NOTE);
|
|
47346
48722
|
continue;
|
|
47347
48723
|
}
|
|
47348
48724
|
try {
|
|
@@ -47355,8 +48731,8 @@ async function drainCronRuns(slots, deps) {
|
|
|
47355
48731
|
model: cron.model,
|
|
47356
48732
|
effort: cron.effort
|
|
47357
48733
|
});
|
|
47358
|
-
if (await markCronLaunched(
|
|
47359
|
-
await recordCronRun(cron.id, cron.name, cron.projectId,
|
|
48734
|
+
if (await markCronLaunched(run3.id, sessionId2))
|
|
48735
|
+
await recordCronRun(cron.id, cron.name, cron.projectId, run3.dueAt, "launched", null);
|
|
47360
48736
|
await prisma.schedulerCron.update({ where: { id: cron.id }, data: { lastRunAt: /* @__PURE__ */ new Date() } });
|
|
47361
48737
|
slots--;
|
|
47362
48738
|
} catch (e) {
|
|
@@ -47881,13 +49257,13 @@ function startWebhookEngine() {
|
|
|
47881
49257
|
// src/services/auto-merge.ts
|
|
47882
49258
|
init_src();
|
|
47883
49259
|
init_db();
|
|
47884
|
-
import { execFile as
|
|
49260
|
+
import { execFile as execFile17 } from "node:child_process";
|
|
47885
49261
|
import { promisify as promisify14 } from "node:util";
|
|
47886
49262
|
init_notifications();
|
|
47887
49263
|
var AUTO_MERGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
47888
49264
|
var AUTO_MERGE_GRACE_MS = 15 * 60 * 1e3;
|
|
47889
49265
|
var TICK_MS5 = 6e4;
|
|
47890
|
-
var exec11 = promisify14(
|
|
49266
|
+
var exec11 = promisify14(execFile17);
|
|
47891
49267
|
var GIT9 = { timeout: 3e4, maxBuffer: 1 << 24, encoding: "utf8" };
|
|
47892
49268
|
var gitOut = async (cwd, args) => {
|
|
47893
49269
|
try {
|
|
@@ -48000,7 +49376,7 @@ init_src2();
|
|
|
48000
49376
|
|
|
48001
49377
|
// src/services/secure-home.ts
|
|
48002
49378
|
import { chmod as chmod2, lstat as lstat4, mkdir as mkdir7 } from "node:fs/promises";
|
|
48003
|
-
import { isAbsolute as
|
|
49379
|
+
import { isAbsolute as isAbsolute7, resolve as resolve20 } from "node:path";
|
|
48004
49380
|
var HomePermissionError = class extends Error {
|
|
48005
49381
|
code = "HOME_SYMLINK";
|
|
48006
49382
|
};
|
|
@@ -48014,7 +49390,7 @@ async function assertNoSymlink(path2, allowMissing) {
|
|
|
48014
49390
|
}
|
|
48015
49391
|
}
|
|
48016
49392
|
async function secureHanomanHome(opts) {
|
|
48017
|
-
if (!
|
|
49393
|
+
if (!isAbsolute7(opts.home)) throw new HomePermissionError("HANOMAN_HOME harus absolut");
|
|
48018
49394
|
await assertNoSymlink(opts.home, true);
|
|
48019
49395
|
await mkdir7(opts.home, { recursive: true, mode: 448 });
|
|
48020
49396
|
await assertNoSymlink(opts.home, false);
|
|
@@ -48229,6 +49605,10 @@ var bootstrapReady = secureHanomanHome({
|
|
|
48229
49605
|
}).then(() => prisma.user.count()).then(async (count) => {
|
|
48230
49606
|
await secureHanomanHome({ home: home2, files: [dbFilePath(process.env.DATABASE_URL)] });
|
|
48231
49607
|
if (count > 0) return;
|
|
49608
|
+
if (!resolveHardening(process.env)) {
|
|
49609
|
+
console.log("setup awal: buka dashboard dan ikuti wizard \u2014 tanpa hardening, akun pertama tak butuh token");
|
|
49610
|
+
return;
|
|
49611
|
+
}
|
|
48232
49612
|
const proof = await ensureSetupToken(resolveHome());
|
|
48233
49613
|
console.log(`setup admin memerlukan token di ${proof.path}; kedaluwarsa ${new Date(proof.expiresAt).toISOString()}`);
|
|
48234
49614
|
});
|