From 0aec994089459a64d7bf9e8584f59f4bf6243e4e Mon Sep 17 00:00:00 2001 From: HardcoreMonk Date: Thu, 13 Aug 2026 03:50:12 +0900 Subject: [PATCH 1/2] fix: close next release security gates --- .github/workflows/ci.yml | 34 + CONTEXT.md | 16 +- README.md | 10 +- RELEASE_NOTES.md | 18 +- cmd/goose-daemon/api.go | 23 +- cmd/goose-daemon/api_test.go | 29 + cmd/goose-daemon/egress_policy.go | 62 +- cmd/goose-daemon/egress_policy_test.go | 77 +- docs/ADR_INDEX.md | 2 +- docs/PUBLIC_RELEASE_BOUNDARY.md | 2 +- .../adr/0002-egress-sni-transparent-filter.md | 41 +- ...roject-process-status-review-2026-08-13.md | 54 + docs/architecture/multi-tenant-roadmap.md | 10 +- docs/architecture/service-logic.md | 9 +- docs/guides/mcp-adapter.md | 8 +- .../2026-08-13-allow-hosts-removal-handoff.md | 91 + ...2026-08-13-release-gate-closure-handoff.md | 26 +- ...13-release-governance-hardening-handoff.md | 98 + ...-13-vm-deletion-failure-cleanup-handoff.md | 111 + docs/operations/runbook.md | 14 +- docs/operations/security-policy.md | 22 +- .../2026-08-13-allow-hosts-removal.md | 71 + ...2026-08-13-release-governance-hardening.md | 61 + .../2026-08-13-vm-deletion-failure-cleanup.md | 60 + .../plans/2026-08-13-allow-hosts-removal.md | 145 + .../plans/2026-08-13-release-gate-closure.md | 18 +- ...2026-08-13-release-governance-hardening.md | 95 + .../2026-08-13-vm-deletion-failure-cleanup.md | 105 + .../2026-08-13-allow-hosts-removal-design.md | 131 + ...-13-release-governance-hardening-design.md | 118 + ...8-13-vm-deletion-failure-cleanup-design.md | 109 + internal/storage/snapshot.go | 38 +- internal/storage/snapshot_teardown_test.go | 91 + web/package-lock.json | 2385 +++++++++-------- web/package.json | 5 + 35 files changed, 2855 insertions(+), 1334 deletions(-) create mode 100644 docs/operations/2026-08-13-allow-hosts-removal-handoff.md create mode 100644 docs/operations/2026-08-13-release-governance-hardening-handoff.md create mode 100644 docs/operations/2026-08-13-vm-deletion-failure-cleanup-handoff.md create mode 100644 docs/superpowers/grill-me/2026-08-13-allow-hosts-removal.md create mode 100644 docs/superpowers/grill-me/2026-08-13-release-governance-hardening.md create mode 100644 docs/superpowers/grill-me/2026-08-13-vm-deletion-failure-cleanup.md create mode 100644 docs/superpowers/plans/2026-08-13-allow-hosts-removal.md create mode 100644 docs/superpowers/plans/2026-08-13-release-governance-hardening.md create mode 100644 docs/superpowers/plans/2026-08-13-vm-deletion-failure-cleanup.md create mode 100644 docs/superpowers/specs/2026-08-13-allow-hosts-removal-design.md create mode 100644 docs/superpowers/specs/2026-08-13-release-governance-hardening-design.md create mode 100644 docs/superpowers/specs/2026-08-13-vm-deletion-failure-cleanup-design.md create mode 100644 internal/storage/snapshot_teardown_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11646aa..f1d270f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,3 +59,37 @@ jobs: run: | go install golang.org/x/vuln/cmd/govulncheck@v1.6.0 "$(go env GOPATH)/bin/govulncheck" ./... + + web-and-security: + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + + - name: Install locked dependencies + run: npm ci + + - name: Svelte check + run: npm run check + + - name: Build embedded Web UI + run: npm run build + + - name: Audit dependencies + run: npm audit --audit-level=moderate + + secret-scan: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v6 + + - name: Scan tracked source for credential patterns + run: bash scripts/secret-scan.sh diff --git a/CONTEXT.md b/CONTEXT.md index 32a4fd9..30fbc60 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -32,7 +32,10 @@ IronClaw 실행 계층으로 통합하는 downstream product fork다. 이 저장 경로와 기존 API/환경 변수에는 `ephemera` 또는 `goose` 이름이 남아 있다. anvil 통합 릴리즈는 ephemera runtime tag와 충돌하지 않도록 `anvil-v0.1.0`처럼 별도 prefix를 사용한다. **`anvil-v0.7.0`부터 anvil 공개 릴리즈 버전은 upstream ephemera -버전과 동일하게 정렬한다.** 현재 최신(Latest) 릴리즈는 `anvil-v0.7.0`이고 tag +버전과 동일하게 정렬한다. 다음 공개 version은 upstream의 첫 post-`v0.7.0` 정식 tag를 +`anvil-`로 정확히 변환한다. downstream-only patch/minor 번호를 만들지 +않는다. 2026-08-13 현재 upstream latest/main은 `v0.7.0`이므로 다음 version number는 +미할당이며 release blocker다.** 현재 최신(Latest) 릴리즈는 `anvil-v0.7.0`이고 tag target은 `2f367dd`(태그 시점 main; parity + release-gate hardening + post-release backlog + open-gate 마감; full KVM e2e 343✓, step 59 실 LLM 포함)이며 설치 아티팩트 (SLIM/FULL tarball + sha256)를 제공한다. 이후 main은 cross-host shared Town @@ -464,8 +467,8 @@ daemon으로 보내는 outbound Bearer token이다. 상세: [`docs/operations/2026-07-11-6b-failover-verification-run.md`](docs/operations/2026-07-11-6b-failover-verification-run.md). - **egress L7/SNI hardening이 구현 완료됐다**(2026-07-13/14, `feature/egress-sni-filter`, 최소 3개 릴리즈(`v0.2.x`~`v0.3.1`)에서 이월되던 후속 후보를 해소). `egressProfile`에 - 신규 `allow_sni []string` 필드(기존 `allow_hosts` substring 재해석 아님, 하위호환 - additive, `*.` leading-label wildcard)를 추가하고, :443 새 흐름의 ClientHello를 + 신규 `allow_sni []string` 필드(`*.` leading-label wildcard)를 추가하고, :443 새 + 흐름의 ClientHello를 `iptables -j NFQUEUE --queue-num 88`(env `ANVIL_SNI_QUEUE_NUM`)로 goose-daemon **in-process** verdict 루프(`github.com/florianl/go-nfqueue/v2` — 이 slice의 유일한 신규 direct 의존)에 dispatch해 실제 파싱된 SNI를 매칭한다. 허용 흐름은 conntrack @@ -552,8 +555,11 @@ daemon으로 보내는 outbound Bearer token이다. PASS(왕복+경계 3종, 신규 결함 없음). [`docs/operations/2026-07-13-mcp-gateway-deployment-verification-run.md`](docs/operations/2026-07-13-mcp-gateway-deployment-verification-run.md). - egress SNI 필터 후속(ADR-0002 잔여 위험/설계 한계에서 파생): - `allow_hosts` 제거 시점은 **2026-07-18 deprecation cycle 확정**(release N 런타임 - 경고 추가 + release N+1(다음 tagged anvil 릴리즈) 제거·loud fail-closed — OQ8). + ~~`allow_hosts` 제거~~는 **2026-08-13 완료**됐다. 2026-07-18에 확정한 deprecation + cycle의 release N 경고 단계를 거쳐 field·validation·iptables packet substring apply + path를 제거했고, 잔존 `allow_hosts` key는 non-empty/empty/`null` 및 과거 + case-insensitive spelling 모두 profile decode에서 loud fail-closed로 거부한다(OQ8). + 마이그레이션은 domain → `allow_sni`, IP/CIDR → `allow_cidrs`다. multi-queue per-VM NFQUEUE는 **2026-07-18 분석 후 YAGNI 확정**(단일 queue 88 + connmark fast-path가 새 flow ClientHello만 처리해 병목 아님; 공유 reassembler LRU 4096은 신뢰-워크로드 모델·실 스케일에 headroom 충분; 재개 트리거 = untrusted diff --git a/README.md b/README.md index 7d15a8c..66a7f96 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,9 @@ Web UI 사용법은 [`docs/guides/runtime-usage.md`](docs/guides/runtime-usage.m 도메인 단위로 강제한다. TCP는 파싱된 TLS ClientHello SNI, QUIC/UDP:443은 자체 구현 QUIC Initial 복호(HKDF+AES-128-GCM+header protection, QUICv1/v2)로 SNI를 추출해 goose-daemon in-process NFQUEUE verdict 루프가 fail-closed로 판정한다(허용=conntrack - connmark 커널 fast-path, 비허용=DROP). CIDR allow가 SNI보다 상위 계약이다. + connmark 커널 fast-path, 비허용=DROP). CIDR allow가 SNI보다 상위 계약이다. 제거된 + `allow_hosts` key는 값이 비어 있어도 profile load가 실패하며, 도메인은 `allow_sni`, + IP/CIDR은 `allow_cidrs`로 마이그레이션해야 한다. - **운영 표면** — Operator CLI `ephemera-ctl`, 브라우저 Web UI(`/ui/`, EN/KO), control-plane 인증(named token·per-token TTL·SIGHUP hot rotation), Prometheus `/metrics`, access audit log, end-user installer(`install.sh`/`uninstall.sh`). @@ -135,6 +137,9 @@ rewrite 없음). ephemera runtime release tag는 `v*`, anvil product release tag [`CONTEXT.md`](CONTEXT.md)). 이후 main은 cross-host routed flock(공유 Town Wall·gtcall· home 재선출 failover), snapshot replication 자동화, egress SNI/L7 필터(TCP+QUIC)와 복구 무결성 하드닝 등 untagged 작업(PR #19 이후)을 더 포함한다. 첫 공개 tag는 `anvil-v0.1.0`이다. +다음 공개 version은 upstream의 첫 post-`v0.7.0` 정식 tag와 같은 번호만 사용한다 +(`vX.Y.Z` → `anvil-vX.Y.Z`). 2026-08-13 현재 새 upstream tag가 없어 다음 번호는 +미할당이며, downstream-only 번호나 추정 tag는 만들지 않는다. remote 설정과 upstream sync 절차 전체는 [`docs/operations/upstream-sync-policy.md`](docs/operations/upstream-sync-policy.md)에 있다. @@ -150,7 +155,7 @@ anvil main runtime baseline은 upstream ephemera `v0.7.0`까지를 병합·적 | 구분 | 현재 기준 | anvil에서의 의미 | |---|---|---| | ephemera runtime baseline | `v0.7.0` | Firecracker VM lifecycle, cold-restart, flock recovery, token rotation, `/metrics`, `/stats`, `slog`, in-VM `gtcall`, webdev demo, v0.4.0-v0.4.5 runtime 안정화(auth/audit, COW spawn, dynamic flock lifecycle, streaming task, nested depth guard, watchdog status, snapshot-restore auto-recovery), v0.5.0-v0.5.5 operator support(Web UI `/ui/`, `/config/*`, per-VM sizing `1` vCPU/`1024` MiB default), v0.6.0-v0.6.4 runtime MCP Gateway(`EPHEMERA_MCP_*`, anti-spoof/rate-limit/stdio backends), v0.7.0 end-user installer + conversation transcript restore 기반. upstream parity scope(v0.4.0-v0.7.0) 코드 편입 완료 | -| upstream latest observed | `v0.7.0` (2026-07-02 확인) | 관찰 범위 전체 병합·적응 완료, pending sync 후보 없음 | +| upstream latest observed | `v0.7.0` (2026-08-13 확인) | upstream main=`v0.7.0`; 관찰 범위 전체 병합·적응 완료, pending sync 후보 없음 | | anvil product surface | `anvil_*` MCP tool, scheduler, tenant/egress, workload runner | IronClaw가 직접 사용하는 공개 실행 계약 | | namespace policy | `EPHEMERA_*`, `goose-*`, `ephemera_*` 유지 | upstream runtime 호환성. anvil 이름으로 일괄 rename하지 않는다. | @@ -165,6 +170,7 @@ anvil control plane은 named token 인증에 per-token TTL과 SIGHUP hot rotatio Prometheus `/metrics`와 access audit log로 운영 가시성을 남긴다. guest egress는 profile `allow_sni`로 :443을 도메인 단위 fail-closed로 강제한다(TCP ClientHello + QUIC Initial 자체 복호, [`docs/adr/0002-egress-sni-transparent-filter.md`](docs/adr/0002-egress-sni-transparent-filter.md)). +legacy packet-substring `allow_hosts`는 제거됐고 잔존 key는 loud fail-closed로 거부된다. 보안 모델, 알려진 제약, Resilience·Observability 전체는 [`docs/guides/security-and-resilience.md`](docs/guides/security-and-resilience.md)에 있다. 공개 노출, 제어 평면 token, guest agent token, snapshot metadata 반출 정책은 diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 76d7c30..599071b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -10,8 +10,9 @@ preflight, [ADR-0002](docs/adr/0002-egress-sni-transparent-filter.md))· **UDP:443(QUIC/HTTP3) SNI 필터 확장**(같은 ADR-0002, 신규 패키지 `internal/network/quic`로 QUIC Initial을 자체 복호해 같은 `allow_sni` 매처를 UDP에도 적용, post-quantum multi-datagram ClientHello 재조립, 신규 direct 의존 -`golang.org/x/crypto`)·`allow_hosts` deprecation cycle 확정(런타임 경고, 다음 -tagged 릴리즈에서 제거)·egress ECH 관측 metric(`ephemera_egress_sni_ech_observed_total`)· +`golang.org/x/crypto`)·`allow_hosts` deprecation cycle 완료(2026-08-13 제거, +잔존 key는 값이 비어 있어도 `allow_sni`/`allow_cidrs` migration error로 loud +fail-closed 거부)·egress ECH 관측 metric(`ephemera_egress_sni_ech_observed_total`)· scheduler aggregate quota metric(`anvil_scheduler_quota_*`)·CI gofmt gate(빌드 전 `gofmt -l .` 검사)·**보안 하드닝 시리즈**(host-local 파일 권한, listener pre-auth 한계, adapter/gateway 입력 검증, Town Wall 레코드 무결성, supply chain·release @@ -21,7 +22,10 @@ workflow, pinned artifact 검증 — PR #91~#99, 아래 「보안/운영 강화 KVM/Firecracker resume-race로 확정 → 2026-07-13 **종결**(default plain 유지·COW opt-in, fc v1.16.1이 실패율 100%→~15–25%로 최대 완화) — `docs/ADR_INDEX.md` v0.4.2 행 참조.) anvil 버전은 이제 -upstream ephemera 버전을 따른다. 이 릴리즈는 아래 `anvil-v0.4.0` 절의 parity +upstream ephemera 버전을 따른다. 다음 public version은 upstream의 첫 post-`v0.7.0` +정식 tag를 `anvil-`로 정확히 변환하며 downstream-only 번호를 만들지 +않는다. 2026-08-13 현재 upstream latest/main이 `v0.7.0`이므로 다음 번호는 미할당 +release blocker다. 이 릴리즈는 아래 `anvil-v0.4.0` 절의 parity 편입(`v0.4.0`-`v0.7.0`)에 더해, `anvil-v0.4.0`(`de82481`) 이후 main에 반영된 post-release backlog batch(PR #18 merge `726cbdc`)와 open-gate closure(step 59 실 LLM e2e 343✓)를 포함한 가장 완전한 상태다. backlog·open-gate 항목은 모두 anvil @@ -221,6 +225,14 @@ post-release backlog batch(PR #18 merge `726cbdc`)와 open-gate closure(step 59 ## 유지보수 +- Web dependency graph의 `esbuild`, `postcss`, `nanoid`를 patched transitive release로 + override해 `npm audit --audit-level=moderate` 0건으로 닫았다(2026-08-13). CI는 기존 + Go `build-and-test`와 별도로 Node 22 `web-and-security`(clean install/check/build/audit)와 + `secret-scan` tracked-tree gate를 실행한다. +- explicit VM delete에서 `dmsetup remove --retry`가 실패해도 COW/base loop detach, + sparse `.cow` unlink와 TAP/IP release를 계속 시도하고 모든 error를 aggregate한다 + (2026-08-13). forced failure unit test와 KVM 종료 후 resource-type별 inventory로 + cleanup 불변 조건을 검증한다. - web `npm audit` 비파괴 findings 정리(`a0fc935`). non-breaking fix가 없는 remainder는 breaking major 업그레이드(vite5→8, svelte4→5)뿐이라 별도 upgrade cycle로 defer한다. - `cmd/goose-daemon/config_api.go`를 surface별로 분할한다(`a01d8da`, #19, 순수 이동): diff --git a/cmd/goose-daemon/api.go b/cmd/goose-daemon/api.go index 7584224..e6e1847 100644 --- a/cmd/goose-daemon/api.go +++ b/cmd/goose-daemon/api.go @@ -510,6 +510,7 @@ type ControlPlane struct { startMachine func(ctx context.Context, cfg vm.VMConfig) (*firecracker.Machine, error) setupDMSnapshot func(baseDiskPath, exceptionStorePath, mountTargetPath string) (*storage.DMSnapshotInfo, error) teardownDMSnapshot func(info *storage.DMSnapshotInfo) + deleteDMSnapshot func(info *storage.DMSnapshotInfo) error setupBindMount func(baseDiskPath, newDiskPath, mountTargetPath string) error restoreMachine func(ctx context.Context, cfg vm.VMConfig, memFilePath, snapshotPath string) (*firecracker.Machine, error) setGuestAgentToken func(vsockPath, token string) error @@ -1835,10 +1836,20 @@ func (cp *ControlPlane) destroyVMUnderSnapshotLock(vmID string) { os.Remove(v.vsockPath) } cp.cleanupEgressPolicy(vmID) + cp.cleanupDeletedVMStorageAndNetwork(vmID, v) + cp.metrics.IncVMDelete() + slog.Warn("vm destroyed", "vm_id", vmID) + cp.metrics.vmDestroyTotal.WithLabelValues("ok").Inc() +} +func (cp *ControlPlane) cleanupDeletedVMStorageAndNetwork(vmID string, v *runningVM) { if v.dmSnapshot != nil { // COW-restored VM: release dm-snapshot device, loop device, and exception store. - if err := storage.TeardownDMSnapshot(v.dmSnapshot); err != nil { + teardown := storage.TeardownDMSnapshot + if cp.deleteDMSnapshot != nil { + teardown = cp.deleteDMSnapshot + } + if err := teardown(v.dmSnapshot); err != nil { cp.metrics.IncCleanupFailure() log.Printf("Warning: failed to teardown COW resources for VM [%s]: %v", vmID, err) } @@ -1850,10 +1861,12 @@ func (cp *ControlPlane) destroyVMUnderSnapshotLock(vmID string) { slog.Warn("delete disk failed", "vm_id", vmID, "disk_path", v.diskPath, "err", err) } } - cp.netManager.Release(v.tapDevice, v.GuestIP) - cp.metrics.IncVMDelete() - slog.Warn("vm destroyed", "vm_id", vmID) - cp.metrics.vmDestroyTotal.WithLabelValues("ok").Inc() + // Network cleanup is independent from storage teardown. Never let a failed + // dm/loop/store operation skip TAP and IP lease release. + if err := cp.releaseAllocatedVMNetwork(v.tapDevice, v.GuestIP); err != nil { + cp.metrics.IncCleanupFailure() + slog.Warn("release deleted VM network failed", "vm_id", vmID, "tap_device", v.tapDevice, "guest_ip", v.GuestIP, "err", err) + } } // gracefulAgentStop best-effort asks a VM's goose-agent to shut down (POST /stop, diff --git a/cmd/goose-daemon/api_test.go b/cmd/goose-daemon/api_test.go index d6c633c..dab8e75 100644 --- a/cmd/goose-daemon/api_test.go +++ b/cmd/goose-daemon/api_test.go @@ -986,6 +986,35 @@ func TestControlPlaneMetricsEndpoint(t *testing.T) { } } +func TestCleanupDeletedVMContinuesNetworkReleaseAfterDMSnapshotFailure(t *testing.T) { + cp := newTestCP(t) + var events []string + cp.deleteDMSnapshot = func(info *storage.DMSnapshotInfo) error { + events = append(events, "storage:"+info.DMDevice) + return errors.New("forced dm teardown failure") + } + cp.releaseVMNetwork = func(tapDevice, guestIP string) error { + events = append(events, "network:"+tapDevice+":"+guestIP) + return nil + } + v := &runningVM{ + VMInfo: VMInfo{ + VMID: "vm-delete-failure", + GuestIP: "10.0.1.44", + }, + tapDevice: "tap-delete-failure", + dmSnapshot: &storage.DMSnapshotInfo{ + DMDevice: "cow-vm-delete-failure.cow", + }, + } + + cp.cleanupDeletedVMStorageAndNetwork(v.VMID, v) + + if got, want := strings.Join(events, ","), "storage:cow-vm-delete-failure.cow,network:tap-delete-failure:10.0.1.44"; got != want { + t.Fatalf("cleanup events = %q, want %q", got, want) + } +} + func TestControlPlaneMetricsEndpointIncludesDurationsAndQueueDepth(t *testing.T) { cp := newTestCP(t) cp.metrics.ObserveDuration("vm_create", 1500*time.Millisecond) diff --git a/cmd/goose-daemon/egress_policy.go b/cmd/goose-daemon/egress_policy.go index cad055b..8ff0e21 100644 --- a/cmd/goose-daemon/egress_policy.go +++ b/cmd/goose-daemon/egress_policy.go @@ -3,7 +3,6 @@ package main import ( "encoding/json" "fmt" - "log/slog" "net" "os" "path/filepath" @@ -38,7 +37,6 @@ func sniQueueNum() int { type egressProfile struct { AllowCIDRs []string `json:"allow_cidrs"` - AllowHosts []string `json:"allow_hosts"` AllowSNI []string `json:"allow_sni"` DNSServers []string `json:"dns_servers"` } @@ -66,37 +64,43 @@ func loadEgressProfile(baseDir, profile string) (egressProfile, bool, error) { } return egressProfile{}, false, fmt.Errorf("read egress profile: %w", err) } - var profileConfig egressProfile - if err := json.Unmarshal(data, &profileConfig); err != nil { + profileConfig, err := decodeEgressProfile(data) + if err != nil { return egressProfile{}, false, fmt.Errorf("parse egress profile: %w", err) } if err := validateEgressProfile(profileConfig); err != nil { return egressProfile{}, false, err } - if len(profileConfig.AllowHosts) > 0 { - // allow_hosts is a deprecated coarse substring matcher superseded by - // allow_sni (parsed ClientHello SNI) + allow_cidrs. Warn on every load so - // the docs-only deprecation becomes a runtime signal; the field is still - // applied (behavior unchanged) and is scheduled for removal in the next - // tagged anvil release (ADR-0002 OQ8). Content-free: profile name + count - // only, never the host values. - slog.Warn("egress profile uses deprecated allow_hosts (coarse packet substring match, fragmentation-evadable); migrate to allow_sni (parsed ClientHello SNI) for domains + allow_cidrs for IPs — allow_hosts will be removed in the next tagged anvil release", - "profile", profile, "allow_hosts_count", len(profileConfig.AllowHosts)) - } return profileConfig, true, nil } +func decodeEgressProfile(data []byte) (egressProfile, error) { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return egressProfile{}, err + } + for key := range raw { + // encoding/json matched struct fields case-insensitively while + // allow_hosts existed. Reject those legacy spellings too so removing the + // field can never turn an old profile into a silently ignored allow rule. + if strings.EqualFold(key, "allow_hosts") { + return egressProfile{}, fmt.Errorf("egress profile uses removed field allow_hosts; migrate domains to allow_sni and IP addresses or ranges to allow_cidrs") + } + } + + var profileConfig egressProfile + if err := json.Unmarshal(data, &profileConfig); err != nil { + return egressProfile{}, err + } + return profileConfig, nil +} + func validateEgressProfile(profile egressProfile) error { for _, cidr := range profile.AllowCIDRs { if _, _, err := net.ParseCIDR(strings.TrimSpace(cidr)); err != nil { return fmt.Errorf("invalid allow_cidrs entry %q: %w", cidr, err) } } - for _, host := range profile.AllowHosts { - if err := validateEgressHost(host); err != nil { - return err - } - } for _, entry := range profile.AllowSNI { if err := validateEgressSNI(entry); err != nil { return err @@ -110,19 +114,9 @@ func validateEgressProfile(profile egressProfile) error { return nil } -func validateEgressHost(host string) error { - host = strings.TrimSpace(host) - if host == "" { - return fmt.Errorf("allow_hosts entries must be non-empty") - } - return validateDomainCharset("allow_hosts", host) -} - -// validateDomainCharset enforces the shared ASCII domain charset (letters, +// validateDomainCharset enforces the allow_sni ASCII domain charset (letters, // digits, '.', '-') on an already-trimmed, non-empty host. field names the JSON -// key the host came from ("allow_hosts" or "allow_sni") so the error points at -// the entry the operator actually wrote — validateEgressHost and -// validateEgressSNI both reuse this so the rule lives in one place. +// key so validation errors point at the entry the operator actually wrote. func validateDomainCharset(field, host string) error { for _, r := range host { if r > 127 { @@ -205,12 +199,6 @@ func planProfileEgressCommands(vmID, guestIP string, profile egressProfile) ([]e Args: []string{"-I", "FORWARD", "-s", guestIP, "-d", strings.TrimSpace(cidr), "-j", "ACCEPT", "-m", "comment", "--comment", fmt.Sprintf("%s-cidr-%d", prefix, idx)}, }) } - for idx, host := range profile.AllowHosts { - commands = append(commands, egressCommand{ - Name: "iptables", - Args: []string{"-I", "FORWARD", "-s", guestIP, "-m", "string", "--string", strings.TrimSpace(host), "--algo", "bm", "-j", "ACCEPT", "-m", "comment", "--comment", fmt.Sprintf("%s-host-%d", prefix, idx)}, - }) - } return commands, nil } diff --git a/cmd/goose-daemon/egress_policy_test.go b/cmd/goose-daemon/egress_policy_test.go index 88e3386..4891af6 100644 --- a/cmd/goose-daemon/egress_policy_test.go +++ b/cmd/goose-daemon/egress_policy_test.go @@ -1,8 +1,6 @@ package main import ( - "bytes" - "log/slog" "os" "path/filepath" "strings" @@ -17,7 +15,7 @@ func TestLoadEgressProfileAndPlanAllowlistCommands(t *testing.T) { } if err := os.WriteFile(filepath.Join(profileDir, "egress.json"), []byte(`{ "allow_cidrs": ["203.0.113.10/32"], - "allow_hosts": ["api.anthropic.com"], + "allow_sni": ["api.anthropic.com"], "dns_servers": ["1.1.1.1"] }`), 0600); err != nil { t.Fatalf("write egress profile: %v", err) @@ -38,7 +36,7 @@ func TestLoadEgressProfileAndPlanAllowlistCommands(t *testing.T) { for _, want := range []string{ "iptables -I FORWARD -s 10.0.1.10 -j REJECT -m comment --comment anvil-egress-vm-1-default", "iptables -I FORWARD -s 10.0.1.10 -d 203.0.113.10/32 -j ACCEPT -m comment --comment anvil-egress-vm-1-cidr-0", - "iptables -I FORWARD -s 10.0.1.10 -m string --string api.anthropic.com --algo bm -j ACCEPT -m comment --comment anvil-egress-vm-1-host-0", + "iptables -I FORWARD -s 10.0.1.10 -p tcp --dport 443 -m connmark ! --mark 0x534e49 -j NFQUEUE --queue-num 88 -m comment --comment anvil-egress-vm-1-sni-nfqueue", } { if !strings.Contains(joined, want) { t.Fatalf("commands missing %q:\n%s", want, joined) @@ -209,53 +207,56 @@ func joinCommands(commands []egressCommand) string { return strings.Join(lines, "\n") } -func TestLoadEgressProfileWarnsOnDeprecatedAllowHosts(t *testing.T) { +func TestLoadEgressProfileRejectsRemovedAllowHosts(t *testing.T) { dir := t.TempDir() - pd := filepath.Join(dir, "legacy") - if err := os.MkdirAll(pd, 0700); err != nil { - t.Fatalf("mkdir profile dir: %v", err) - } - if err := os.WriteFile(filepath.Join(pd, "egress.json"), - []byte(`{"allow_hosts": ["api.anthropic.com"]}`), 0600); err != nil { - t.Fatalf("write egress profile: %v", err) - } - var buf bytes.Buffer - old := slog.Default() - slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn}))) - defer slog.SetDefault(old) + tests := map[string]string{ + "non-empty": `{"allow_hosts":["private-api.example.com"]}`, + "empty": `{"allow_hosts":[]}`, + "null": `{"allow_hosts":null}`, + "mixed-case": `{"ALLOW_HOSTS":["private-api.example.com"]}`, + } + for name, body := range tests { + t.Run(name, func(t *testing.T) { + pd := filepath.Join(dir, name) + if err := os.MkdirAll(pd, 0700); err != nil { + t.Fatalf("mkdir profile dir: %v", err) + } + if err := os.WriteFile(filepath.Join(pd, "egress.json"), []byte(body), 0600); err != nil { + t.Fatalf("write egress profile: %v", err) + } - profile, ok, err := loadEgressProfile(dir, "legacy") - if err != nil || !ok { - t.Fatalf("loadEgressProfile ok=%v err=%v", ok, err) - } - // Behavior unchanged: allow_hosts is still parsed/applied. - if len(profile.AllowHosts) != 1 || profile.AllowHosts[0] != "api.anthropic.com" { - t.Fatalf("allow_hosts must still be parsed unchanged, got %+v", profile.AllowHosts) - } - if out := buf.String(); !strings.Contains(out, "deprecated allow_hosts") || !strings.Contains(out, "allow_sni") { - t.Fatalf("expected a deprecation warning naming the allow_sni migration, got: %q", out) + _, ok, err := loadEgressProfile(dir, name) + if err == nil || ok { + t.Fatalf("loadEgressProfile ok=%v err=%v, want loud removal error", ok, err) + } + for _, want := range []string{"allow_hosts", "allow_sni", "allow_cidrs"} { + if !strings.Contains(err.Error(), want) { + t.Fatalf("error %q missing migration field %q", err, want) + } + } + if strings.Contains(err.Error(), "private-api.example.com") { + t.Fatalf("error must not repeat removed field values: %q", err) + } + }) } } -func TestLoadEgressProfileNoWarnWithoutAllowHosts(t *testing.T) { +func TestLoadEgressProfileKeepsUnrelatedUnknownFieldCompatibility(t *testing.T) { dir := t.TempDir() - pd := filepath.Join(dir, "sni") + pd := filepath.Join(dir, "metadata") if err := os.MkdirAll(pd, 0700); err != nil { t.Fatalf("mkdir profile dir: %v", err) } if err := os.WriteFile(filepath.Join(pd, "egress.json"), - []byte(`{"allow_sni": ["api.anthropic.com"]}`), 0600); err != nil { + []byte(`{"allow_sni":["api.anthropic.com"],"operator_note":"kept compatible"}`), 0600); err != nil { t.Fatalf("write egress profile: %v", err) } - var buf bytes.Buffer - old := slog.Default() - slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn}))) - defer slog.SetDefault(old) - if _, ok, err := loadEgressProfile(dir, "sni"); err != nil || !ok { - t.Fatalf("loadEgressProfile ok=%v err=%v", ok, err) + profile, ok, err := loadEgressProfile(dir, "metadata") + if err != nil || !ok { + t.Fatalf("loadEgressProfile ok=%v err=%v, want unrelated unknown field ignored", ok, err) } - if strings.Contains(buf.String(), "deprecated allow_hosts") { - t.Fatalf("no allow_hosts warning expected for an allow_sni-only profile, got: %q", buf.String()) + if len(profile.AllowSNI) != 1 || profile.AllowSNI[0] != "api.anthropic.com" { + t.Fatalf("AllowSNI = %#v", profile.AllowSNI) } } diff --git a/docs/ADR_INDEX.md b/docs/ADR_INDEX.md index edd01a5..d5d226b 100644 --- a/docs/ADR_INDEX.md +++ b/docs/ADR_INDEX.md @@ -43,7 +43,7 @@ | ADR | 상태 | 적용 요약 | |---|---|---| | [ADR-0001](adr/0001-anvil-public-boundary-and-upstream-adoption.md) | accepted | `PUBLIC_RELEASE_BOUNDARY.md`와 ADR_INDEX를 도입하고, upstream ephemera 변경을 `adopted/adapted/excluded/deferred/historical`로 분류한다. | -| [ADR-0002](adr/0002-egress-sni-transparent-filter.md) | accepted | `profile` egress의 도메인 통제를 substring `-m string`(coarse, fragmentation/오탐 취약)에서 실제 파싱된 TLS ClientHello SNI로 전환한다. 신규 `allow_sni []string` 필드(기존 `allow_hosts` 재해석 아님, 하위호환 additive)를 :443 새 흐름에 한해 강제한다 — `iptables -j NFQUEUE --queue-num 88`(env `ANVIL_SNI_QUEUE_NUM`)로 dispatch, goose-daemon in-process verdict 루프(`github.com/florianl/go-nfqueue/v2`, 이 slice의 유일한 신규 direct 의존)가 SNI를 파싱해 `allow_sni` 매처와 대조한다. 허용 SNI는 conntrack mark(`0x534e49`)를 찍고 이후 패킷은 커널 fast-path ACCEPT, 비허용/파싱불가는 fail-closed DROP(+best-effort RST). `--queue-bypass`(fail-open)는 명시 배제하고, verdict 루프가 `Ready()`가 아니면 `allow_sni` profile의 spawn 자체를 preflight 거부한다(규칙만 깔리고 검사기가 없는 상태를 원천 차단). **위협 모델**: 신뢰 golden-image 워크로드의 의도된 egress 강제·감사가 in-scope, 적대적 in-guest 루트의 spoof/fronting/ECH 완전봉쇄는 out-of-scope(잔여 위험으로 계약 — ECH는 outer(공개) SNI만 관측 — outer가 allowlisted면 flow 허용+암호화 inner 은닉(guest-asserted SNI와 동일 신뢰등급 잔여), outer가 없거나 비허용이면 fail-closed deny, QUIC/UDP:443도 자체 복호(HKDF+AES-128-GCM+header protection) SNI 필터로 동일 `allow_sni` 매처를 적용하며 미지원 버전/복호실패는 fail-closed DROP(non-TLS UDP는 여전히 SNI 층 밖 — CIDR/base REJECT만), SNI는 guest-asserted라 CIDR 핀 없이는 스푸핑 가능, domain fronting은 TLS 종단 없이 미탐지, 멀티세그먼트 ClientHello의 미완결 세그먼트는 판정 전 unmarked로 통과하되 승인 mark는 완결된 positive match에서만 찍힘). **additive 계약**: CIDR allow 규칙이 head-insert 순서상 SNI dispatch보다 위에 앉아 :443 CIDR 매치가 SNI 검사보다 우선한다(명시 IP 신뢰가 도메인 검사를 우회). `allow_hosts`(substring)는 deprecation cycle 확정(OQ8, 2026-07-18): release N(지금)은 profile 로드 시 런타임 deprecation 경고를 남기고(`loadEgressProfile`), release N+1(다음 tagged anvil 릴리즈)에서 필드·apply·validate·cleanup·test를 제거하고 `egressProfile` unmarshal에 `DisallowUnknownFields`로 잔존 `allow_hosts` profile을 loud fail-closed 거부한다(마이그레이션: 도메인 → `allow_sni`, IP → `allow_cidrs`). **ECH-on-allowed-flow는 이제 관측 가능(2026-07-18)**: 파서가 ECH 확장(`0xfe0d`)을 best-effort 탐지해 allowlisted outer SNI로 허용된 flow가 ECH를 담으면 `ephemera_egress_sni_ech_observed_total{proto}` metric + content-free `slog.Info`를 방출한다(관측 전용, verdict/deny 경로 불변). 유닛(`internal/network/sni` 파서/매처, `cmd/goose-daemon` decide/preflight/rollback/recovery/audit/metric) + KVM e2e `scripts/anvil-egress-sni-e2e.sh`(허용 도달·비허용 차단(RST)·감사 레코드·fast-path metric delta, 독립 재실행 exit 0 확인)로 검증됐다. 상세: [ADR-0002 원문](adr/0002-egress-sni-transparent-filter.md), [design spec](superpowers/specs/2026-07-13-egress-sni-filter-design.md), [plan](superpowers/plans/2026-07-13-egress-sni-filter.md), [handoff](operations/2026-07-13-egress-sni-handoff.md). | +| [ADR-0002](adr/0002-egress-sni-transparent-filter.md) | accepted | `profile` egress의 도메인 통제를 substring `-m string`(coarse, fragmentation/오탐 취약)에서 실제 파싱된 TLS ClientHello SNI로 전환한다. `allow_sni []string` 필드를 :443 새 흐름에 강제한다 — `iptables -j NFQUEUE --queue-num 88`(env `ANVIL_SNI_QUEUE_NUM`)로 dispatch, goose-daemon in-process verdict 루프(`github.com/florianl/go-nfqueue/v2`, 이 slice의 유일한 신규 direct 의존)가 SNI를 파싱해 `allow_sni` 매처와 대조한다. 허용 SNI는 conntrack mark(`0x534e49`)를 찍고 이후 패킷은 커널 fast-path ACCEPT, 비허용/파싱불가는 fail-closed DROP(+best-effort RST). `--queue-bypass`(fail-open)는 명시 배제하고, verdict 루프가 `Ready()`가 아니면 `allow_sni` profile의 spawn 자체를 preflight 거부한다(규칙만 깔리고 검사기가 없는 상태를 원천 차단). **위협 모델**: 신뢰 golden-image 워크로드의 의도된 egress 강제·감사가 in-scope, 적대적 in-guest 루트의 spoof/fronting/ECH 완전봉쇄는 out-of-scope(잔여 위험으로 계약 — ECH는 outer(공개) SNI만 관측 — outer가 allowlisted면 flow 허용+암호화 inner 은닉(guest-asserted SNI와 동일 신뢰등급 잔여), outer가 없거나 비허용이면 fail-closed deny, QUIC/UDP:443도 자체 복호(HKDF+AES-128-GCM+header protection) SNI 필터로 동일 `allow_sni` 매처를 적용하며 미지원 버전/복호실패는 fail-closed DROP(non-TLS UDP는 여전히 SNI 층 밖 — CIDR/base REJECT만), SNI는 guest-asserted라 CIDR 핀 없이는 스푸핑 가능, domain fronting은 TLS 종단 없이 미탐지, 멀티세그먼트 ClientHello의 미완결 세그먼트는 판정 전 unmarked로 통과하되 승인 mark는 완결된 positive match에서만 찍힘). **additive 계약**: CIDR allow 규칙이 head-insert 순서상 SNI dispatch보다 위에 앉아 :443 CIDR 매치가 SNI 검사보다 우선한다(명시 IP 신뢰가 도메인 검사를 우회). `allow_hosts`(substring)는 OQ8 deprecation cycle을 거쳐 2026-08-13 field·apply·validate·test가 제거됐다. 잔존 key는 empty/`null` 및 과거 case-insensitive spelling도 explicit legacy-key 검사로 loud fail-closed 거부한다(마이그레이션: 도메인 → `allow_sni`, IP/CIDR → `allow_cidrs`). **ECH-on-allowed-flow는 이제 관측 가능(2026-07-18)**: 파서가 ECH 확장(`0xfe0d`)을 best-effort 탐지해 allowlisted outer SNI로 허용된 flow가 ECH를 담으면 `ephemera_egress_sni_ech_observed_total{proto}` metric + content-free `slog.Info`를 방출한다(관측 전용, verdict/deny 경로 불변). 유닛(`internal/network/sni` 파서/매처, `cmd/goose-daemon` decide/preflight/rollback/recovery/audit/metric) + KVM e2e `scripts/anvil-egress-sni-e2e.sh`(허용 도달·비허용 차단(RST)·감사 레코드·fast-path metric delta, 독립 재실행 exit 0 확인)로 검증됐다. 상세: [ADR-0002 원문](adr/0002-egress-sni-transparent-filter.md), [design spec](superpowers/specs/2026-07-13-egress-sni-filter-design.md), [plan](superpowers/plans/2026-07-13-egress-sni-filter.md), [handoff](operations/2026-07-13-egress-sni-handoff.md). | | [ADR-0003](adr/0003-per-flock-guest-capability-tokens.md) | accepted | flock member VM에 주입하는 자격증명을 **per-flock guest 능력 토큰으로 통일**한다. local flock도 routed flock과 같은 admission store(`cp.relayTokens`)를 쓰므로, guest 토큰은 그 flock의 wall sub-path(`post\|wall\|wall/history`)와 `call` 진입만 admit하고 어떤 control-plane 경로(`/vms`, `/config/*`, `/tenants`, `/snapshots`)도 열지 않는다 — `authMiddleware` 로직은 무변경이며 바뀐 것은 *무엇을 주입하는가*뿐이다. 토큰은 flock 디렉토리의 별도 파일 `guest-token`에 **호출 지점 명시 0600**(umask 의존 아님) + atomic tmp+rename으로 영속된다(`FlockMetadata`의 무조건 no-secret 불변식을 조건부로 만들지 않으려고 분리). `cp.relayTokens`가 in-memory 전용이고 `LoadFromDisk`가 `ControlPlane`을 참조하지 못하므로 **daemon 시작 경로에 재수화 단계**를 함께 낸다(없으면 재시작 후 첫 member spawn이 빈 토큰을 주입 — 구 모델 대비 회귀). member spawn 3개 지점(`spawnVMForFlock`·`restartAgent`·`changeFlockAgentRole`)이 `ControlPlaneTokenManaged=false`를 세워 SIGHUP 회전이 능력 토큰을 운영자 bearer로 덮어쓰지 못하게 한다. **무중단 업그레이드**: 이전에 spawn된 VM은 `CPTokenManaged=true`가 디스크에 남아 계속 회전을 받고 운영자 bearer도 계속 admit되므로, 회전 대상은 VM 교체와 함께 자연히 말라붙는다(마이그레이션·버전 게이트 없음). **명시된 거래**: 만료되는 넓은 자격증명 → **만료되지 않는** 좁은 것. flock에 TTL·reaper·GC가 없어 토큰 수명 = flock 수명이며 폐기 수단은 flock 삭제뿐이다. blast radius 축소(제어평면 전체 → 한 flock의 wall/call 서브패스)가 이 거래를 정당화한다. **기각 대안**: per-member 토큰 — 재수화 요구를 소멸시키는 것은 사실이나 admission이 동등 비교에서 집합 소속 검사로 바뀌고 `removeFlockAgent`에 per-member 폐기 경로가 필요해 폐기 표면이 member 수만큼 늘어난다. 상세: [ADR-0003 원문](adr/0003-per-flock-guest-capability-tokens.md). | | [Cross-host shared Town Wall](superpowers/specs/2026-07-06-cross-host-shared-townwall-design.md) | accepted | home-host hub 토폴로지: `roles[0]` 배치 호스트가 canonical `TownWall`(hub flock)을 소유하고, 나머지 멤버 host daemon은 relay flock으로 `/flocks/{id}/post`·`/wall`·`/wall/history`·SSE를 home으로 forward/proxy한다. daemon-to-daemon hop은 flock-scoped `relay_token`으로 인증하며, `authMiddleware`는 이 token을 해당 flock의 wall sub-path(`/flocks/{id}/(post\|wall\|wall/history)`)에 admit한다(도입 당시 wall 전용 — cross-host gtcall 편입부터 guest 능력 토큰으로 `call` 진입도 admit, 아래 행) — 일반 control-plane bearer로 승격하지 않는다. `relay_token`은 `PlacementStore`에 영속되지만 모든 MCP output/audit/HTTP view에서 redact한다(`State()`가 nil 처리). **전제**: member↔home daemon은 control-plane 포트로 상호 도달 가능한 신뢰(private) 네트워크 위에 있어야 한다 — 외부 노출은 기존 reverse-proxy/TLS 정책 뒤에서만. **진화 경로**: 도입 당시 home 단일 장애점(SPOF)을 1차 수용했으나, 재선출 failover로 해소됐다(아래 "Home 재선출 failover" row — §6b 실 2-daemon 수동 검증까지 2026-07-11 PASS로 완료). daemon-to-daemon hop은 dial-실패 한정 동기 bounded retry(총 3시도, 1s/2s)로 순단을 흡수한다(전달 semantics 불변). cross-host broadcast fan-out은 비목표(cross-host `gtcall`은 별도 결정으로 편입, 아래 행). `TestIronClawSchemasExcludeCrossHostWallTools`가 새 `anvil_*` MCP tool 미노출을 고정한다. 상세: [design spec](superpowers/specs/2026-07-06-cross-host-shared-townwall-design.md), [handoff](operations/2026-07-06-cross-host-town-wall-handoff.md). 별도 `docs/adr/*.md` 원문은 없음 — 이 row는 design spec을 결정 원문으로 삼는다. | | [Cross-host gtcall](superpowers/specs/2026-07-07-cross-host-gtcall-design.md) | accepted | routed flock의 임의 member가 다른 임의 member를 daemon `POST /flocks/{id}/call`(`{agent_id, prompt}`)로 호출한다. member→home→target 2-hop(home이 canonical roster로 agent→host,vm_id 해석), guest는 bridge-only 유지. **토큰 모델(A안, 2026-07-07 재확정)**: `relay_token`이 guest 능력 토큰으로 재해석돼 그 flock의 wall sub-path **와 `call` 진입**을 모두 admit한다(단일 host flock의 기존 guest CP token→gtcall 개방과 동형). 별도 `call_token`은 daemon-to-daemon call hop 전용(member→home, home→target)으로 **오직 `call` 경로만** admit하고 **wall 경로는 거부**한다(배타는 유지·테스트 고정, control-plane bearer 승격 금지). `call_token`은 `relay_token`과 나란히 `PlacementStore`에 영속되고 `State()` 등 모든 직렬화 표면에서 redact된다. hop 요청은 `X-Ephemera-Call-Hop`으로 loop guard(kind와 무관하게 로컬 해석 전용 — 2026-07-08 설계 보정)하되, member→home 구간은 unmarked(home은 해석자이지 종단이 아니며 자신의 2번째 hop을 위해 표식 없이 받아야 한다), home→target 종단 hop만 marked다(2026-07-08 최종 리뷰 C1 수정 — 이전에는 member→home 구간에도 무조건 표식을 붙여 home의 2번째 hop이 성립하지 않았다). `X-Ephemera-Task-Depth`를 전 hop 전파해 `EPHEMERA_MAX_TASK_DEPTH`가 cross-host에서도 성립한다. 이 slice가 wall slice의 잠재 결함(`registerRelayFlock`이 admit 등록을 하지 않아 auth-on member daemon에서 routed guest의 gtwall이 401되던 문제)을 동반 수정한다. 기존 2-step `GET /flocks/{id}` + `POST /vms/{vm_id}/tasks` 계약은 하위호환 유지, 신규 `anvil_*` MCP tool 없음. KVM e2e `scripts/anvil-cross-host-gtcall-e2e.sh`(real member VM + stub home, **auth-on** member daemon)로 검증됐다. home SPOF는 wall과 동일하게 재선출 failover로 해소(아래 row — §6b 실 2-daemon 수동 검증 2026-07-11 PASS), cross-host broadcast fan-out은 계속 비목표. daemon-to-daemon hop은 dial-실패 한정 동기 bounded retry(총 3시도, 1s/2s)로 순단을 흡수한다(전달 semantics 불변). 상세: [design spec](superpowers/specs/2026-07-07-cross-host-gtcall-design.md), [handoff](operations/2026-07-08-cross-host-gtcall-handoff.md). 별도 `docs/adr/*.md` 원문은 없음 — 이 row는 design spec을 결정 원문으로 삼는다. | diff --git a/docs/PUBLIC_RELEASE_BOUNDARY.md b/docs/PUBLIC_RELEASE_BOUNDARY.md index 12db5b8..c303b9e 100644 --- a/docs/PUBLIC_RELEASE_BOUNDARY.md +++ b/docs/PUBLIC_RELEASE_BOUNDARY.md @@ -32,7 +32,7 @@ | VM lifecycle | VM 생성, task 실행(`POST /vms/{id}/tasks?stream=1` optional NDJSON streaming, 기본은 buffered `{"output","error"}` 계약), health, stop, delete | ephemera daemon API + anvil MCP wrapper | | Snapshot lifecycle | full/diff snapshot 생성, 목록, restore, 삭제 | `internal/storage`, `anvil_create_snapshot` 계열 MCP tool | | Runtime boundary | Firecracker MicroVM, TAP/IP, rootfs, guest agent proxy | `cmd/goose-daemon`, `internal/vm`, `internal/network`, `internal/storage` | -| Egress SNI filter | `profile` egress policy의 `configs/profiles/{profile}/egress.json`(`EPHEMERA_EGRESS_PROFILE_DIR`/`ANVIL_EGRESS_PROFILE_DIR` 고정 계약) 스키마에 신규 `allow_sni []string` 필드(exact + leading-label `*.` wildcard, `allow_cidrs`/`dns_servers`와 병렬 additive, 하위호환)를 추가한다. :443 새 흐름의 ClientHello가 `iptables -j NFQUEUE --queue-num 88`(env `ANVIL_SNI_QUEUE_NUM` override)로 goose-daemon in-process verdict 루프(`github.com/florianl/go-nfqueue/v2` — 유일한 신규 direct 의존)에 dispatch되어 SNI를 파싱·매칭한다. 허용 흐름은 conntrack mark `0x534e49`를 찍고 커널 fast-path ACCEPT, 비허용/파싱불가는 fail-closed DROP(+best-effort RST) — `--queue-bypass`(fail-open)는 명시 배제한다. **NFQUEUE baseline 요구**: `profile` egress policy를 지원하는 모든 host는 verdict 루프 바인딩이 가능해야 하며, 불가능한 host는 `allow_sni` profile의 VM spawn을 preflight로 거부한다(규칙만 깔리고 검사기가 없는 상태를 원천 차단). CIDR allow는 SNI dispatch보다 head-insert 순서상 위에 있어 :443 CIDR 매치가 SNI 검사보다 우선한다. `allow_hosts`(packet substring match)는 **legacy/deprecated**다 — profile 로드 시 daemon이 런타임 경고를 남기고 **다음 tagged anvil 릴리즈에서 제거** 예정이다(제거 후 잔존 profile은 loud fail-closed 거부; 마이그레이션 도메인→`allow_sni`/IP→`allow_cidrs`, ADR-0002 OQ8). 위협 모델·잔여 위험(ECH/spoofing/fronting/pre-decision 부분전달)은 ADR-0002 계약. **UDP:443(QUIC/HTTP3) 확장(2026-07-14)**: 같은 queue 88·같은 connmark를 UDP:443에도 적용한다 — `allow_sni` profile은 TCP `-sni-nfqueue`/`-sni-fastpath`와 대칭으로 `iptables -p udp --dport 443 ... --comment -sni-udp-nfqueue`/`-sni-udp-fastpath` 규칙도 head-insert한다. verdict 루프가 QUIC Initial을 자체 구현 crypto(`internal/network/quic`, HKDF+AES-128-GCM+header protection, 신규 direct 의존 `golang.org/x/crypto` 하나)로 복호해 CRYPTO 프레임에서 TLS ClientHello를 얻어 같은 `allow_sni` 매처와 대조한다. QUICv1(`0x00000001`)+QUICv2(`0x6b3343cf`) 지원, 미지원 버전은 fail-closed deny. post-quantum(X25519MLKEM768) ClientHello가 Initial 데이터그램 2개에 걸치는 경우를 flow별 bounded-LRU reassembler로 재조립하며, 미완결 데이터그램은 drop(fail-closed)하되 CRYPTO는 누적한다(connmark가 conntrack에 first-accepted 패킷으로 confirm되게 하기 위함 — 미완결 passthrough-accept는 mark 0 confirm으로 fast-path를 깨뜨린다). UDP엔 RST가 없어 deny는 silent DROP이며, QUIC 타임아웃 후 브라우저가 TCP/HTTP2로 fallback하면 TCP:443 SNI 필터가 이어받는다. 재조립은 데이터그램 수에 하드 제한이 없다(3개 이상 데이터그램 ClientHello 지원) — 실질 상한은 per-flow 8192B 캡이며 이를 초과하는 ClientHello만 fail-closed deny(주류 클라 미해당). **ECH 관측(2026-07-18)**: allowlisted outer SNI로 허용된 flow가 ECH(`0xfe0d`)를 담으면 `ephemera_egress_sni_ech_observed_total{proto}` + content-free `slog.Info`를 방출한다(관측 전용, verdict 불변) | `cmd/goose-daemon/egress_policy.go`, `cmd/goose-daemon/sni_verdict.go`, `internal/network/sni`, `internal/network/quic`, [`docs/adr/0002-egress-sni-transparent-filter.md`](adr/0002-egress-sni-transparent-filter.md) | +| Egress SNI filter | `profile` egress policy의 `configs/profiles/{profile}/egress.json`(`EPHEMERA_EGRESS_PROFILE_DIR`/`ANVIL_EGRESS_PROFILE_DIR` 고정 계약) 스키마에 `allow_sni []string` 필드(exact + leading-label `*.` wildcard)를 `allow_cidrs`/`dns_servers`와 병렬로 사용한다. :443 새 흐름의 ClientHello가 `iptables -j NFQUEUE --queue-num 88`(env `ANVIL_SNI_QUEUE_NUM` override)로 goose-daemon in-process verdict 루프(`github.com/florianl/go-nfqueue/v2` — 유일한 신규 direct 의존)에 dispatch되어 SNI를 파싱·매칭한다. 허용 흐름은 conntrack mark `0x534e49`를 찍고 커널 fast-path ACCEPT, 비허용/파싱불가는 fail-closed DROP(+best-effort RST) — `--queue-bypass`(fail-open)는 명시 배제한다. **NFQUEUE baseline 요구**: `profile` egress policy를 지원하는 모든 host는 verdict 루프 바인딩이 가능해야 하며, 불가능한 host는 `allow_sni` profile의 VM spawn을 preflight로 거부한다(규칙만 깔리고 검사기가 없는 상태를 원천 차단). CIDR allow는 SNI dispatch보다 head-insert 순서상 위에 있어 :443 CIDR 매치가 SNI 검사보다 우선한다. `allow_hosts`(packet substring match)는 **2026-08-13 제거**됐다. 잔존 key는 empty/`null`을 포함해 loud fail-closed 거부하며, 마이그레이션은 도메인→`allow_sni`, IP/CIDR→`allow_cidrs`다(ADR-0002 OQ8). 위협 모델·잔여 위험(ECH/spoofing/fronting/pre-decision 부분전달)은 ADR-0002 계약. **UDP:443(QUIC/HTTP3) 확장(2026-07-14)**: 같은 queue 88·같은 connmark를 UDP:443에도 적용한다 — `allow_sni` profile은 TCP `-sni-nfqueue`/`-sni-fastpath`와 대칭으로 `iptables -p udp --dport 443 ... --comment -sni-udp-nfqueue`/`-sni-udp-fastpath` 규칙도 head-insert한다. verdict 루프가 QUIC Initial을 자체 구현 crypto(`internal/network/quic`, HKDF+AES-128-GCM+header protection, 신규 direct 의존 `golang.org/x/crypto` 하나)로 복호해 CRYPTO 프레임에서 TLS ClientHello를 얻어 같은 `allow_sni` 매처와 대조한다. QUICv1(`0x00000001`)+QUICv2(`0x6b3343cf`) 지원, 미지원 버전은 fail-closed deny. post-quantum(X25519MLKEM768) ClientHello가 Initial 데이터그램 2개에 걸치는 경우를 flow별 bounded-LRU reassembler로 재조립하며, 미완결 데이터그램은 drop(fail-closed)하되 CRYPTO는 누적한다(connmark가 conntrack에 first-accepted 패킷으로 confirm되게 하기 위함 — 미완결 passthrough-accept는 mark 0 confirm으로 fast-path를 깨뜨린다). UDP엔 RST가 없어 deny는 silent DROP이며, QUIC 타임아웃 후 브라우저가 TCP/HTTP2로 fallback하면 TCP:443 SNI 필터가 이어받는다. 재조립은 데이터그램 수에 하드 제한이 없다(3개 이상 데이터그램 ClientHello 지원) — 실질 상한은 per-flow 8192B 캡이며 이를 초과하는 ClientHello만 fail-closed deny(주류 클라 미해당). **ECH 관측(2026-07-18)**: allowlisted outer SNI로 허용된 flow가 ECH(`0xfe0d`)를 담으면 `ephemera_egress_sni_ech_observed_total{proto}` + content-free `slog.Info`를 방출한다(관측 전용, verdict 불변) | `cmd/goose-daemon/egress_policy.go`, `cmd/goose-daemon/sni_verdict.go`, `internal/network/sni`, `internal/network/quic`, [`docs/adr/0002-egress-sni-transparent-filter.md`](adr/0002-egress-sni-transparent-filter.md) | | Runtime observability | `/metrics`, `/metrics/vms`, `/vms/{vm_id}/stats`, `GET /watchdog/status`(read-only count/ID/config), structured daemon logs | upstream ephemera runtime namespace + anvil 운영 문서 | | Operator Web console | daemon이 `/ui/`로 serve하는 embedded Svelte SPA(EN/KO). `/ui/`(정적 bundle + login)만 auth 밖, VM list/create/detail/stats/settings/delete·multi-turn session data API는 bearer 뒤. runtime/operator surface이며 IronClaw MCP surface가 아니다 | `cmd/goose-daemon/uidist/`, `cmd/goose-daemon/config_api.go`, `docs/architecture/service-logic.md` | | Profile config surface | `/config/profiles`(provider/model), `/config/providers`(key 존재 여부만), `/config/presets`(sizing preset), `/config/clients`(이름+만료만), profile `system.md` 편집(`64 KiB` cap). `goose-secrets.yaml` 값은 read/write·노출하지 않음 | `cmd/goose-daemon/config_api.go`(profile CRUD·`system.md`·`/ui/`), `providers.go`, `presets.go`, `clients.go` | diff --git a/docs/adr/0002-egress-sni-transparent-filter.md b/docs/adr/0002-egress-sni-transparent-filter.md index 51c624f..dd53eb8 100644 --- a/docs/adr/0002-egress-sni-transparent-filter.md +++ b/docs/adr/0002-egress-sni-transparent-filter.md @@ -2,6 +2,7 @@ > **상태:** accepted > **날짜:** 2026-07-13 +> **개정:** 2026-08-13 (`allow_hosts` 제거 및 loud legacy-key rejection 완료) > **대상:** anvil downstream repository (`cmd/goose-daemon`, `internal/network`) --- @@ -125,30 +126,29 @@ verdict 루프가 QUIC Initial을 **복호**해 TLS ClientHello를 얻고 기존 [design spec](../superpowers/specs/2026-07-14-quic-sni-filter-design.md)에 보존한다. -### 스키마 확장 +### 현재 스키마 -`egressProfile`에 신규 `allow_sni []string` 필드를 추가한다(기존 -`allow_hosts`의 의미 재해석이 아니다): +`egressProfile`은 `allow_sni []string`으로 domain egress를 표현한다. 2026-08-13 +deprecation cycle을 완료하면서 legacy `allow_hosts` field와 packet substring apply +path를 제거했다: ```go type egressProfile struct { AllowCIDRs []string `json:"allow_cidrs"` - AllowHosts []string `json:"allow_hosts"` // legacy: -m string substring (deprecated) AllowSNI []string `json:"allow_sni"` // parsed ClientHello SNI, default-deny DNSServers []string `json:"dns_servers"` } ``` -- **하위호환**: `allow_cidrs`/`allow_hosts`/`dns_servers`만 있는 기존 - profile은 무변경 동작. `allow_sni`는 opt-in(비어 있으면 :443용 NFQUEUE - dispatch 규칙 자체를 생성하지 않는다). +- **legacy rejection**: JSON에 `allow_hosts` key가 있으면 값이 non-empty, empty, + `null` 또는 과거 case-insensitive spelling이어도 profile load가 loud fail-closed로 + 실패한다. error는 host value를 반복하지 않고 `allow_sni`/`allow_cidrs` migration을 + 지시한다. 다른 unknown metadata의 기존 ignore 동작은 유지한다. +- **opt-in**: `allow_sni`가 비어 있으면 :443용 NFQUEUE dispatch 규칙 자체를 생성하지 + 않는다. - **wildcard**: `*.example.com` = 왼쪽 한 개 이상 라벨 매치(leading label만, 임의 위치 glob은 비지원). exact match가 기본. -- **검증 재사용**: `validateEgressHost`(ASCII 영숫자/`.`/`-`)를 SNI 항목에도 - 재사용한다 — 알려진 부작용으로 에러 메시지가 `allow_sni` 항목에도 - `allow_hosts`라는 문자열을 쓴다(cosmetic, 코드 리뷰 triage 항목으로 등재). -- `allow_hosts`는 **legacy/deprecated**(substring, coarse)로 표기하고 유지한다 - (OQ8, 아래). +- **검증**: `allow_sni` exact/wildcard host는 ASCII 영숫자/`.`/`-` contract를 사용한다. - profile directory 계약은 변경하지 않는다: `configs/profiles/{profile}/egress.json`, `EPHEMERA_EGRESS_PROFILE_DIR`, `ANVIL_EGRESS_PROFILE_DIR`. @@ -197,13 +197,12 @@ verdict 루프를 갖춰야 한다"는 **baseline 요구**로 문서화한다( - **OQ7 (ECH fallback)**: ECH 엔드포인트에 outer-SNI allowlist를 허용하지 않는다 — **CIDR fallback만** 명시 opt-in으로 허용한다. outer SNI는 신뢰가 약하다는 판단. -- **OQ8 (`allow_hosts` 폐기 시점)**: **deprecation cycle 확정(2026-07-18).** - release N(지금): profile 로드 시 daemon이 런타임 deprecation 경고를 남긴다 - (`loadEgressProfile` — 그동안 deprecation은 문서에만 있었다). release N+1(다음 - tagged anvil 릴리즈): 필드·apply·validate·cleanup·test 제거 + `egressProfile` - unmarshal에 `DisallowUnknownFields`(또는 명시 거부)로 잔존 `allow_hosts` profile을 - loud fail-closed 거부(조용한 drop 방지). 마이그레이션: 도메인 → `allow_sni`, - IP → `allow_cidrs`(`docs/operations/runbook.md`). +- **OQ8 (`allow_hosts` 폐기 시점)**: **deprecation cycle 완료(2026-08-13).** + 2026-07-18 확정한 runtime warning 단계를 거쳐 field·apply·validate·test를 제거했다. + global `DisallowUnknownFields` 확대 대신 explicit legacy-key rejection을 사용해 잔존 + `allow_hosts`를 loud fail-closed 거부하고, unrelated unknown metadata의 기존 동작은 + 유지한다. 마이그레이션: 도메인 → `allow_sni`, IP/CIDR → `allow_cidrs` + (`docs/operations/runbook.md`). - **OQ9 (single vs per-VM multi-queue)**: 단일 NFQUEUE(queue 88) + src-IP 라우팅을 유지한다(per-VM 멀티 큐 아님). **재검토 결과(2026-07-18): YAGNI.** established flow는 connmark(`0x534e49`) fast-path로 커널이 바로 ACCEPT하므로 @@ -275,8 +274,8 @@ CIDR로, DNS는 `dns_servers`로 — 세 층은 병렬 additive 계약이다. 매치에서만 찍힌다) 전달되는 것은 판정 전 ClientHello 앞부분 바이트뿐이라, 같은 묶음으로 읽으면 위험을 과대 서술하게 된다. 표의 나머지 항목 중 QUIC/UDP:443과 복구 중 transient egress 창은 이미 구현·해소로 닫혔다. -- `allow_hosts`(legacy substring)를 당장 제거하지 않아 두 계층(coarse - substring + precise SNI)이 당분간 공존한다. +- legacy packet-substring 계층은 2026-08-13 제거됐다. 현재 domain 계층은 parsed SNI, + IP 계층은 CIDR allow contract만 사용한다. --- diff --git a/docs/analysis/12-anvil-project-process-status-review-2026-08-13.md b/docs/analysis/12-anvil-project-process-status-review-2026-08-13.md index bd6d38b..a16b6ea 100644 --- a/docs/analysis/12-anvil-project-process-status-review-2026-08-13.md +++ b/docs/analysis/12-anvil-project-process-status-review-2026-08-13.md @@ -11,6 +11,8 @@ source-level probe로 재검토. 핵심 판정 승인, 아래 두 항목 정정 - secret scan 원인은 한 개가 아니라 같은 test file의 OpenAI·Google sentinel 2개 - PR #109에는 merge 당시 미해결 CodeRabbit documentation comment 2개가 존재 +- 실행 갱신: 같은 날 사용자가 후속 작업 1–7 전체 실행을 승인했다. 원 조사 snapshot은 + 보존하고 §14에 PR #110 merge 이후 gate closure 진행 상태를 추가했다. - 문서 목적: 다음 정식 태그 이전에 현재 공정 단계, 증적 공백, release blocker와 후속 작업을 하나의 기준 보고서로 고정 - 비밀정보 처리: 실제 credential, 호스트 주소, password, private key는 기록하지 않음 @@ -517,3 +519,55 @@ tag authorization: hold ``` 구현 자체보다 릴리스 공정의 강제성·재현성·증적 일치가 현재 가장 큰 프로젝트 위험이다. + +## 14. 후속 작업 1–7 실행 갱신 + +### 14.1 완료 또는 코드상 폐쇄 + +| 항목 | 결과 | 증거 | +|---|---|---| +| PR #110 review/merge | 완료 | exact head `c394f7d...`의 Go CI green, unresolved thread 0에서 merge commit `794d0ae...`로 `main` 병합. CodeRabbit 완료 리뷰는 merge 직후 회수 | +| strict secret gate | 완료 | tracked tree PASS, scanner allowlist/regex 완화 없음 | +| KVM release-candidate gate | 완료 | full E2E `All test steps passed`, lifecycle/semantic/flock smoke는 선행 handoff에서 통과 | +| `allow_hosts` 제거 | 구현·local 검증 완료, PR 대기 | field/validation/iptables string matcher 제거, non-empty/empty/`null`/mixed-case key loud rejection, unrelated unknown metadata 호환 | +| VM 삭제 실패 cleanup | 구현·local 검증 완료, PR 대기 | CodeRabbit 사후 Major finding. forced dm failure 뒤 양 loop/store/TAP-IP cleanup continuation test, KVM resource inventory clean | +| npm audit | 폐쇄 | High 2/Moderate 2 → 0, clean install/check/build 통과 | +| Web/secret CI | 구현 완료, PR 대기 | `web-and-security`, `secret-scan` 독립 job, `contents: read` | +| version policy | 결정 규칙 확정 | upstream 첫 post-`v0.7.0` tag `vX.Y.Z` → `anvil-vX.Y.Z`; downstream-only 번호 금지 | + +### 14.2 PR #110 사후 review disposition + +CodeRabbit가 merge 완료 직후 4개 actionable comment를 게시했다. + +1. lifecycle 링크 복구 지적: **invalid**. 이 프로젝트 지침은 zone 상대 경로를 사용하며 + `../../../docs/governance/codex-lifecycle-control-plane.md`는 실제 zone 문서로 해석돼 + 존재한다. changed-document relative-link scan도 통과했다. +2. named Go build evidence 누락: **valid**, plan/handoff 보정과 실제 세 build 실행. +3. spec 수용 기준 11 누락: **valid**, plan 완료 조건을 1–11로 보정. +4. dm remove failure 뒤 loop/store cleanup 중단: **valid Major**, 별도 + `vm-deletion-failure-cleanup` full lifecycle/TDD로 수정. + +### 14.3 아직 열린 blocker + +1. **Host A1/A2:** 비밀 주소를 출력하지 않는 재점검에서 A1 endpoint 한 대는 tcp/22에 + 도달하지만 현재 개발 키를 거부하고, 나머지 두 배포 endpoint는 tcp/22 도달 불가였다. + password rotation, key rollout, permission remediation을 실행할 인증 경로가 없다. +2. **Next version number:** upstream latest/main이 계속 `v0.7.0`이라 결정 규칙의 입력이 + 없다. 번호는 의도적으로 미할당이다. +3. **Remote integration:** `agent/next-release-gates` exact-SHA CI/review/merge가 남았다. +4. **Branch protection:** 새 CI context가 `main`에 병합된 뒤 적용해야 한다. collaborator가 + owner 1명뿐이라 admin-enforced approval 1 설정 후 두 번째 reviewer가 필요하다. + +### 14.4 갱신 판정 + +```text +released baseline: anvil-v0.7.0 = operate +main after PR #110: release-gate evidence merged +next-gates branch: code-review -> remote CI/merge +host security gate: blocked (access unavailable) +next version number: blocked (no post-v0.7.0 upstream tag) +tag authorization: hold +``` + +코드·CI 측 P0 대부분은 폐쇄됐지만 host security와 upstream version input은 아직 외부 +blocker다. 따라서 후속 작업 7의 “blocker 종료 전 tag 금지”는 계속 유효하다. diff --git a/docs/architecture/multi-tenant-roadmap.md b/docs/architecture/multi-tenant-roadmap.md index 592c768..d232208 100644 --- a/docs/architecture/multi-tenant-roadmap.md +++ b/docs/architecture/multi-tenant-roadmap.md @@ -243,11 +243,11 @@ daemon은 선택된 policy를 VM/snapshot/restore metadata에 보존한다. `den host-local `iptables` reject rule로 강제한다. `profile`은 `configs/profiles/{profile}/egress.json`, `EPHEMERA_EGRESS_PROFILE_DIR` 또는 `ANVIL_EGRESS_PROFILE_DIR` 아래의 -profile별 policy 파일이 있을 때 allow CIDR, allow host string match(legacy -`allow_hosts`, substring 기반 deprecated), `allow_sni`(파싱된 ClientHello SNI -기반 default-deny `:443` 필터 — ADR-0002), DNS server allowlist와 DNS/default -reject rule을 적용한다. policy 파일이 없으면 기존 profile 동작과 호환되도록 -no-op이다. SNI 필터의 계약·위협 모델·잔여 위험은 ADR-0002가 권위다. +profile별 policy 파일이 있을 때 allow CIDR, `allow_sni`(파싱된 ClientHello SNI 기반 +default-deny `:443` 필터 — ADR-0002), DNS server allowlist와 DNS/default reject rule을 +적용한다. 제거된 `allow_hosts` key는 profile decode에서 loud fail-closed 거부된다. +policy 파일이 없으면 기존 profile 동작과 호환되도록 no-op이다. SNI 필터의 +계약·위협 모델·잔여 위험은 ADR-0002가 권위다. ## 감사 저장소 diff --git a/docs/architecture/service-logic.md b/docs/architecture/service-logic.md index de16b56..5da4ba6 100644 --- a/docs/architecture/service-logic.md +++ b/docs/architecture/service-logic.md @@ -264,10 +264,11 @@ Runtime audit record는 symlink path append를 거부하고 `0600`으로 저장 - `deny_all`: VM guest IP 기준 default reject rule을 적용한다. - `profile`: `configs/profiles/{profile}/egress.json`, `EPHEMERA_EGRESS_PROFILE_DIR`, `ANVIL_EGRESS_PROFILE_DIR` 아래의 profile별 - `egress.json`을 읽고 allow CIDR, allow host string match(legacy `allow_hosts`, - substring 기반 deprecated), `allow_sni`(파싱된 ClientHello SNI 기반 default-deny - `:443` 필터 — ADR-0002), DNS server allowlist와 default reject rule을 적용한다. - policy 파일이 없으면 no-op이다. + `egress.json`을 읽고 allow CIDR, `allow_sni`(파싱된 ClientHello SNI 기반 + default-deny `:443` 필터 — ADR-0002), DNS server allowlist와 default reject rule을 + 적용한다. 제거된 `allow_hosts` key가 있으면 값이 empty/`null`이어도 decode 단계에서 + `allow_sni`/`allow_cidrs` migration error로 loud fail-closed 거부한다. 다른 unknown + metadata의 기존 ignore 동작은 유지한다. policy 파일이 없으면 no-op이다. - `allow_all`: 기존 NAT outbound 동작을 유지한다. 선택된 policy는 VM/snapshot/restore metadata에 보존된다. host rule cleanup은 diff --git a/docs/guides/mcp-adapter.md b/docs/guides/mcp-adapter.md index b87f2a4..301bb4f 100644 --- a/docs/guides/mcp-adapter.md +++ b/docs/guides/mcp-adapter.md @@ -292,21 +292,21 @@ scheduler 운영 경계처럼 loopback/private network 또는 reverse proxy poli `deny_all` egress policy는 host `iptables` reject rule로 강제한다. `profile` policy는 `configs/profiles/{profile}/egress.json`, `EPHEMERA_EGRESS_PROFILE_DIR`, `ANVIL_EGRESS_PROFILE_DIR` 아래의 profile별 -`egress.json`이 있을 때 allow CIDR/host/SNI/DNS rule을 적용하고, policy 파일이 +`egress.json`이 있을 때 allow CIDR/SNI/DNS rule을 적용하고, policy 파일이 없으면 기존 profile 호환성을 위해 no-op이다. 예시: ```json { "allow_cidrs": ["203.0.113.10/32"], - "allow_hosts": ["api.anthropic.com"], "allow_sni": ["api.anthropic.com"], "dns_servers": ["1.1.1.1"] } ``` `allow_sni`는 파싱된 ClientHello SNI 기준 :443 도메인 allowlist다(TCP+QUIC/UDP -모두 적용). `allow_hosts`의 packet-string 매치보다 정밀하며, 신규 profile은 -`allow_sni`를 쓴다. 상세는 [security-and-resilience.md](security-and-resilience.md)와 +모두 적용). 제거된 `allow_hosts` key는 값이 비어 있어도 loud fail-closed로 거부된다. +domain은 `allow_sni`, IP/CIDR은 `allow_cidrs`를 쓴다. 상세는 +[security-and-resilience.md](security-and-resilience.md)와 [ADR-0002](../adr/0002-egress-sni-transparent-filter.md)를 참고한다. Optional trace export는 `ANVIL_OTEL_EXPORTER_OTLP_ENDPOINT` 또는 diff --git a/docs/operations/2026-08-13-allow-hosts-removal-handoff.md b/docs/operations/2026-08-13-allow-hosts-removal-handoff.md new file mode 100644 index 0000000..d5dadf6 --- /dev/null +++ b/docs/operations/2026-08-13-allow-hosts-removal-handoff.md @@ -0,0 +1,91 @@ +# `allow_hosts` 제거 handoff + +## 문서 상태 + +- 날짜: 2026-08-13 +- topic: `allow-hosts-removal` +- branch: `agent/next-release-gates` +- 설계: + [`2026-08-13-allow-hosts-removal-design.md`](../superpowers/specs/2026-08-13-allow-hosts-removal-design.md) +- 계획: + [`2026-08-13-allow-hosts-removal.md`](../superpowers/plans/2026-08-13-allow-hosts-removal.md) + +## Release Scope + +- `egressProfile.AllowHosts`와 iptables `-m string --string ... --algo bm` apply path 제거 +- legacy `allow_hosts` key의 명시적 loud fail-closed rejection +- domain → `allow_sni`, IP/CIDR → `allow_cidrs` 문서화 +- canonical context, README, release notes, architecture, ADR, operator guide 정합화 + +tag 또는 GitHub Release는 범위 밖이다. + +## Verification + +### TDD / effect-anchored evidence + +구현 전 targeted test는 non-empty, empty, `null` 세 legacy profile을 모두 `ok=true`, +`err=nil`로 받아 실패했다. non-empty case에서는 기존 deprecation warning까지 발화해 +legacy apply contract가 실제로 살아 있음을 보였다. + +구현 후: + +- `TestLoadEgressProfileRejectsRemovedAllowHosts`: non-empty/empty/`null`/mixed-case 모두 통과 +- error는 `allow_hosts`, `allow_sni`, `allow_cidrs`를 포함하고 hostname value는 미포함 +- `TestLoadEgressProfileKeepsUnrelatedUnknownFieldCompatibility`: 통과 +- `TestLoadEgressProfileAndPlanAllowlistCommands`: CIDR/SNI/DNS current path 통과 +- production source search: `AllowHosts`, `--string`, `--algo`, `-m string` 0건 + +### Repository gate + +- `go test ./cmd/goose-daemon -count=1`: 통과 +- `go test ./... -count=1`: 통과 +- `go test -race ./... -count=1`: 통과 +- named daemon/MCP/scheduler build: 통과 +- `go vet ./...`, `go mod verify`, `gofmt -l .`: 통과/빈 출력 +- `govulncheck ./...`: reachable vulnerability 0 +- `bash scripts/secret-scan.sh`: tracked tree PASS +- Markdown relative links 155건: PASS +- `git diff --check`: PASS +- full KVM `e2e_test.sh`: `All test steps passed` + +## Audit + +- removed key는 값 형태와 무관하게 typed validation/apply 전에 종료된다. +- error/log에 legacy hostname value를 반복하지 않는다. +- global `DisallowUnknownFields`를 도입하지 않아 unrelated unknown metadata behavior는 불변이다. +- profile/config example에서 `allow_hosts` 사용 0건이다. +- historical deprecation spec/plan과 과거 handoff의 당시 사실은 보존했다. +- tag 생성 없음. + +## Blockers + +- 이 변경의 PR exact-SHA CI와 merge가 아직 남아 있다. +- 다음 public version number는 upstream post-`v0.7.0` tag 부재로 미할당이다. +- deployment host credential/key/permission remediation이 외부 접근 부재로 미완료다. + +## Warnings + +- `allow_hosts`를 가진 외부 operator profile은 upgrade 후 VM spawn/profile load가 실패한다. + key 삭제와 migration이 선행돼야 한다. + +## Residual Risk + +- `allow_sni`의 spoofing/fronting/ECH와 신뢰 golden-image 위협 모델 한계는 ADR-0002에 + 그대로 남는다. +- 다른 unknown JSON field는 호환성 때문에 계속 무시된다. 전체 strict schema는 별도 결정이다. + +## Current Lifecycle Stage + +local implement, verification, code review와 release handoff 작성이 끝났다. PR exact-SHA +remote CI/merge 전이므로 `operate`에는 진입하지 않았다. + +## Next Action + +통합 변경을 PR로 올리고 exact-SHA CI/CodeRabbit review를 처리한다. + +## Follow-Up Tasks + +1. 외부 profile에서 `allow_hosts` key 제거 여부를 배포 전 확인 +2. 통합 PR review/merge +3. host security blocker와 upstream-derived version blocker 해소 전 tag 금지 + diff --git a/docs/operations/2026-08-13-release-gate-closure-handoff.md b/docs/operations/2026-08-13-release-gate-closure-handoff.md index e74bf57..0eea4b4 100644 --- a/docs/operations/2026-08-13-release-gate-closure-handoff.md +++ b/docs/operations/2026-08-13-release-gate-closure-handoff.md @@ -7,6 +7,7 @@ - branch: `agent/release-gate-closure` - 기준 parent: `main@3033cddac5a6764c5d1cb12221e3a2d88b1928db` - draft PR: [#110](https://github.com/HardcoreMonk/anvil/pull/110) +- merge: 완료, `main@794d0ae4debc02612106b647e680084167922ab5` - code-bearing commit: `576165429b1ecee8b25697b103b533e452a9cb98` - 설계: [`2026-08-13-release-gate-closure-design.md`](../superpowers/specs/2026-08-13-release-gate-closure-design.md) @@ -54,6 +55,9 @@ scanner allowlist는 추가하지 않았다. - `go test ./... -count=1`: 통과 - `go test -race ./... -count=1`: 통과 - `go build ./...`: 통과 +- `go build -o anvil-daemon ./cmd/goose-daemon/`: 통과 +- `go build ./cmd/anvil-mcp`: 통과 +- `go build ./cmd/anvil-scheduler`: 통과 - `go vet ./...`: 통과 - `go mod verify`: 통과 - `gofmt -l .`: 출력 없음 @@ -83,6 +87,9 @@ sudo -n env PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr 결과: `All test steps passed`. +AGENTS의 표준 표기인 `sudo bash e2e_test.sh`와 동형이며, 실제 실행은 sudo secure PATH에 +Go를 명시하기 위해 위 `sudo -n env PATH=... bash e2e_test.sh`를 사용했다. + 확인된 주요 경로: - VM spawn/task/stream/delete @@ -130,10 +137,15 @@ flock prove-broken은 daemon authorship guard가 실제로 roster 밖 author를 - scanner 정규식/실패 동작: 불변 - full E2E 전 삭제 대상 `vms/`, `snapshots/`, `flocks/`, `/tmp/goose-workspaces/`: 모두 empty 확인 -- E2E/MCP 종료 후 VM·dm-snapshot·loop device는 정리됐으나 root-owned Town Wall log - 10개와 0-byte workspace placeholder 7개가 남은 것을 사후 probe에서 확인했다. 실행 전 - empty 상태와 ID를 대조한 뒤 이번 test artifact만 비재귀 삭제했고, `vms/`, - `snapshots/`, `flocks/` entry 0 및 `/tmp/goose-workspaces/` 부재를 재확인했다. +- E2E/MCP 종료 후 resource별 probe: VM state 0, TAP/IP lease 0, project bind mount 0, + `cow-vm-*` dm-snapshot 0, anvil workspace-backed loop device 0, sparse `.cow` store 0. + root-owned Town Wall log 10개와 0-byte workspace placeholder 7개는 실행 전 empty 상태와 + ID를 대조한 뒤 이번 test artifact만 비재귀 삭제했고, `vms/`, `snapshots/`, `flocks/` + entry 0 및 `/tmp/goose-workspaces/` 부재를 재확인했다. +- PR #110 사후 CodeRabbit review에서 dm remove failure 뒤 후속 loop/store cleanup이 + 중단되는 source path를 발견했다. 이 정상-path inventory만으로 failure-path invariant를 + 과장하지 않고, `vm-deletion-failure-cleanup` full lifecycle의 forced failure test와 + 수정으로 별도 폐쇄한다. 해당 lifecycle이 green이기 전 release gate는 열린 상태다. - tag/release/deployment mutation: 없음 ## Blockers @@ -179,6 +191,12 @@ adequacy 관점에서 검토했다. - 검증 유효성: secret control은 prove-broken, runtime unit test, independent raw-source scan, 음성대조를 모두 가짐. MCP flock은 실제 daemon의 roster guard와 history를 관측 +CodeRabbit 완료 리뷰는 merge 직후 도착했다. 4건 중 zone-relative lifecycle link 지적은 +실제 target 존재로 invalid 처리했고, named build와 수용 기준 11 누락은 문서 보정했다. +dm removal failure 뒤 loop/store cleanup 중단 Major finding은 별도 +[`vm-deletion-failure-cleanup`](2026-08-13-vm-deletion-failure-cleanup-handoff.md) +full lifecycle/TDD로 수정·검증했다. + ## Current Lifecycle Stage local `code-review`와 code-bearing exact SHA remote CI가 완료됐다. version, deprecated diff --git a/docs/operations/2026-08-13-release-governance-hardening-handoff.md b/docs/operations/2026-08-13-release-governance-hardening-handoff.md new file mode 100644 index 0000000..07f5a3f --- /dev/null +++ b/docs/operations/2026-08-13-release-governance-hardening-handoff.md @@ -0,0 +1,98 @@ +# Release governance hardening handoff + +## 문서 상태 + +- 날짜: 2026-08-13 +- topic: `release-governance-hardening` +- branch: `agent/next-release-gates` +- 설계: + [`2026-08-13-release-governance-hardening-design.md`](../superpowers/specs/2026-08-13-release-governance-hardening-design.md) +- 계획: + [`2026-08-13-release-governance-hardening.md`](../superpowers/plans/2026-08-13-release-governance-hardening.md) + +## Release Scope + +- npm transitive advisory remediation +- Node 22 Web check/build/audit CI와 tracked-tree secret CI +- upstream-derived next version selection rule +- PR merge 후 strict `main` branch protection 적용 +- no-tag audit + +## Verification + +### npm prove-broken / fixed + +변경 전 `npm audit --json`: + +- High 2: `nanoid`, `postcss` +- Moderate 2: `esbuild`, direct `svelte-i18n` effect + +patched graph: + +- `esbuild@0.28.2` override, Vite와 dedupe +- `postcss@8.5.26` +- `nanoid@3.3.18` +- direct `svelte-i18n@4.0.1` 유지 + +결과: + +- `npm ci`: 통과, vulnerabilities 0 +- `npm run check`: error 0, 기존 warning 10 +- `npm run build`: 통과, embedded bundle deterministic +- `npm audit --audit-level=moderate`: 0건, exit 0 + +### CI / repository + +- `.github/workflows/ci.yml` PyYAML parse: PASS +- job `web-and-security`: clean install/check/build/moderate audit +- job `secret-scan`: tracked-tree scanner +- workflow permissions: `contents: read` +- Go full/race/named builds/vet/gofmt/govulncheck: 통과 +- Markdown relative links 155건, `git diff --check`: PASS + +remote exact-SHA 결과와 branch-protection read-back은 PR/merge 후 이 문서에 추가한다. + +## Audit + +- direct Svelte package downgrade 없음. +- audit ignore/allowlist 없음. +- CI에 write permission 또는 secret 전달 없음. +- upstream `git ls-remote --tags upstream`: latest `v0.7.0`. +- upstream main: `8db2fb4...`, `v0.7.0` exact. +- tag 생성 없음. + +## Blockers + +1. next public version number는 post-`v0.7.0` upstream tag 부재로 미할당이다. +2. deployment host A1/A2는 인증/도달 경로 부재로 미완료다. +3. 통합 PR CI/review/merge와 merge 후 branch protection 적용이 남았다. + +## Warnings + +- Svelte `state_referenced_locally` warning 10건은 기존 상태이며 CI는 error로 취급하지 않는다. +- collaborator는 repository owner 1명뿐이다. admin-enforced approval 1을 적용하면 두 번째 + eligible reviewer가 추가될 때까지 이후 PR merge가 차단된다. + +## Residual Risk + +- npm override는 upstream direct dependency range 밖의 `esbuild`를 강제한다. clean + install/check/build로 현재 compatibility를 검증했지만 direct package가 dependency를 + 갱신하면 override를 재검토해야 한다. +- npm advisory DB 변화는 향후 CI를 새로 실패시킬 수 있으며 이는 의도된 fail-closed다. + +## Current Lifecycle Stage + +local implement/verification/code review 완료. PR exact-SHA 검증과 external protection이 +남아 있어 `operate` 미진입이다. + +## Next Action + +통합 PR을 생성하고 CI/CodeRabbit review를 처리해 병합한 뒤 branch protection을 적용한다. + +## Follow-Up Tasks + +1. exact-SHA remote CI와 review/merge +2. `main` protection 적용/read-back +3. 두 번째 eligible reviewer 추가 +4. host A1/A2와 upstream version input 전 tag 금지 + diff --git a/docs/operations/2026-08-13-vm-deletion-failure-cleanup-handoff.md b/docs/operations/2026-08-13-vm-deletion-failure-cleanup-handoff.md new file mode 100644 index 0000000..9daa9dd --- /dev/null +++ b/docs/operations/2026-08-13-vm-deletion-failure-cleanup-handoff.md @@ -0,0 +1,111 @@ +# VM 삭제 실패 cleanup handoff + +## 문서 상태 + +- 날짜: 2026-08-13 +- topic: `vm-deletion-failure-cleanup` +- branch: `agent/next-release-gates` +- trigger: merged PR #110에 대한 CodeRabbit 사후 Major review +- 설계: + [`2026-08-13-vm-deletion-failure-cleanup-design.md`](../superpowers/specs/2026-08-13-vm-deletion-failure-cleanup-design.md) +- 계획: + [`2026-08-13-vm-deletion-failure-cleanup.md`](../superpowers/plans/2026-08-13-vm-deletion-failure-cleanup.md) + +## Release Scope + +- `dmsetup remove --retry` 소진 뒤 early return 제거 +- COW/base loop detach와 full-delete sparse `.cow` unlink 계속 시도 +- 모든 storage cleanup error aggregation +- storage failure와 무관한 TAP/IP release orchestration 회귀 test +- PR #110 named build/acceptance/resource evidence 보정 + +## Verification + +### Prove-broken-first / forced failure + +초기 test compile은 `dmSnapshotTeardownOps`와 `teardownDMSnapshotWithOps`가 없어 실패했다. +기존 source control flow는 dm remove 실패 직후 return해 loop/store 호출에 도달하지 않았다. + +구현 후 `TestTeardownDMSnapshotContinuesAfterDMRemoveFailure`가 다음을 강제·검증한다. + +- dm remove는 timeout failure +- COW loop와 base loop detach 모두 호출 +- full teardown은 store remove 호출, keep-store는 미호출 +- dm, 양 loop, store failure detail이 joined error에 포함 + +`TestCleanupDeletedVMContinuesNetworkReleaseAfterDMSnapshotFailure`는 injected storage error 뒤 +TAP/IP release seam이 호출되는 순서를 검증한다. + +### Real KVM / independent inventory + +`sudo -n env PATH=... bash e2e_test.sh`: `All test steps passed`. + +- COW restore/delete: dm device 1개 활성 관측 후 delete, dm 0/store 제거 확인 +- concurrent restore cleanup: `cow-vm-*` 0 +- COW spawn graceful/crash recovery와 orphan reclaim: 통과 +- disk-missing recovery: stale TAP release 확인 + +종료 후 독립 host inventory: + +| Resource | 결과 | +|---|---:| +| `tap[0-9]+` interface | 0 | +| project bind mount | 0 | +| `cow-vm-*` dm-snapshot | 0 | +| project workspace-backed loop | 0 | +| sparse `.cow` store | 0 | +| `vms/`, `snapshots/` entry | 0 | + +`goose-br0`의 link-down base route는 bridge baseline이며 guest TAP/IP lease가 아니다. +E2E가 남긴 root-owned Town Wall log 8개와 0-byte rootfs placeholder 7개는 실행 전 empty +상태와 대조해 이번 test artifact임을 확인한 뒤 삭제했다. 최종 `flocks/` entry 0, +`/tmp/goose-workspaces/` absent다. + +### Repository gate + +- targeted forced-failure tests: 통과 +- Go full/race/named builds/vet/module verify/gofmt: 통과 +- govulncheck reachable 0, tracked secret scan PASS +- full KVM E2E: 통과 +- `git diff --check`: 통과 + +## Audit + +- bounded 10초 dm retry는 유지했다. +- failure를 성공으로 가장하지 않고 error와 cleanup failure metric을 보존한다. +- keep-store daemon shutdown contract는 store unlink를 하지 않는다. +- HTTP/MCP/schema/storage format 변경 없음. +- tag 생성 없음. + +## Blockers + +- 통합 PR exact-SHA CI와 merge가 남아 있다. +- 실제 kernel이 resource를 계속 busy로 유지하면 후속 detach도 실패할 수 있다. 이 경우 + error와 metric이 남으며 release gate는 host inventory clean 전까지 열려 있다. +- deployment host security operations가 미완료다. + +## Warnings + +- `TeardownBindMount`는 legacy best-effort void API라 unmount 실패 detail을 반환하지 않는다. + 정상 KVM inventory는 clean이지만 failure error aggregation은 dm path만 강화됐다. + +## Residual Risk + +- kernel refusal 자체를 소프트웨어가 강제로 성공시킬 수 없다. 이번 보장은 “모든 단계를 + 계속 시도하고 잔여를 관측”하는 control-flow invariant다. + +## Current Lifecycle Stage + +implement, local verification, code review, release handoff 완료. remote PR 검증 전이므로 +`operate` 미진입이다. + +## Next Action + +통합 PR에서 exact-SHA CI와 review를 받고 병합한다. + +## Follow-Up Tasks + +1. branch exact-SHA CI/CodeRabbit review +2. 배포 전 host resource inventory 재확인 +3. host/security/version blockers 전 tag 금지 + diff --git a/docs/operations/runbook.md b/docs/operations/runbook.md index 043513f..2b2f01d 100644 --- a/docs/operations/runbook.md +++ b/docs/operations/runbook.md @@ -276,20 +276,18 @@ iptables -j NFQUEUE --help >/dev/null 2>&1 && echo "NFQUEUE target OK" ``` 복구: `nfnetlink_queue` 커널 모듈을 로드하거나(`modprobe nfnetlink_queue`), -`allow_sni` 없이 `allow_cidrs`만 쓰는 profile로 전환한다(`allow_hosts`는 deprecated — -아래). +`allow_sni` 없이 `allow_cidrs`만 쓰는 profile로 전환한다. `profile` egress를 쓰는 모든 host는 NFQUEUE 사용 가능이 baseline 요구다 — 스케줄러는 아직 별도 capability 축으로 이를 걸러내지 않으므로, `allow_sni` profile을 쓰는 tenant는 대상 host 전체가 NFQUEUE를 지원하는지 운영자가 사전 확인해야 한다. -**`allow_hosts` deprecation(마이그레이션)**: `allow_hosts`(packet substring match)는 -deprecated다 — profile 로드 시 daemon이 런타임 경고를 남기고(`loadEgressProfile`), -**다음 tagged anvil 릴리즈에서 제거**된다(제거 후 잔존 `allow_hosts` profile은 loud -fail-closed로 거부, ADR-0002 OQ8). 마이그레이션: 도메인(`api.example.com`) → +**제거된 `allow_hosts` 마이그레이션**: packet substring match였던 `allow_hosts`는 +제거됐다(ADR-0002 OQ8). profile JSON에 key가 남아 있으면 non-empty/empty/`null`과 +무관하게 load가 loud fail-closed로 실패한다. 도메인(`api.example.com`)은 `allow_sni`(exact 또는 `*.` wildcard — 파싱된 ClientHello SNI 강제), 고정 IP/CIDR -백엔드 → `allow_cidrs`. substring match와 달리 `allow_sni`는 정확 SNI 매치라 부분 -문자열 매치가 없으니 도메인 목록을 명시화한다. +backend는 `allow_cidrs`로 옮긴 뒤 key 자체를 삭제한다. substring match와 달리 +`allow_sni`에는 부분 문자열 match가 없으므로 도메인 목록을 명시화한다. ### (c) verdict 루프 사망 시 :443 차단(fail-closed) 진단 diff --git a/docs/operations/security-policy.md b/docs/operations/security-policy.md index 5c75784..4261bdb 100644 --- a/docs/operations/security-policy.md +++ b/docs/operations/security-policy.md @@ -148,26 +148,22 @@ policy를 VM/snapshot/restore metadata에 보존하고, host-local network rule - `deny_all`: guest IP 기준 `iptables FORWARD` reject rule을 적용한다. - `profile`: `configs/profiles/{profile}/egress.json`, `EPHEMERA_EGRESS_PROFILE_DIR`, `ANVIL_EGRESS_PROFILE_DIR` 아래의 profile별 - `egress.json`이 있으면 allow CIDR, allow host string match, `allow_sni` - transparent SNI 필터(아래), DNS server allowlist와 default reject rule을 - 적용한다. + `egress.json`이 있으면 allow CIDR, `allow_sni` transparent SNI 필터(아래), DNS + server allowlist와 default reject rule을 적용한다. - `allow_all`: 기존 NAT outbound 동작을 유지한다. `egress.json`은 secret 저장소가 아니다. provider API key, Bearer token, 내부 -credential을 넣지 않는다. `allow_hosts` rule은 **legacy/deprecated**다 — -packet string match(`-m string --algo bm`) 기반의 coarse host allowlist이며, -TLS ClientHello가 여러 TCP 세그먼트로 쪼개지면 매치가 실패하고 SNI가 아닌 -위치의 같은 문자열에도 우연히 매치한다. 신규 profile은 `allow_sni`를 쓴다. -`allow_sni`가 없고 `allow_hosts`만 있는 기존 profile은 무변경 동작을 -유지한다(하위호환). policy 파일이 없는 `profile`은 기존 profile 호환성을 -위해 no-op이다. +credential을 넣지 않는다. packet string match(`-m string --algo bm`) 기반이던 +`allow_hosts` rule은 제거됐다. 잔존 key는 값이 empty/`null`이어도 profile load를 +실패시키며 값은 error에 반복하지 않는다. domain은 `allow_sni`, 고정 IP/CIDR은 +`allow_cidrs`로 옮기고 key를 삭제한다. policy 파일이 없는 `profile`은 기존 profile +호환성을 위해 no-op이다. ### `allow_sni` — transparent SNI 필터 (ADR-0002) `allow_sni []string`은 실제 파싱된 TLS ClientHello의 `server_name` -extension을 :443 새 TCP 흐름 단위로 강제하는 신규·additive 필드다(기존 -`allow_hosts`의 재해석이 아니며, `allow_cidrs`/`dns_servers`와 병렬로 -동작한다). exact match가 기본이고 `*.example.com` 형태로 leading label +extension을 :443 새 TCP 흐름 단위로 강제하며 `allow_cidrs`/`dns_servers`와 병렬로 +동작한다. exact match가 기본이고 `*.example.com` 형태로 leading label wildcard(한 개 이상 라벨)를 지원한다 — 임의 위치 glob은 비지원. **메커니즘**: :443 새 흐름의 ClientHello 세그먼트가 diff --git a/docs/superpowers/grill-me/2026-08-13-allow-hosts-removal.md b/docs/superpowers/grill-me/2026-08-13-allow-hosts-removal.md new file mode 100644 index 0000000..5129d72 --- /dev/null +++ b/docs/superpowers/grill-me/2026-08-13-allow-hosts-removal.md @@ -0,0 +1,71 @@ +# `allow_hosts` 제거 grill-me 기록 + +**날짜:** 2026-08-13 + +**topic:** `allow-hosts-removal` + +**설계:** +[`2026-08-13-allow-hosts-removal-design.md`](../specs/2026-08-13-allow-hosts-removal-design.md) + +## 압박 질문과 결정 + +### Q1. struct field만 지우면 충분한가? + +아니다. Go `json.Unmarshal`은 unknown field를 무시하므로 오래된 profile이 성공한 것처럼 +보이면서 의도한 allow rule만 잃는다. `allow_hosts` raw key를 decode boundary에서 +명시적으로 거부한다. + +### Q2. 빈 배열은 실제 rule을 만들지 않는데 허용해도 되는가? + +허용하지 않는다. 빈 key도 stale template/automation의 증거이며 다음 편집에서 위험한 +값이 다시 들어갈 수 있다. key 존재 자체를 migration failure로 본다. + +### Q3. `DisallowUnknownFields`가 더 단순하지 않은가? + +더 넓은 계약 변경이다. 제거된 단일 위험 field를 닫는 목적과 무관한 metadata까지 +거부할 수 있다. 이번에는 explicit legacy rejection을 사용하고 전체 strict schema는 +별도 결정으로 남긴다. + +### Q4. parse error 전에 migration error를 내면 malformed JSON을 잘못 분류할 수 있는가? + +raw map unmarshal 자체가 먼저 JSON 문법을 검증한다. 문법이 깨졌으면 parse error를, +유효한 object에 `allow_hosts`가 있으면 removal error를 반환한다. + +### Q5. JSON root가 배열이나 scalar이면 어떻게 되는가? + +현재 typed struct unmarshal의 오류 semantics를 유지해야 한다. raw legacy probe와 typed +unmarshal을 모두 통과해야 하며, root-shape 회귀 테스트는 기존/추가 test로 보호한다. + +### Q6. migration error에 host value를 넣어야 디버깅이 쉽지 않은가? + +값은 불필요하다. field 이름과 대체 field만으로 수정 가능하며 hostname이 내부 목적지를 +노출할 수 있으므로 value는 반복하지 않는다. + +### Q7. 기존 `allow_hosts` validation helper를 남겨 호환 계층으로 쓸 이유가 있는가? + +없다. apply path가 제거된 helper는 잘못된 호환 기대와 dead code만 남긴다. shared domain +charset helper는 `allow_sni` 전용 설명으로 정리한다. + +### Q8. 과거 spec과 ADR의 deprecation 문장을 모두 제거해야 하는가? + +과거 lifecycle 기록은 당시 사실이므로 보존한다. ADR은 현재 decision status를 업데이트하고 +canonical/current 운영 문서는 “removed”로 맞춘다. + +### Q9. 이 변경만으로 다음 tag를 만들어도 되는가? + +아니다. upstream version alignment, Web dependency, deployment host, branch governance가 +독립 gate다. 제거 완료는 필요조건이지 release 승인 자체가 아니다. + +### Q10. rollback은 무엇인가? + +코드 rollback은 field/apply/warning을 되살리지만 보안상 열화다. release 전 발견된 +호환성 문제는 profile을 `allow_sni`/`allow_cidrs`로 마이그레이션하는 것이 우선이다. +이미 배포된 artifact rollback은 별도 release 승인 없이 수행하지 않는다. + +## Grill 결과 + +- 열린 설계 질문: 없음 +- design blocker: 없음 +- 핵심 고정 결정: explicit key rejection, empty/null 포함, global strictness 확대 금지, + value 비노출, tag 금지 + diff --git a/docs/superpowers/grill-me/2026-08-13-release-governance-hardening.md b/docs/superpowers/grill-me/2026-08-13-release-governance-hardening.md new file mode 100644 index 0000000..8fb2365 --- /dev/null +++ b/docs/superpowers/grill-me/2026-08-13-release-governance-hardening.md @@ -0,0 +1,61 @@ +# Release governance hardening grill-me 기록 + +**날짜:** 2026-08-13 + +**topic:** `release-governance-hardening` + +**설계:** +[`2026-08-13-release-governance-hardening-design.md`](../specs/2026-08-13-release-governance-hardening-design.md) + +## 압박 질문과 결정 + +### Q1. `npm audit fix --force`를 쓰면 더 간단한가? + +direct `svelte-i18n`을 3.x로 downgrade하는 major change를 제안한다. Svelte 5 app behavior +risk가 크므로 patched transitive override와 check/build를 선택한다. + +### Q2. audit가 dev-server advisory인데 production blocker인가? + +현재 repository가 source와 build toolchain을 배포·개발 contract로 포함하고, High +postcss/nanoid도 함께 있다. CI가 moderate부터 0을 요구해 supply-chain baseline을 단순하게 +유지한다. + +### Q3. Web과 secret을 Go job에 합치면 안 되는가? + +가능하지만 failure ownership과 required context가 흐려진다. 독립 job은 병렬화되고 +branch rule에서 각 control의 존재를 확인할 수 있다. + +### Q4. CodeRabbit가 approval 1건을 대신할 수 있는가? + +아니다. 현재 bot은 status/comment review이며 GitHub approving review가 아니다. strict +human approval을 만족하려면 두 번째 eligible collaborator가 필요하다. + +### Q5. owner 한 명인데 admin enforce 1 approval은 지나치지 않은가? + +사용자는 required review를 요청했고 강한 release posture가 목적이다. protection은 +reversible하며 repository admin이 reviewer를 추가하거나 rule을 변경할 수 있다. silent +bypass보다 명시적 잠금과 follow-up을 선택한다. + +### Q6. 다음 version을 `anvil-v0.8.0`으로 확정하면 안 되는가? + +upstream alignment policy와 충돌한다. upstream에는 post-v0.7.0 tag가 없다. 실제 번호를 +추측하지 않고 first-next-upstream transform을 확정하는 것이 현재 가능한 유일한 정합 +결정이다. + +### Q7. branch rule을 PR 전에 켜면 어떻게 되는가? + +현재 collaborator 구조에서는 이 PR 자체를 merge할 수 없다. 변경 PR을 merge한 뒤 exact +checks가 존재할 때 protection을 적용한다. + +### Q8. tag가 우발적으로 생기지 않았는지 어떻게 확인하는가? + +작업 전후 `git tag --points-at HEAD`, `git ls-remote --tags origin/upstream`을 비교하고 +handoff에 no-new-tag를 기록한다. + +## Grill 결과 + +- 열린 질문: 없음 +- blocker: branch protection 적용은 변경 PR merge 후 수행 +- 고정 결정: audit moderate 0, 독립 jobs, admin-enforced 1 approval, upstream-derived version, + no tag + diff --git a/docs/superpowers/grill-me/2026-08-13-vm-deletion-failure-cleanup.md b/docs/superpowers/grill-me/2026-08-13-vm-deletion-failure-cleanup.md new file mode 100644 index 0000000..12a9fee --- /dev/null +++ b/docs/superpowers/grill-me/2026-08-13-vm-deletion-failure-cleanup.md @@ -0,0 +1,60 @@ +# VM 삭제 실패 cleanup grill-me 기록 + +**날짜:** 2026-08-13 + +**topic:** `vm-deletion-failure-cleanup` + +**설계:** +[`2026-08-13-vm-deletion-failure-cleanup-design.md`](../specs/2026-08-13-vm-deletion-failure-cleanup-design.md) + +## 압박 질문과 결정 + +### Q1. dm device가 남았는데 loop detach를 시도하는 것이 의미가 있는가? + +성공 가능성은 낮지만 생략하면 cleanup 시도 자체가 0이다. dependency가 예상과 다르게 +풀린 경우 성공할 수 있고, 실패해도 error aggregation이 정확한 잔여를 보여 준다. + +### Q2. store를 unlink하면 orphan recovery가 더 어려워지는가? + +full delete는 store 보존 의도가 없다. unlink는 path leak을 없애고 열린 inode는 kernel +reference 종료 후 회수된다. keep-store shutdown 경로는 unlink하지 않는다. + +### Q3. 무한 retry가 더 안전하지 않은가? + +HTTP delete와 daemon shutdown을 무한정 멈출 수 없다. 기존 10초 bounded retry를 유지하고 +후속 cleanup·error reporting으로 진행한다. + +### Q4. 실패를 무시하고 성공 metric을 올리는 문제는 없는가? + +기존 `anvil_cleanup_failure_total`은 teardown error에서 증가한다. VM identity 삭제와 +network release 완료 의미의 delete metric은 유지하되 handoff/release gate는 resource +inventory로 별도 판정한다. + +### Q5. fake command test가 실제 kernel behavior를 증명하는가? + +실제 busy semantics 자체가 아니라 control flow 불변식(실패 뒤 다음 단계 호출)을 +결정적으로 증명한다. 정상 real KVM E2E와 host inventory를 두 번째 modality로 사용한다. + +### Q6. bind-mount restore도 같은 결함이 있는가? + +`TeardownBindMount`는 unmount 실패와 무관하게 disk remove를 계속한다. 이번 regression은 +dm branch에 한정하되 KVM inventory에서 bind mount도 확인한다. + +### Q7. TAP/IP release는 현재도 storage error 뒤 호출되지 않는가? + +코드상 그렇지만 회귀 test가 없다. cleanup helper를 분리해 storage failure를 강제하고 +network release 호출을 실행 증거로 고정한다. + +### Q8. 실제 dm failure fault injection을 production host에서 해야 하는가? + +의도적으로 kernel resource를 남기는 fault는 운영 host를 오염시킬 수 있다. deterministic +unit fault injection으로 failure branch를 검증하고, real KVM은 정상 경로 잔여 inventory를 +검증한다. 실제 kernel refusal에서 “모든 제거 성공”은 보장하지 않고 error/blocker로 남긴다. + +## Grill 결과 + +- 열린 질문: 없음 +- blocker: 없음 +- 고정 결정: bounded retry 유지, early return 제거, keep-store 보존, aggregate error, + forced unit failure + real inventory + diff --git a/docs/superpowers/plans/2026-08-13-allow-hosts-removal.md b/docs/superpowers/plans/2026-08-13-allow-hosts-removal.md new file mode 100644 index 0000000..573519e --- /dev/null +++ b/docs/superpowers/plans/2026-08-13-allow-hosts-removal.md @@ -0,0 +1,145 @@ +# `allow_hosts` 제거 구현 계획 + +**날짜:** 2026-08-13 + +**상태:** 승인된 spec 기반 실행 계획 + +**Spec:** +[`2026-08-13-allow-hosts-removal-design.md`](../specs/2026-08-13-allow-hosts-removal-design.md) + +**Grill:** +[`2026-08-13-allow-hosts-removal.md`](../grill-me/2026-08-13-allow-hosts-removal.md) + +## Goal + +legacy `allow_hosts`를 runtime과 command planner에서 제거하고, 오래된 profile이 조용히 +무시되지 않도록 decode boundary에서 migration error로 거부한다. + +## Engineering Review + +### Architecture와 data flow + +`egress.json` bytes → raw top-level key probe → typed `egressProfile` unmarshal → validation → +iptables command plan 순서를 사용한다. legacy key는 typed decode 전 종료되므로 apply에 +도달하지 않는다. HTTP/MCP/guest boundary에는 변화가 없다. + +### Security review + +- substring matcher source와 emitted command를 모두 제거한다. +- error는 field/migration만 기록하고 host value는 기록하지 않는다. +- key 값이 empty/null이어도 거부한다. +- 현행 allowlist fields의 fail-closed default REJECT는 유지한다. + +### Test validity + +co-generated test만으로 끝내지 않는다. prove-broken-first test, raw source search, +command plan negative assertion, compiler/전체 unit test를 독립 채널로 사용한다. + +### Rollback + +git diff를 수동 반전할 수 있으나 legacy matcher 복원은 보안 열화다. operator 호환 문제는 +profile migration으로 해결하고, release 후 rollback은 별도 승인 대상으로 둔다. + +### Execution Environment Constraints + +- Go unit/race/build/vet는 Linux에서 KVM 없이 수행 가능 +- full E2E는 `/dev/kvm`, root, Firecracker와 local secrets가 필요하며 최종 release gate에서 + 별도 수행 +- profile path와 env 이름의 기존 `EPHEMERA_*`/`ANVIL_*` contract는 유지 +- tag/publish 금지 + +### Engineering gate + +architecture, error ordering, security, test falsifiability, rollback과 환경 제약을 검토했다. +구현을 막는 issue는 없다. + +## Task 1. 제거 계약을 테스트로 먼저 고정 + +**Consumes:** + +- `cmd/goose-daemon/egress_policy.go:48` `loadEgressProfile` +- `cmd/goose-daemon/egress_policy_test.go:11` profile load/plan test +- `cmd/goose-daemon/egress_policy_test.go:212` deprecation warning test + +**Files:** + +- Modify: `cmd/goose-daemon/egress_policy_test.go` + +1. `allow_hosts` non-empty/empty/null을 table test로 load한다. +2. 모두 `ok=false`와 migration error를 기대한다. +3. error가 제거 field와 두 대체 field를 포함하고 host value를 포함하지 않는지 확인한다. +4. current fields profile이 계속 load/plan되는 test로 양성대조한다. +5. 다른 unknown field가 기존대로 무시되는 음성대조를 추가한다. +6. 구현 전 targeted test가 실패하는 prove-broken 결과를 기록한다. + +**적대적 수용 기준:** + +- 선행조건: 기존 runtime이 non-empty/empty/null legacy key를 허용함 +- probe: 값 형태 3종과 secret-like hostname 비노출 assertion +- 독립 채널: loader error + command source/plan negative search +- 음성대조: `allow_sni`/`allow_cidrs`와 unrelated unknown key 정상 + +## Task 2. 최소 removal 구현 + +**Consumes:** + +- `cmd/goose-daemon/egress_policy.go:38-43` `egressProfile` +- `cmd/goose-daemon/egress_policy.go:48-87` loader/deprecation warning +- `cmd/goose-daemon/egress_policy.go:90-119` validation +- `cmd/goose-daemon/egress_policy.go:208-213` string matcher plan + +**Files:** + +- Modify: `cmd/goose-daemon/egress_policy.go` + +1. `AllowHosts`, warning, validator와 apply loop를 제거한다. +2. raw top-level object에서 `allow_hosts` key를 찾는 decode helper를 추가한다. +3. field와 migration target만 포함한 error를 반환한다. +4. typed unmarshal과 기존 validation error wrapping을 유지한다. +5. `validateDomainCharset` 설명을 `allow_sni` 현재 계약에 맞춘다. + +## Task 3. canonical/운영 문서 정합화 + +**Consumes:** + +- `CONTEXT.md`의 egress 상태와 backlog +- `README.md` egress/operator surface +- `RELEASE_NOTES.md` unreleased/current change 기록 +- `docs/architecture/service-logic.md` egress policy flow +- `docs/adr/0002-egress-sni-transparent-filter.md` OQ8 +- `docs/PUBLIC_RELEASE_BOUNDARY.md`, `docs/operations/runbook.md`, + `docs/operations/security-policy.md`, `docs/guides/mcp-adapter.md` + +1. canonical 문서에서 deprecated/예정 표현을 removed/loud rejection으로 갱신한다. +2. migration mapping과 key-existence rejection을 operator 문서에 쓴다. +3. historical spec/plan은 수정하지 않는다. +4. `README.md`, architecture, release notes를 반드시 포함한다. + +## Task 4. 검증과 code review + +1. targeted removal tests +2. `go test ./cmd/goose-daemon -count=1` +3. `go test ./... -count=1` +4. `go test -race ./... -count=1` +5. `go build ./...`, `go vet ./...`, `gofmt -l .` +6. `bash scripts/secret-scan.sh` +7. `rg`로 production `allow_hosts`/iptables string matcher 부재 확인 +8. Markdown relative-link scan과 `git diff --check` +9. actual diff를 requirement/security/regression/test adequacy 관점에서 review한다. + +## Task 5. release-to-operate handoff + +`docs/operations/2026-08-13-allow-hosts-removal-handoff.md`에 Release Scope, +Verification, Audit, Blockers, Warnings, Residual Risk, Current Lifecycle Stage, +Next Action, Follow-Up Tasks를 기록한다. 이 변경만으로 tag 또는 operate 진입을 선언하지 +않는다. + +## 완료 조건 + +- spec 수용 기준이 실행 증거와 함께 handoff에 기록됨 +- legacy key가 값 형태와 무관하게 loud failure +- matcher source/apply path 제거 +- canonical/current 문서 정합화 +- blocking code review finding 0 +- tag/release 생성 없음 + diff --git a/docs/superpowers/plans/2026-08-13-release-gate-closure.md b/docs/superpowers/plans/2026-08-13-release-gate-closure.md index 05c096d..4cd7e5b 100644 --- a/docs/superpowers/plans/2026-08-13-release-gate-closure.md +++ b/docs/superpowers/plans/2026-08-13-release-gate-closure.md @@ -129,9 +129,13 @@ pre-release handoff를 만든다. 1. targeted daemon test 2. `bash scripts/secret-scan.sh` 3. Go test/race/build/vet/module/gofmt/govulncheck -4. Web check/build/audit -5. bash syntax와 Markdown relative-link scan -6. `git diff --check` +4. aggregate build와 별개로 release entrypoint 세 개를 named command로 검증 + - `go build -o anvil-daemon ./cmd/goose-daemon/` + - `go build ./cmd/anvil-mcp` + - `go build ./cmd/anvil-scheduler` +5. Web check/build/audit +6. bash syntax와 Markdown relative-link scan +7. `git diff --check` 실패는 코드, dependency, local environment, known warning으로 분류한다. @@ -159,7 +163,11 @@ scripts/anvil-mcp-e2e.sh flock 각 smoke가 독립 daemon lifecycle을 요구하는지 script 계약을 먼저 읽고 안전한 순서로 실행한다. 실패 시 device/network/provider/runtime 원인을 구분하고 생성 resource cleanup을 -확인한다. +확인한다. `sudo bash e2e_test.sh`의 삭제 경로와 별도로 forced dm-snapshot 삭제 실패 +unit probe를 실행해 `dmsetup remove --retry` 실패 뒤에도 COW/base loop detach, sparse +`.cow` store removal, TAP/IP release가 계속 시도되는지 검증한다. E2E 종료 뒤 +TAP/IP resource, bind mount, loop device, dm-snapshot, sparse `.cow` exception-store를 +각각 독립 조회하고, 하나라도 남으면 release gate를 열린 상태로 유지한다. ## Task 6. Code review와 pre-release handoff @@ -180,7 +188,7 @@ scripts/anvil-mcp-e2e.sh flock ## 완료 조건 -- spec 수용 기준 1-10의 상태가 handoff에 실제 evidence와 함께 기록됨 +- spec 수용 기준 1-11의 상태가 handoff에 실제 evidence와 함께 기록됨 - report/index/fixture diff가 review됨 - exact SHA CI와 KVM 결과가 성공 또는 명확한 blocker로 분류됨 - tag/release가 생성되지 않음 diff --git a/docs/superpowers/plans/2026-08-13-release-governance-hardening.md b/docs/superpowers/plans/2026-08-13-release-governance-hardening.md new file mode 100644 index 0000000..6be467f --- /dev/null +++ b/docs/superpowers/plans/2026-08-13-release-governance-hardening.md @@ -0,0 +1,95 @@ +# Release governance hardening 구현 계획 + +**날짜:** 2026-08-13 + +**상태:** 승인된 spec 기반 실행 계획 + +**Spec:** +[`2026-08-13-release-governance-hardening-design.md`](../specs/2026-08-13-release-governance-hardening-design.md) + +**Grill:** +[`2026-08-13-release-governance-hardening.md`](../grill-me/2026-08-13-release-governance-hardening.md) + +## Engineering Review + +### Dependency와 workflow + +package override는 lockfile에 resolved graph를 고정하고 `npm ci`로 재현한다. GitHub Actions +jobs는 contents read만 필요하며 secret write/PR write 권한이 없다. + +### Governance ordering + +workflow commit → PR exact checks → review/merge → main checks 확인 → branch protection 적용 +순서다. protection을 먼저 적용해 sole-owner deadlock을 만들지 않는다. + +### Rollback + +dependency override/workflow는 git revert 가능하다. protection은 GitHub API로 이전 설정을 +복원할 수 있다. tag는 만들지 않아 immutable rollback 문제가 없다. + +### Execution Environment Constraints + +- local Node 22/npm 9와 CI Node 22 +- npm registry/advisory DB network 필요 +- GitHub admin token과 branch protection API 필요 +- upstream tag 조회는 `git ls-remote --tags upstream`, force fetch 금지 + +### Engineering gate + +dependency compatibility, job least privilege, required context ordering, sole-owner deadlock, +version alignment와 rollback을 검토했다. 구현 blocker 없음. + +## Task 1. npm advisory를 prove-broken 후 제거 + +1. 기존 `npm audit --json`에서 High 2/Moderate 2를 기록한다. +2. `package.json`에 최소 transitive overrides를 추가한다. +3. `npm install`로 lockfile을 갱신한다. +4. `npm ci`, check, build, audit moderate를 실행한다. +5. `npm ls`로 patched resolved versions를 확인한다. + +## Task 2. Web/secret CI + +1. `.github/workflows/ci.yml`에 Node 22 `web-and-security` job을 추가한다. +2. npm cache dependency path를 `web/package-lock.json`으로 고정한다. +3. `web` working-directory에서 ci/check/build/audit를 실행한다. +4. 별도 `secret-scan` job에서 strict tracked-tree scanner를 실행한다. +5. permissions `contents: read` 외 권한을 추가하지 않는다. + +## Task 3. version/canonical docs + +1. `CONTEXT.md`, `README.md`, `RELEASE_NOTES.md`에 next-upstream exact transform을 기록한다. +2. current next number가 unassigned blocker임을 쓴다. +3. downstream-only patch/minor 추측을 금지한다. + +## Task 4. local/remote 검증과 PR + +1. Go 전체와 named builds, race/vet/gofmt/secret scan +2. Web clean install/check/build/audit +3. workflow syntax와 `git diff --check` +4. intentional commit/push, PR 생성 +5. exact SHA의 세 Actions job + CodeRabbit 결과 확인 +6. actionable review 처리 후 merge + +## Task 5. branch protection + +1. collaborator inventory를 재확인한다. +2. `main`에 strict required checks 4개, admin enforce, approval 1, stale review dismiss, + last-push approval, conversation resolution, force-push/delete 금지를 적용한다. +3. GET API로 실제 설정을 read-back한다. +4. reviewer 부재를 handoff follow-up으로 기록한다. + +## Task 6. no-tag audit와 handoff + +1. origin/upstream tag inventory를 재확인한다. +2. head points-at tag가 없음을 확인한다. +3. `docs/operations/2026-08-13-release-governance-hardening-handoff.md`에 필수 fields와 + remaining host/version blockers를 기록한다. + +## 완료 조건 + +- spec 수용 기준 1–8 evidence 기록 +- npm moderate+ advisory 0 +- exact SHA jobs/CodeRabbit green +- PR merged 뒤 protection read-back 일치 +- no tag + diff --git a/docs/superpowers/plans/2026-08-13-vm-deletion-failure-cleanup.md b/docs/superpowers/plans/2026-08-13-vm-deletion-failure-cleanup.md new file mode 100644 index 0000000..7a1e10b --- /dev/null +++ b/docs/superpowers/plans/2026-08-13-vm-deletion-failure-cleanup.md @@ -0,0 +1,105 @@ +# VM 삭제 실패 cleanup 구현 계획 + +**날짜:** 2026-08-13 + +**상태:** 승인된 security invariant 기반 실행 계획 + +**Spec:** +[`2026-08-13-vm-deletion-failure-cleanup-design.md`](../specs/2026-08-13-vm-deletion-failure-cleanup-design.md) + +**Grill:** +[`2026-08-13-vm-deletion-failure-cleanup.md`](../grill-me/2026-08-13-vm-deletion-failure-cleanup.md) + +## Engineering Review + +### Data flow와 ordering + +VMM stop → socket/vsock/egress cleanup → storage teardown(all attempts) → TAP/IP release 순서를 +유지한다. storage 내부는 unmount → bounded dm remove → loop detach 2개 → optional store +remove이며, 실패는 진행 제어가 아니라 반환 evidence다. + +### Security와 rollback + +resource leak을 줄이는 fail-cleanup change다. rollback은 early return을 되살려 보안 열화가 +되므로 compatibility rollback 사유가 없다. HTTP/schema/data format은 불변이다. + +### Execution Environment Constraints + +- deterministic unit fault는 KVM/root 불필요 +- real inventory는 Linux x86_64, `/dev/kvm`, root, dm/loop/TAP tools 필요 +- host-wide resource probe는 anvil prefix/workspace로 scope를 제한 +- tag/publish 금지 + +### Engineering gate + +dependency ordering, bounded wait, keep-store, metrics, test seam과 host 안전을 검토했다. +구현 blocker 없음. + +## Task 1. storage early-return regression을 test-first로 고정 + +**Consumes:** + +- `internal/storage/snapshot.go:354` `teardownDMSnapshot` +- `internal/storage/snapshot.go:339` `TeardownDMSnapshot` +- `internal/storage/snapshot.go:347` `TeardownDMSnapshotKeepStore` + +**Files:** + +- Modify: `internal/storage/snapshot_test.go` +- Modify: `internal/storage/snapshot.go` + +1. command/remove/clock를 기록하는 fake operations를 만든다. +2. dm remove를 강제로 timeout 실패시킨다. +3. 구현 전 loop/store 호출 누락으로 test가 실패하는 prove-broken evidence를 기록한다. +4. early return을 제거하고 후속 단계 error를 누적한다. +5. full/keep-store 양쪽을 검증한다. + +**적대적 수용 기준:** forced dm failure, 두 loop detach 호출, full store remove 호출, +keep-store 미호출, 모든 error join. + +## Task 2. ControlPlane TAP/IP continuation 회귀 + +**Consumes:** + +- `cmd/goose-daemon/api.go:1806` `destroyVMUnderSnapshotLock` +- `cmd/goose-daemon/api.go:1840` COW teardown branch +- `cmd/goose-daemon/api.go:1854` network release + +1. VMM stop 이후 host-resource cleanup을 helper로 추출한다. +2. delete-only dm teardown test seam을 둔다. +3. storage error를 강제하고 cleanup failure metric과 network release 호출을 검증한다. +4. bind/plain disk branches와 ordering은 보존한다. + +## Task 3. PR #110 review 문서 보정 + +1. plan 완료 기준을 spec 1–11로 고친다. +2. 세 named build와 `sudo bash e2e_test.sh`를 plan/handoff에 명시한다. +3. resource type별 inventory와 failure gate를 handoff에 기록한다. +4. zone-relative governance 링크 지적은 실제 path probe를 근거로 오탐 처리한다. + +## Task 4. 검증 + +1. targeted forced-failure tests +2. `go test ./... -count=1`, `go test -race ./... -count=1` +3. `go build -o anvil-daemon ./cmd/goose-daemon/` +4. `go build ./cmd/anvil-mcp` +5. `go build ./cmd/anvil-scheduler` +6. `go vet ./...`, `gofmt -l .`, secret scan +7. `sudo bash e2e_test.sh` +8. scoped TAP/IP, bind mount, dm, loop, `.cow` post-inventory +9. `git diff --check`와 code review + +## Task 5. handoff + +`docs/operations/2026-08-13-vm-deletion-failure-cleanup-handoff.md`에 필수 operate fields와 +잔여 kernel refusal risk를 기록한다. cleanup 잔여 또는 test 실패가 있으면 release/operate +진입을 선언하지 않는다. + +## 완료 조건 + +- spec 수용 기준 1–9 evidence 기록 +- forced failure 뒤 모든 후속 cleanup 시도 +- CP network continuation 실행 검증 +- real KVM inventory clean +- review blocker 0, tag 없음 + diff --git a/docs/superpowers/specs/2026-08-13-allow-hosts-removal-design.md b/docs/superpowers/specs/2026-08-13-allow-hosts-removal-design.md new file mode 100644 index 0000000..bc0cb56 --- /dev/null +++ b/docs/superpowers/specs/2026-08-13-allow-hosts-removal-design.md @@ -0,0 +1,131 @@ +# `allow_hosts` 제거 설계 + +**날짜:** 2026-08-13 + +**상태:** 승인됨 — 2026-07-18 deprecation 설계와 사용자의 후속 작업 전체 실행 승인에 근거 + +**topic:** `allow-hosts-removal` + +**선행 설계:** +[`2026-07-18-allow-hosts-deprecation-design.md`](./2026-07-18-allow-hosts-deprecation-design.md) + +## 목표 + +fragmentation으로 우회 가능한 iptables substring matcher인 `allow_hosts`를 runtime +contract에서 제거한다. 기존 profile이 이 필드를 계속 포함하면 조용히 규칙을 잃지 +않도록 profile load를 명시적으로 실패시키고, operator에게 `allow_sni`와 +`allow_cidrs` 마이그레이션 경로를 알려 준다. + +## 포함 범위 + +1. `egressProfile.AllowHosts`와 관련 validation/apply rule 제거 +2. JSON 최상위 `allow_hosts` key의 값이 빈 배열 또는 `null`이어도 loud failure +3. domain은 `allow_sni`, IP/CIDR은 `allow_cidrs`로 이동하는 운영 문서 갱신 +4. `CONTEXT.md`, `README.md`, `RELEASE_NOTES.md`, service architecture와 ADR 정합화 +5. 제거 전 실패·제거 후 거부·현행 profile 정상 동작을 구분하는 TDD 회귀 검증 + +## 비목표 + +- 다른 미지 JSON 필드를 일괄 거부하는 schema strictness 확대 +- `allow_sni`/`allow_cidrs` semantics 변경 +- profile 자동 변환 또는 운영 환경 파일의 자동 수정 +- 다음 tag 생성이나 version 번호 발행 +- historical lifecycle 문서의 과거 사실 재작성 + +## Domain Architecture + +### 계약 경계 + +- `egressProfile`은 daemon 내부의 현재 egress 계약만 표현한다. +- `loadEgressProfile`은 disk JSON과 현재 계약 사이의 anti-corruption boundary다. +- 제거된 `allow_hosts` 감지는 apply/validation보다 앞선 decode 단계에서 수행한다. +- `planProfileEgressCommands`는 `allow_cidrs`, `allow_sni`, `dns_servers`만 소비한다. + +공개 HTTP/MCP API, guest protocol, storage format은 바뀌지 않는다. operator가 작성하는 +`configs/profiles//egress.json` contract만 바뀌므로 +`docs/architecture/service-logic.md`를 갱신한다. + +### 용어 + +- **removed legacy key:** 현재 schema에는 없지만 과거에 안전하지 않은 의미를 가졌던 + `allow_hosts` +- **loud fail-closed:** key를 발견하면 profile load를 error로 끝내 VM 생성/egress 적용을 + 진행하지 않는 동작 +- **explicit legacy rejection:** 모든 unknown field가 아니라 `allow_hosts`만 raw JSON key로 + 탐지해 거부하는 decode 방식 + +## 설계 결정 + +### D1. 모든 `allow_hosts` key를 명시적으로 거부한다 + +`json.Unmarshal` 전에 최상위 object를 `map[string]json.RawMessage`로 읽어 +`allow_hosts` 존재 여부를 확인한다. 값의 길이나 타입을 보지 않으므로 다음 모두 실패한다. + +```json +{"allow_hosts": ["api.example.com"]} +{"allow_hosts": []} +{"allow_hosts": null} +``` + +오류는 제거된 field 이름과 두 대체 field를 포함한다. host value는 log/error에 +반복하지 않는다. + +### D2. 모든 unknown field를 거부하지 않는다 + +`json.Decoder.DisallowUnknownFields`는 제거 목표보다 넓은 behavior change다. 이미 존재하는 +다른 unknown metadata를 갑자기 거부할 수 있으므로 이번에는 explicit legacy rejection을 +사용한다. 일반 schema strictness는 별도 lifecycle 대상이다. + +### D3. substring iptables rule을 완전히 제거한다 + +`-m string --string ... --algo bm` command 생성 경로와 `AllowHosts` validation을 제거한다. +현재 profile 예시는 `allow_hosts`를 사용하지 않으므로 repository-owned config migration은 +필요 없다. + +### D4. historical 기록은 보존하고 canonical 문서를 현재화한다 + +과거 deprecation spec/plan은 당시 사실이므로 바꾸지 않는다. 현재 진실 기준인 +`CONTEXT.md`, `README.md`, `RELEASE_NOTES.md`, architecture/ADR와 운영 guide에는 제거 완료와 +마이그레이션 계약을 기록한다. + +### D5. version은 이 변경에서 발행하지 않는다 + +제거는 다음 tagged release에 포함될 breaking candidate지만, tag/version은 upstream +alignment gate와 다른 blockers가 닫힌 뒤 결정한다. 이번 diff에 tag를 만들지 않는다. + +## Plan Design Review + +- **발견성:** README의 egress 설명과 runbook에서 제거 사실·대체 필드를 직접 찾을 수 있다. +- **오류 예방:** 빈 `allow_hosts`도 받아들이지 않아 오래된 template이 잠복하지 않는다. +- **오류 메시지:** 제거 field와 `allow_sni`/`allow_cidrs`를 함께 명시한다. +- **상태 명확성:** “deprecated”와 “removed”를 canonical 문서에서 혼용하지 않는다. +- **visual/UI:** UI 변경이 없어 visual review는 해당 없다. operator IA와 오류 발견성 + review로 대체했고 blocking issue가 없다. + +## 수용 기준 + +1. `egressProfile`에 `AllowHosts`가 없다. +2. `allow_hosts` key는 non-empty, empty, `null` 모두 profile load error가 된다. +3. 오류는 `allow_hosts`, `allow_sni`, `allow_cidrs`를 명시하고 값은 노출하지 않는다. +4. 제거 후 command plan에 iptables string matcher가 생성되는 코드가 없다. +5. `allow_sni`, `allow_cidrs`, `dns_servers` profile은 기존대로 load/validate/plan된다. +6. 다른 unknown field의 기존 ignore behavior는 이번 변경으로 바뀌지 않는다. +7. 변경 전 테스트가 legacy profile을 허용해 실패하고, 변경 후 통과하는 prove-broken-first + evidence가 있다. +8. Go 전체 test/build/vet/race/gofmt와 secret scan이 통과한다. +9. 진실 기준 문서와 operator 문서가 제거 계약으로 정합화된다. +10. operation handoff가 verification, blocker, residual risk를 기록한다. + +## Spec Freeze Snapshot + +- **Approved objective:** deprecated `allow_hosts` 실제 제거와 loud fail-closed migration +- **Domain boundary:** disk JSON decode boundary와 host egress command planner +- **Decode decision:** explicit top-level `allow_hosts` rejection; global unknown rejection 아님 +- **Migration:** domain → `allow_sni`, IP/CIDR → `allow_cidrs` +- **Security invariant:** packet substring matcher 완전 제거, legacy key 조용한 무시 금지 +- **Non-goals:** 자동 profile 수정, 다른 schema strictness, tag/version 발행 +- **Acceptance:** key 형태 3종 거부, 현행 fields 정상, string matcher source/plan 부재, + 전체 검증 green +- **Environment:** Linux/Go unit test는 KVM 불필요; full KVM은 최종 release gate에서 별도 +- **Open design questions:** 없음 + diff --git a/docs/superpowers/specs/2026-08-13-release-governance-hardening-design.md b/docs/superpowers/specs/2026-08-13-release-governance-hardening-design.md new file mode 100644 index 0000000..3c1e3b5 --- /dev/null +++ b/docs/superpowers/specs/2026-08-13-release-governance-hardening-design.md @@ -0,0 +1,118 @@ +# Release governance hardening 설계 + +**날짜:** 2026-08-13 + +**상태:** 승인됨 — 사용자의 후속 작업 3·5·6·7 전체 실행 승인 + +**topic:** `release-governance-hardening` + +## 목표 + +Web dependency advisory, 누락된 Web/secret CI, 무보호 `main`, 다음 version 결정 규칙을 +하나의 release governance contract로 닫는다. 모든 gate가 닫히기 전에는 tag를 만들지 +않는다. + +## 포함 범위 + +1. `web/package-lock.json`의 npm High/Moderate advisory 0건 처리 +2. CI에 Web check/build/audit와 tracked-tree secret scan 추가 +3. merge 후 `main` branch protection에 required status/review/conversation resolution 적용 +4. next anvil version selection rule과 현재 번호 미할당 blocker 명문화 +5. exact SHA CI와 operation handoff + +## 비목표 + +- Svelte UI/runes migration 또는 warning 10건 전면 수정 +- upstream tag 생성 요청이나 upstream release 일정 추측 +- current release tag/GitHub Release 생성 +- 실제 secret 또는 credential 출력 + +## Domain Architecture + +- **Dependency boundary:** `web/package.json` direct dependencies와 npm overrides, + `package-lock.json` resolved graph +- **Verification boundary:** GitHub Actions의 `build-and-test`, `web-and-security`, + `secret-scan` 독립 job +- **Merge governance boundary:** GitHub `main` branch protection +- **Version boundary:** upstream ephemera version → downstream `anvil-` tag transform + +runtime process, HTTP/MCP schema, UI behavior는 바뀌지 않는다. workflow/release contract +변경이므로 full lifecycle을 사용한다. + +## 설계 결정 + +### D1. 취약 transitive를 override하고 audit를 moderate부터 차단한다 + +- `svelte-i18n@4.0.1`의 `esbuild@0.19.12`는 Vite graph와 공유 가능한 patched + `0.28.2`로 override한다. +- Vite graph의 `postcss`와 `nanoid`는 현재 compatible patched release로 override한다. +- `npm audit --audit-level=moderate`가 0건이어야 CI green이다. +- `npm ci`, `npm run check`, `npm run build`로 override compatibility를 검증한다. + +direct `svelte-i18n`을 npm fixer 제안대로 3.x로 downgrade하지 않는다. 현재 Svelte 5 app의 +direct major를 되돌리는 것보다 build-tested transitive patch가 작다. + +### D2. Web과 secret을 독립 CI job으로 둔다 + +- `web-and-security`: Node 22, npm cache, `npm ci`, check, build, moderate audit +- `secret-scan`: least-privilege checkout 뒤 `bash scripts/secret-scan.sh` + +두 job을 분리해 dependency failure와 source-secret failure를 구분한다. + +### D3. branch protection은 strict required checks + 1 approval이다 + +`main`에 세 CI context와 CodeRabbit review status, 1 approving review, stale review dismiss, +last-push approval, conversation resolution, force-push/deletion 금지를 적용하고 admin에도 +enforce한다. + +현재 collaborator는 repository owner 한 명뿐이라 작성자가 self-approve할 수 없다. 이 +설정 이후 merge하려면 두 번째 eligible reviewer를 collaborator로 추가해야 한다. 이는 +의도된 강한 gate이며 handoff의 immediate follow-up이다. + +### D4. 다음 version은 규칙을 확정하되 번호를 발명하지 않는다 + +결정 규칙: + +```text +next_upstream_tag = upstream의 첫 post-v0.7.0 정식 tag +next_anvil_tag = "anvil-" + next_upstream_tag +``` + +downstream-only `anvil-v0.7.1`이나 추정 `anvil-v0.8.0`은 만들지 않는다. 조사 시점 upstream +latest/main은 `v0.7.0`이므로 next version number는 **미할당(blocked)**이다. 이는 모호한 +TBD가 아니라 upstream event를 입력으로 하는 결정적 policy와 명시적 release gate다. + +### D5. tag는 금지한다 + +이번 workflow, dependency, protection 작업은 tag 생성 권한으로 확대되지 않는다. +host security operation과 version input이 닫힐 때까지 current head에는 tag가 없어야 한다. + +## Plan Design Review + +- CI job 이름은 branch protection context와 정확히 일치한다. +- Web audit와 source-secret 실패가 별도 job으로 보여 triage가 명확하다. +- version 문서는 “예상 번호” 대신 입력/변환/gate를 보여 operator 오판을 막는다. +- strict review가 sole-owner repository를 잠그는 결과를 사전에 명시한다. +- UI 변경 없음. workflow IA와 gate discoverability review로 대체했고 blocker 없음. + +## 수용 기준 + +1. `npm audit --audit-level=moderate`가 vulnerability 0, exit 0이다. +2. `npm ci`, `npm run check`, `npm run build`가 통과한다. +3. CI에 `web-and-security`, `secret-scan` job이 있고 least-privilege다. +4. branch exact SHA에서 Go/Web/secret jobs가 green이다. +5. `main` protection이 strict checks, CodeRabbit, 1 approval, stale/last-push, + conversation resolution, admin enforce, force-push/delete 금지를 반영한다. +6. next version transform rule과 current unassigned blocker가 canonical 문서에 있다. +7. current/upstream remote tag inventory에 새 tag가 없다. +8. handoff가 sole-owner merge consequence와 reviewer 추가 필요를 기록한다. + +## Spec Freeze Snapshot + +- **Objective:** npm advisory 0, Web/secret required CI, strict main governance, version rule +- **Dependency decision:** patched transitive overrides + compatibility build +- **CI contexts:** `build-and-test`, `web-and-security`, `secret-scan`, `CodeRabbit` +- **Review decision:** admin-enforced 1 approval; second collaborator required +- **Version decision:** next upstream tag를 exact transform, current number unassigned +- **Publish decision:** no tag/release +- **Open questions:** 없음 diff --git a/docs/superpowers/specs/2026-08-13-vm-deletion-failure-cleanup-design.md b/docs/superpowers/specs/2026-08-13-vm-deletion-failure-cleanup-design.md new file mode 100644 index 0000000..88e4001 --- /dev/null +++ b/docs/superpowers/specs/2026-08-13-vm-deletion-failure-cleanup-design.md @@ -0,0 +1,109 @@ +# VM 삭제 실패 cleanup 설계 + +**날짜:** 2026-08-13 + +**상태:** 승인됨 — PR #110 사후 CodeRabbit review와 프로젝트 불변 조건에 따른 보안 수정 + +**topic:** `vm-deletion-failure-cleanup` + +## 목표 + +명시적 VM 삭제 중 dm-snapshot device 제거가 실패해도 loop device detach, sparse COW +exception-store 제거, TAP/IP release가 중단되지 않게 한다. 모든 cleanup 단계의 오류를 +모아 반환·계측하되 한 단계 실패가 뒤 단계를 건너뛰는 control flow를 금지한다. + +## 문제 증거 + +현재 `internal/storage.teardownDMSnapshot`은 `dmsetup remove --retry`가 retry window 안에 +성공하지 못하면 즉시 return한다. 따라서 다음 `losetup -d` 2회와 +`os.Remove(ExceptionStore)`가 시도조차 되지 않는다. `destroyVMUnderSnapshotLock`은 storage +호출 뒤 TAP/IP를 release하지만, 내부 loop/store leak을 식별 가능한 실행 test가 없다. + +## 포함 범위 + +1. dm removal failure 뒤에도 COW/base loop detach와 store removal 시도 +2. 모든 단계 error aggregation 유지 +3. `KeepStore` 경로는 store를 보존하면서 kernel object cleanup을 계속 시도 +4. forced storage failure 뒤 ControlPlane의 TAP/IP release 지속을 test seam으로 검증 +5. 정상 KVM E2E 종료 뒤 TAP/IP, bind mount, dm device, loop, `.cow` 잔여 inventory 확인 +6. PR #110 release-gate plan/handoff에 실제 named build와 cleanup evidence 보강 + +## 비목표 + +- kernel이 busy resource 제거를 거부할 때 성공을 가장하는 것 +- unrelated orphan cleanup 구조 재작성 +- VM delete HTTP response contract 변경 +- tag/release 생성 + +## Domain Architecture + +- `ControlPlane.destroyVMUnderSnapshotLock`: VM identity 제거, VMM stop, host resource + cleanup을 orchestration한다. +- `storage.TeardownDMSnapshot`: mount → dm device → COW/base loop → optional store 순서의 + best-effort cleanup owner다. +- `network.Manager.Release`: TAP/IP lease cleanup owner다. + +storage failure가 network boundary로 전파되어 cleanup을 중단하면 안 된다. 반대로 +ControlPlane이 storage 내부 단계 성공을 추측하지 않고 반환 error를 계측한다. +HTTP/MCP surface와 storage format은 바뀌지 않아 architecture pack 추가 수정은 필요 없다. + +## 설계 결정 + +### D1. early return을 제거하고 오류를 누적한다 + +dm removal retry가 소진되면 error를 `errs`에 추가한 뒤 loop detach와 store removal을 +계속 시도한다. 각 단계 error도 `errors.Join`으로 반환한다. + +### D2. dependency 실패 뒤 시도는 의도된 best-effort다 + +dm device가 loop를 계속 참조하면 `losetup -d`가 실패할 수 있다. 그래도 명령을 생략하는 +것보다 시도하고 각 실패를 독립 증거로 남기는 편이 cleanup invariant에 맞다. +store unlink는 path 잔여를 없애며 열린 inode의 실제 공간은 kernel reference가 사라질 때 +회수된다. 반환 error가 있으므로 성공으로 과장하지 않는다. + +### D3. deterministic fake operations로 forced failure를 실증한다 + +private helper에 command runner, file remover, clock/sleep을 주입한다. production wrapper는 +`exec.Command`, `os.Remove`, real clock을 사용한다. test는 dm removal을 강제로 계속 +실패시키고 이후 두 loop detach와 store remove가 호출됐는지 순서와 error로 관측한다. + +### D4. ControlPlane의 독립 TAP/IP continuation을 별도 test한다 + +post-VMM host cleanup을 작은 helper로 추출한다. injected storage teardown이 실패해도 +injected network release가 호출되는지 검증한다. storage와 network self-report 하나에 +의존하지 않는다. + +### D5. 운영 gate는 잔여 resource가 하나라도 있으면 열어 둔다 + +KVM E2E 후 host inventory에서 project-owned TAP/IP, bind mount, dm-snapshot, loop, +sparse `.cow`를 각각 확인한다. 잔여가 있으면 release blocker다. + +## Plan Design Review + +- operator는 하나의 “cleanup passed” 문장 대신 resource type별 결과를 본다. +- error aggregation은 첫 실패와 후속 실패를 모두 보존한다. +- cleanup failure metric은 기존대로 증가한다. +- UI 변경 없음. gate clarity와 failure discoverability review로 대체했으며 blocker 없음. + +## 수용 기준 + +1. forced `dmsetup remove` failure 뒤 COW/base `losetup -d`가 모두 호출된다. +2. full teardown이면 store removal이 호출되고, keep-store면 호출되지 않는다. +3. dm/loop/store errors가 반환 error에 함께 포함된다. +4. storage teardown error 뒤에도 TAP/IP release가 호출된다. +5. 정상 삭제의 기존 ordering과 metrics가 유지된다. +6. Go test/race/build/vet/gofmt와 KVM E2E가 통과한다. +7. E2E 후 TAP/IP, bind mount, dm device, loop device, sparse `.cow` 잔여가 없다. +8. named daemon/MCP/scheduler build 증거가 기록된다. +9. tag/release를 생성하지 않는다. + +## Spec Freeze Snapshot + +- **Objective:** deletion failure path에서 모든 host resource cleanup 계속 시도 +- **Boundaries:** ControlPlane orchestration, storage dm teardown, network TAP/IP release +- **Control decision:** early return 제거 + aggregate errors +- **Test decision:** forced fake dm failure + CP network continuation + real KVM inventory +- **KeepStore:** file preservation 불변 +- **Release rule:** resource 잔여 하나라도 blocker, tag 금지 +- **Open questions:** 없음 + diff --git a/internal/storage/snapshot.go b/internal/storage/snapshot.go index bcb844a..5eb961e 100644 --- a/internal/storage/snapshot.go +++ b/internal/storage/snapshot.go @@ -347,16 +347,34 @@ func TeardownDMSnapshotKeepStore(info *DMSnapshotInfo) error { return teardownDMSnapshot(info, true) } +type dmSnapshotTeardownOps struct { + run func(name string, args ...string) ([]byte, error) + remove func(path string) error + now func() time.Time + sleep func(time.Duration) +} + // teardownDMSnapshot releases the kernel resources created by SetupDMSnapshot. // Uses lazy unmount so any Firecracker fd that was already opened continues to work // until the process exits, at which point the underlying inode is freed. When // keepStore is true the exception store file is left on disk for a later restore. func teardownDMSnapshot(info *DMSnapshotInfo, keepStore bool) error { + return teardownDMSnapshotWithOps(info, keepStore, dmSnapshotTeardownOps{ + run: func(name string, args ...string) ([]byte, error) { + return exec.Command(name, args...).CombinedOutput() + }, + remove: os.Remove, + now: time.Now, + sleep: time.Sleep, + }) +} + +func teardownDMSnapshotWithOps(info *DMSnapshotInfo, keepStore bool, ops dmSnapshotTeardownOps) error { if info == nil { return nil } var errs []error - if out, err := exec.Command("umount", "-l", info.MountTarget).CombinedOutput(); err != nil { + if out, err := ops.run("umount", "-l", info.MountTarget); err != nil { errs = append(errs, fmt.Errorf("umount -l %s: %w: %s", info.MountTarget, err, strings.TrimSpace(string(out)))) } @@ -366,32 +384,34 @@ func teardownDMSnapshot(info *DMSnapshotInfo, keepStore bool) error { // where Firecracker process exit can lag behind the HTTP stop/delete request. removed := false var lastRemoveErr error - deadline := time.Now().Add(10 * time.Second) + deadline := ops.now().Add(10 * time.Second) for { - if out, err := exec.Command("dmsetup", "remove", "--retry", info.DMDevice).CombinedOutput(); err == nil { + if out, err := ops.run("dmsetup", "remove", "--retry", info.DMDevice); err == nil { removed = true break } else { lastRemoveErr = fmt.Errorf("dmsetup remove --retry %s: %w: %s", info.DMDevice, err, strings.TrimSpace(string(out))) } - if time.Now().After(deadline) { + if ops.now().After(deadline) { break } - time.Sleep(250 * time.Millisecond) + ops.sleep(250 * time.Millisecond) } if !removed { errs = append(errs, lastRemoveErr) - return errors.Join(errs...) } - if out, err := exec.Command("losetup", "-d", info.COWLoopDevice).CombinedOutput(); err != nil { + // Continue after a dm removal failure. The still-live dm device may make loop + // detach fail too, but skipping these attempts guarantees a leak and hides the + // independent failure details from the caller. + if out, err := ops.run("losetup", "-d", info.COWLoopDevice); err != nil { errs = append(errs, fmt.Errorf("losetup -d %s: %w: %s", info.COWLoopDevice, err, strings.TrimSpace(string(out)))) } - if out, err := exec.Command("losetup", "-d", info.LoopDevice).CombinedOutput(); err != nil { + if out, err := ops.run("losetup", "-d", info.LoopDevice); err != nil { errs = append(errs, fmt.Errorf("losetup -d %s: %w: %s", info.LoopDevice, err, strings.TrimSpace(string(out)))) } if !keepStore { - if err := os.Remove(info.ExceptionStore); err != nil && !os.IsNotExist(err) { + if err := ops.remove(info.ExceptionStore); err != nil && !os.IsNotExist(err) { errs = append(errs, fmt.Errorf("remove exception store %s: %w", info.ExceptionStore, err)) } } diff --git a/internal/storage/snapshot_teardown_test.go b/internal/storage/snapshot_teardown_test.go new file mode 100644 index 0000000..fc8f804 --- /dev/null +++ b/internal/storage/snapshot_teardown_test.go @@ -0,0 +1,91 @@ +package storage + +import ( + "errors" + "strings" + "testing" + "time" +) + +func TestTeardownDMSnapshotContinuesAfterDMRemoveFailure(t *testing.T) { + for _, keepStore := range []bool{false, true} { + name := "remove-store" + if keepStore { + name = "keep-store" + } + t.Run(name, func(t *testing.T) { + var commands []string + var removedPaths []string + nowCalls := 0 + start := time.Unix(0, 0) + ops := dmSnapshotTeardownOps{ + run: func(command string, args ...string) ([]byte, error) { + call := command + " " + strings.Join(args, " ") + commands = append(commands, call) + switch command { + case "dmsetup": + return []byte("device busy"), errors.New("forced dm failure") + case "losetup": + return []byte("still referenced"), errors.New("forced loop failure") + default: + return nil, nil + } + }, + remove: func(path string) error { + removedPaths = append(removedPaths, path) + return errors.New("forced store failure") + }, + now: func() time.Time { + nowCalls++ + if nowCalls == 1 { + return start + } + return start.Add(11 * time.Second) + }, + sleep: func(time.Duration) {}, + } + info := &DMSnapshotInfo{ + LoopDevice: "/dev/loop-base", + COWLoopDevice: "/dev/loop-cow", + DMDevice: "cow-vm-test.cow", + ExceptionStore: "/workspace/vm-test.cow", + MountTarget: "/workspace/vm-test.ext4", + } + + err := teardownDMSnapshotWithOps(info, keepStore, ops) + if err == nil { + t.Fatal("teardown error = nil, want forced failures") + } + joined := strings.Join(commands, "\n") + for _, want := range []string{ + "dmsetup remove --retry cow-vm-test.cow", + "losetup -d /dev/loop-cow", + "losetup -d /dev/loop-base", + } { + if !strings.Contains(joined, want) { + t.Fatalf("commands missing %q after forced dm failure:\n%s", want, joined) + } + } + for _, want := range []string{"dmsetup remove", "/dev/loop-cow", "/dev/loop-base"} { + if !strings.Contains(err.Error(), want) { + t.Fatalf("error %q missing %q", err, want) + } + } + if keepStore { + if len(removedPaths) != 0 { + t.Fatalf("keep-store teardown removed paths: %v", removedPaths) + } + if strings.Contains(err.Error(), "forced store failure") { + t.Fatalf("keep-store error unexpectedly contains store removal: %v", err) + } + } else { + if len(removedPaths) != 1 || removedPaths[0] != info.ExceptionStore { + t.Fatalf("removed paths = %v, want [%s]", removedPaths, info.ExceptionStore) + } + if !strings.Contains(err.Error(), "forced store failure") { + t.Fatalf("error %q missing store removal failure", err) + } + } + }) + } +} diff --git a/web/package-lock.json b/web/package-lock.json index 3d6844c..9c2e531 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -52,1740 +52,1787 @@ "tslib": "^2.4.0" } }, - "node_modules/@formatjs/ecma402-abstract": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", - "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", - "license": "MIT", - "dependencies": { - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/intl-localematcher": "0.6.2", - "decimal.js": "^10.4.3", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/fast-memoize": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", - "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", - "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "@formatjs/icu-skeleton-parser": "1.8.16", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.8.16", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", - "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/intl-localematcher": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", - "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "peer": true, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "node": ">=18" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" } }, - "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ - "x64" + "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ - "arm" + "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ - "arm64" + "arm" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ - "ppc64" + "ia32" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ - "s390x" + "loong64" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ - "x64" + "mips64el" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ - "x64" + "ppc64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ - "arm64" + "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ - "openharmony" + "linux" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ - "wasm32" + "s390x" ], "dev": true, - "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, + "os": [ + "linux" + ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ - "win32" + "netbsd" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ - "win32" + "netbsd" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", - "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8.9.0" + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" } }, - "node_modules/@sveltejs/load-config": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", - "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, "engines": { - "node": ">= 18.0.0" + "node": ">=18" } }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.2.0.tgz", - "integrity": "sha512-1SpkuMSRLfugrVX+IrKfE1RUegzo8AQzKQ6qQPfVzbcWi5IhuTPaKb5ZrLpucleFznkc4/RTeSPoRnGWFxX+EQ==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "deepmerge": "^4.3.1", - "magic-string": "^0.30.21", - "obug": "^2.1.0", - "vitefu": "^1.1.2" - }, + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, "engines": { - "node": "^20.19 || ^22.12 || >=24" - }, - "peerDependencies": { - "svelte": "^5.46.4", - "vite": "^8.0.0-beta.7 || ^8.0.0" + "node": ">=18" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" } }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "license": "MIT" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, "engines": { - "node": ">=0.4.0" + "node": ">=18" } }, - "node_modules/aria-query": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", - "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "license": "Apache-2.0", + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "license": "Apache-2.0", + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" } }, - "node_modules/cli-color": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", - "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", - "license": "ISC", + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.64", - "es6-iterator": "^2.0.3", - "memoizee": "^0.4.15", - "timers-ext": "^0.1.7" - }, - "engines": { - "node": ">=0.10" + "tslib": "^2.8.0" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" } }, - "node_modules/d": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", - "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", - "license": "ISC", + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "license": "MIT", "dependencies": { - "es5-ext": "^0.10.64", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.12" + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/devalue": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", - "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", - "license": "MIT" - }, - "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", - "hasInstallScript": true, - "license": "ISC", "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" + "tslib": "^2.8.0" } }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/es6-symbol": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", - "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", - "license": "ISC", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", "dependencies": { - "d": "^1.0.2", - "ext": "^1.7.0" - }, - "engines": { - "node": ">=0.12" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "license": "ISC", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" } }, - "node_modules/esm-env": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "license": "ISC", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/esrap": { - "version": "2.2.13", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz", - "integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@tybys/wasm-util": "^0.10.3" }, - "peerDependencies": { - "@typescript-eslint/types": "^8.2.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" }, - "peerDependenciesMeta": { - "@typescript-eslint/types": { - "optional": true - } + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, "license": "MIT", - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "license": "ISC", - "dependencies": { - "type": "^2.7.2" + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "license": "MIT" - }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "license": "MIT" + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/intl-messageformat": { - "version": "10.7.18", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", - "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", - "license": "BSD-3-Clause", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/icu-messageformat-parser": "2.11.4", - "tslib": "^2.8.0" - } - }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "license": "MIT" - }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.6" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ - "arm64" + "arm" ], "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ - "android" + "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ - "x64" + "arm64" ], "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ - "x64" + "ppc64" ], "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ - "arm" + "s390x" ], "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ - "x64" + "arm64" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" + "openharmony" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ - "x64" + "wasm32" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", + "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ "arm64" ], "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, "license": "MIT" }, - "node_modules/lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", + "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", "license": "MIT", - "dependencies": { - "es5-ext": "~0.10.2" + "peerDependencies": { + "acorn": "^8.9.0" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/@sveltejs/load-config": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", + "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", + "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "engines": { + "node": ">= 18.0.0" } }, - "node_modules/memoizee": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", - "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", - "license": "ISC", + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.2.0.tgz", + "integrity": "sha512-1SpkuMSRLfugrVX+IrKfE1RUegzo8AQzKQ6qQPfVzbcWi5IhuTPaKb5ZrLpucleFznkc4/RTeSPoRnGWFxX+EQ==", + "dev": true, + "license": "MIT", "dependencies": { - "d": "^1.0.2", - "es5-ext": "^0.10.64", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.2" }, "engines": { - "node": ">=0.12" + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" } }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=4" + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", "bin": { - "nanoid": "bin/nanoid.cjs" + "acorn": "bin/acorn" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=0.4.0" } }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "license": "ISC" - }, - "node_modules/obug": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", - "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", "engines": { - "node": ">=12.20.0" + "node": ">= 0.4" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">= 0.4" } }, - "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "readdirp": "^4.0.1" }, "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" + "node": ">= 14.16.0" }, "funding": { - "type": "individual", "url": "https://paulmillr.com/funding/" } }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "dev": true, - "license": "MIT", + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "license": "ISC", "dependencies": { - "@oxc-project/types": "=0.139.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" }, "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" + "node": ">=0.10" } }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", "dependencies": { - "mri": "^1.1.0" + "es5-ext": "^0.10.64", + "type": "^2.7.2" }, "engines": { - "node": ">=6" + "node": ">=0.12" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/svelte": { - "version": "5.56.4", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz", - "integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==", - "license": "MIT", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "license": "MIT" + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.10", - "@types/estree": "^1.0.5", - "@types/trusted-types": "^2.0.7", - "acorn": "^8.12.1", - "aria-query": "5.3.1", - "axobject-query": "^4.1.0", - "clsx": "^2.1.1", - "devalue": "^5.8.1", - "esm-env": "^1.2.1", - "esrap": "^2.2.12", - "is-reference": "^3.0.3", - "locate-character": "^3.0.0", - "magic-string": "^0.30.11", - "zimmerframe": "^1.1.2" + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" }, "engines": { - "node": ">=18" + "node": ">=0.10" } }, - "node_modules/svelte-check": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.3.tgz", - "integrity": "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==", - "dev": true, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "@sveltejs/load-config": "^0.2.0", - "chokidar": "^4.0.1", - "fdir": "^6.2.0", - "picocolors": "^1.0.0", - "sade": "^1.7.4" + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "peer": true, "bin": { - "svelte-check": "bin/svelte-check" + "esbuild": "bin/esbuild" }, "engines": { - "node": ">= 18.0.0" + "node": ">=18" }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": ">=5.0.0" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, - "node_modules/svelte-i18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.1.tgz", - "integrity": "sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==", + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz", + "integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==", "license": "MIT", "dependencies": { - "cli-color": "^2.0.3", - "deepmerge": "^4.2.2", - "esbuild": "^0.19.2", - "estree-walker": "^2", - "intl-messageformat": "^10.5.3", - "sade": "^1.8.1", - "tiny-glob": "^0.2.9" + "@jridgewell/sourcemap-codec": "^1.4.15" }, - "bin": { - "svelte-i18n": "dist/cli.js" + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 16" + "node": ">=12.0.0" }, "peerDependencies": { - "svelte": "^3 || ^4 || ^5" + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "license": "MIT" + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "license": "MIT" + }, + "node_modules/intl-messageformat": { + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ - "ppc64" + "arm64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "aix" + "android" ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ - "arm" + "arm64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "android" + "darwin" ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ - "arm64" + "x64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "android" + "darwin" ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "android" + "freebsd" ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ - "arm64" + "arm" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ - "x64" + "arm64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ - "arm" + "x64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ - "ia32" + "x64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, "engines": { - "node": ">=12" + "node": ">=0.12" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "license": "MIT", - "optional": true, - "os": [ - "linux" + "engines": { + "node": ">=4" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, "engines": { - "node": ">=12" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" }, - "node_modules/svelte-i18n/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=12.20.0" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, "engines": { - "node": ">=12" + "node": "^10 || ^12 || >=14" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], "engines": { - "node": ">=12" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "mri": "^1.1.0" + }, "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], + "node_modules/svelte": { + "version": "5.56.4", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz", + "integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.12", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/svelte-i18n/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], + "node_modules/svelte-check": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.3.tgz", + "integrity": "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.0", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, "engines": { - "node": ">=12" + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" } }, - "node_modules/svelte-i18n/node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "hasInstallScript": true, + "node_modules/svelte-i18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.1.tgz", + "integrity": "sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==", "license": "MIT", + "dependencies": { + "cli-color": "^2.0.3", + "deepmerge": "^4.2.2", + "esbuild": "^0.19.2", + "estree-walker": "^2", + "intl-messageformat": "^10.5.3", + "sade": "^1.8.1", + "tiny-glob": "^0.2.9" + }, "bin": { - "esbuild": "bin/esbuild" + "svelte-i18n": "dist/cli.js" }, "engines": { - "node": ">=12" + "node": ">= 16" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "peerDependencies": { + "svelte": "^3 || ^4 || ^5" } }, "node_modules/timers-ext": { diff --git a/web/package.json b/web/package.json index 056408e..befde7b 100644 --- a/web/package.json +++ b/web/package.json @@ -18,5 +18,10 @@ }, "dependencies": { "svelte-i18n": "^4.0.1" + }, + "overrides": { + "esbuild": "0.28.2", + "nanoid": "3.3.18", + "postcss": "8.5.26" } } From 72f721331274101f2ea276f84caebe62f5c147ae Mon Sep 17 00:00:00 2001 From: HardcoreMonk Date: Thu, 13 Aug 2026 03:53:27 +0900 Subject: [PATCH 2/2] docs: record exact release gate evidence --- ...l-project-process-status-review-2026-08-13.md | 7 ++++--- .../2026-08-13-allow-hosts-removal-handoff.md | 12 +++++++----- ...08-13-release-governance-hardening-handoff.md | 16 ++++++++++------ ...-08-13-vm-deletion-failure-cleanup-handoff.md | 12 +++++++----- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/docs/analysis/12-anvil-project-process-status-review-2026-08-13.md b/docs/analysis/12-anvil-project-process-status-review-2026-08-13.md index a16b6ea..29caabd 100644 --- a/docs/analysis/12-anvil-project-process-status-review-2026-08-13.md +++ b/docs/analysis/12-anvil-project-process-status-review-2026-08-13.md @@ -529,8 +529,8 @@ tag authorization: hold | PR #110 review/merge | 완료 | exact head `c394f7d...`의 Go CI green, unresolved thread 0에서 merge commit `794d0ae...`로 `main` 병합. CodeRabbit 완료 리뷰는 merge 직후 회수 | | strict secret gate | 완료 | tracked tree PASS, scanner allowlist/regex 완화 없음 | | KVM release-candidate gate | 완료 | full E2E `All test steps passed`, lifecycle/semantic/flock smoke는 선행 handoff에서 통과 | -| `allow_hosts` 제거 | 구현·local 검증 완료, PR 대기 | field/validation/iptables string matcher 제거, non-empty/empty/`null`/mixed-case key loud rejection, unrelated unknown metadata 호환 | -| VM 삭제 실패 cleanup | 구현·local 검증 완료, PR 대기 | CodeRabbit 사후 Major finding. forced dm failure 뒤 양 loop/store/TAP-IP cleanup continuation test, KVM resource inventory clean | +| `allow_hosts` 제거 | 구현·local/remote 검증 완료, PR #111 merge 대기 | field/validation/iptables string matcher 제거, non-empty/empty/`null`/mixed-case key loud rejection, unrelated unknown metadata 호환 | +| VM 삭제 실패 cleanup | 구현·local/remote 검증 완료, PR #111 merge 대기 | CodeRabbit 사후 Major finding. forced dm failure 뒤 양 loop/store/TAP-IP cleanup continuation test, KVM resource inventory clean | | npm audit | 폐쇄 | High 2/Moderate 2 → 0, clean install/check/build 통과 | | Web/secret CI | 구현 완료, PR 대기 | `web-and-security`, `secret-scan` 독립 job, `contents: read` | | version policy | 결정 규칙 확정 | upstream 첫 post-`v0.7.0` tag `vX.Y.Z` → `anvil-vX.Y.Z`; downstream-only 번호 금지 | @@ -554,7 +554,8 @@ CodeRabbit가 merge 완료 직후 4개 actionable comment를 게시했다. password rotation, key rollout, permission remediation을 실행할 인증 경로가 없다. 2. **Next version number:** upstream latest/main이 계속 `v0.7.0`이라 결정 규칙의 입력이 없다. 번호는 의도적으로 미할당이다. -3. **Remote integration:** `agent/next-release-gates` exact-SHA CI/review/merge가 남았다. +3. **Remote integration:** PR #111 code-bearing SHA의 Go/Web/secret CI는 green이다. + CodeRabbit는 rate limit로 실제 review를 수행하지 못했으며 PR merge가 남았다. 4. **Branch protection:** 새 CI context가 `main`에 병합된 뒤 적용해야 한다. collaborator가 owner 1명뿐이라 admin-enforced approval 1 설정 후 두 번째 reviewer가 필요하다. diff --git a/docs/operations/2026-08-13-allow-hosts-removal-handoff.md b/docs/operations/2026-08-13-allow-hosts-removal-handoff.md index d5dadf6..1b27615 100644 --- a/docs/operations/2026-08-13-allow-hosts-removal-handoff.md +++ b/docs/operations/2026-08-13-allow-hosts-removal-handoff.md @@ -5,6 +5,8 @@ - 날짜: 2026-08-13 - topic: `allow-hosts-removal` - branch: `agent/next-release-gates` +- PR: [#111](https://github.com/HardcoreMonk/anvil/pull/111) +- code-bearing commit: `0aec994089459a64d7bf9e8584f59f4bf6243e4e` - 설계: [`2026-08-13-allow-hosts-removal-design.md`](../superpowers/specs/2026-08-13-allow-hosts-removal-design.md) - 계획: @@ -47,6 +49,7 @@ legacy apply contract가 실제로 살아 있음을 보였다. - Markdown relative links 155건: PASS - `git diff --check`: PASS - full KVM `e2e_test.sh`: `All test steps passed` +- PR #111 exact code-bearing SHA CI: Go/Web/secret 3 jobs green ## Audit @@ -59,7 +62,7 @@ legacy apply contract가 실제로 살아 있음을 보였다. ## Blockers -- 이 변경의 PR exact-SHA CI와 merge가 아직 남아 있다. +- PR #111 merge가 아직 남아 있다. - 다음 public version number는 upstream post-`v0.7.0` tag 부재로 미할당이다. - deployment host credential/key/permission remediation이 외부 접근 부재로 미완료다. @@ -76,16 +79,15 @@ legacy apply contract가 실제로 살아 있음을 보였다. ## Current Lifecycle Stage -local implement, verification, code review와 release handoff 작성이 끝났다. PR exact-SHA -remote CI/merge 전이므로 `operate`에는 진입하지 않았다. +local implement, verification, code review, exact code-bearing SHA remote CI와 release +handoff 작성이 끝났다. PR merge 전이므로 `operate`에는 진입하지 않았다. ## Next Action -통합 변경을 PR로 올리고 exact-SHA CI/CodeRabbit review를 처리한다. +PR #111의 final head CI를 확인하고 병합한다. ## Follow-Up Tasks 1. 외부 profile에서 `allow_hosts` key 제거 여부를 배포 전 확인 2. 통합 PR review/merge 3. host security blocker와 upstream-derived version blocker 해소 전 tag 금지 - diff --git a/docs/operations/2026-08-13-release-governance-hardening-handoff.md b/docs/operations/2026-08-13-release-governance-hardening-handoff.md index 07f5a3f..826ace5 100644 --- a/docs/operations/2026-08-13-release-governance-hardening-handoff.md +++ b/docs/operations/2026-08-13-release-governance-hardening-handoff.md @@ -5,6 +5,8 @@ - 날짜: 2026-08-13 - topic: `release-governance-hardening` - branch: `agent/next-release-gates` +- PR: [#111](https://github.com/HardcoreMonk/anvil/pull/111) +- code-bearing commit: `0aec994089459a64d7bf9e8584f59f4bf6243e4e` - 설계: [`2026-08-13-release-governance-hardening-design.md`](../superpowers/specs/2026-08-13-release-governance-hardening-design.md) - 계획: @@ -50,7 +52,10 @@ patched graph: - Go full/race/named builds/vet/gofmt/govulncheck: 통과 - Markdown relative links 155건, `git diff --check`: PASS -remote exact-SHA 결과와 branch-protection read-back은 PR/merge 후 이 문서에 추가한다. +PR #111 code-bearing exact SHA의 `build-and-test`, `web-and-security`, `secret-scan`은 모두 +green이다. CodeRabbit status는 service review-rate-limit으로 pass됐지만 실제 review는 +수행되지 않았다. 이를 approval로 세지 않았고 manual actual-diff review에서 blocking +finding은 없었다. branch-protection read-back은 merge 후 추가한다. ## Audit @@ -65,7 +70,7 @@ remote exact-SHA 결과와 branch-protection read-back은 PR/merge 후 이 문 1. next public version number는 post-`v0.7.0` upstream tag 부재로 미할당이다. 2. deployment host A1/A2는 인증/도달 경로 부재로 미완료다. -3. 통합 PR CI/review/merge와 merge 후 branch protection 적용이 남았다. +3. PR #111 merge와 merge 후 branch protection 적용이 남았다. ## Warnings @@ -82,12 +87,12 @@ remote exact-SHA 결과와 branch-protection read-back은 PR/merge 후 이 문 ## Current Lifecycle Stage -local implement/verification/code review 완료. PR exact-SHA 검증과 external protection이 -남아 있어 `operate` 미진입이다. +local implement/verification/code review와 code-bearing exact-SHA remote CI 완료. PR merge와 +external protection이 남아 있어 `operate` 미진입이다. ## Next Action -통합 PR을 생성하고 CI/CodeRabbit review를 처리해 병합한 뒤 branch protection을 적용한다. +PR #111 final head CI를 확인해 병합한 뒤 branch protection을 적용한다. ## Follow-Up Tasks @@ -95,4 +100,3 @@ local implement/verification/code review 완료. PR exact-SHA 검증과 external 2. `main` protection 적용/read-back 3. 두 번째 eligible reviewer 추가 4. host A1/A2와 upstream version input 전 tag 금지 - diff --git a/docs/operations/2026-08-13-vm-deletion-failure-cleanup-handoff.md b/docs/operations/2026-08-13-vm-deletion-failure-cleanup-handoff.md index 9daa9dd..602b021 100644 --- a/docs/operations/2026-08-13-vm-deletion-failure-cleanup-handoff.md +++ b/docs/operations/2026-08-13-vm-deletion-failure-cleanup-handoff.md @@ -5,6 +5,8 @@ - 날짜: 2026-08-13 - topic: `vm-deletion-failure-cleanup` - branch: `agent/next-release-gates` +- PR: [#111](https://github.com/HardcoreMonk/anvil/pull/111) +- code-bearing commit: `0aec994089459a64d7bf9e8584f59f4bf6243e4e` - trigger: merged PR #110에 대한 CodeRabbit 사후 Major review - 설계: [`2026-08-13-vm-deletion-failure-cleanup-design.md`](../superpowers/specs/2026-08-13-vm-deletion-failure-cleanup-design.md) @@ -67,6 +69,7 @@ E2E가 남긴 root-owned Town Wall log 8개와 0-byte rootfs placeholder 7개는 - Go full/race/named builds/vet/module verify/gofmt: 통과 - govulncheck reachable 0, tracked secret scan PASS - full KVM E2E: 통과 +- PR #111 exact code-bearing SHA Go/Web/secret CI: 통과 - `git diff --check`: 통과 ## Audit @@ -79,7 +82,7 @@ E2E가 남긴 root-owned Town Wall log 8개와 0-byte rootfs placeholder 7개는 ## Blockers -- 통합 PR exact-SHA CI와 merge가 남아 있다. +- PR #111 merge가 남아 있다. - 실제 kernel이 resource를 계속 busy로 유지하면 후속 detach도 실패할 수 있다. 이 경우 error와 metric이 남으며 release gate는 host inventory clean 전까지 열려 있다. - deployment host security operations가 미완료다. @@ -96,16 +99,15 @@ E2E가 남긴 root-owned Town Wall log 8개와 0-byte rootfs placeholder 7개는 ## Current Lifecycle Stage -implement, local verification, code review, release handoff 완료. remote PR 검증 전이므로 -`operate` 미진입이다. +implement, local/KVM verification, code review, exact code-bearing SHA remote CI, release +handoff 완료. PR merge 전이므로 `operate` 미진입이다. ## Next Action -통합 PR에서 exact-SHA CI와 review를 받고 병합한다. +PR #111 final head CI를 확인하고 병합한다. ## Follow-Up Tasks 1. branch exact-SHA CI/CodeRabbit review 2. 배포 전 host resource inventory 재확인 3. host/security/version blockers 전 tag 금지 -