diff --git a/ci/wait-for-healthy.sh b/ci/wait-for-healthy.sh index c903aaf..5995f28 100755 --- a/ci/wait-for-healthy.sh +++ b/ci/wait-for-healthy.sh @@ -10,7 +10,7 @@ MAX_RETRIES="${2:-60}" echo "Waiting for ${HOST} to be healthy..." for i in $(seq 1 "$MAX_RETRIES"); do - if curl -sf --connect-timeout 2 "${HOST}/api/v1/auth/login" -o /dev/null 2>/dev/null; then + if curl -sf --connect-timeout 2 "${HOST}/api/v1/health" -o /dev/null 2>/dev/null; then echo "Server healthy after ${i}s" exit 0 fi diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 9159888..81bf0ef 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -18,7 +18,7 @@ services: armature: healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost:80/api/v1/auth/login"] + test: ["CMD", "curl", "-sf", "http://localhost:80/api/v1/health"] interval: 2s timeout: 3s retries: 30 diff --git a/server/handlers/packages.go b/server/handlers/packages.go index 464eb63..f5b8a8e 100644 --- a/server/handlers/packages.go +++ b/server/handlers/packages.go @@ -524,7 +524,7 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) { pkgPath := filepath.Join(h.bundledDir, libID+".pkg") if _, statErr := os.Stat(pkgPath); statErr == nil { log.Printf("[packages] auto-installing bundled dependency %s for %s", libID, pkgID) - if _, installErr := installBundledPackage(pkgPath, h.packagesDir, h.stores, h.runner); installErr != nil { + if _, installErr := installBundledPackage(pkgPath, h.packagesDir, h.stores, h.runner, c.GetString("user_id")); installErr != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("dependency %s auto-install failed: %v", libID, installErr), }) diff --git a/server/handlers/packages_bundled.go b/server/handlers/packages_bundled.go index 63b447e..3d06439 100644 --- a/server/handlers/packages_bundled.go +++ b/server/handlers/packages_bundled.go @@ -51,6 +51,10 @@ func InstallBundledPackages(bundledDir, packagesDir, allowlist string, stores st return } + // Resolve first admin user ID for FK-constrained writes (default_surface, + // workflow creation). Falls back to empty string if no admin exists yet. + adminUserID := resolveFirstAdminID(stores) + // Parse allowlist: "" → curated defaults, "*" → all, "a,b" → explicit list allowed := parseBundleAllowlist(allowlist) if allowed != nil { @@ -78,7 +82,7 @@ func InstallBundledPackages(bundledDir, packagesDir, allowlist string, stores st } pkgPath := filepath.Join(bundledDir, entry.Name()) - result, err := installBundledPackage(pkgPath, packagesDir, stores, runner) + result, err := installBundledPackage(pkgPath, packagesDir, stores, runner, adminUserID) if err != nil { log.Printf("[bundled] ⚠️ Failed to install %s: %v", entry.Name(), err) continue @@ -125,7 +129,7 @@ func parseBundleAllowlist(s string) map[string]bool { // installBundledPackage installs a single .pkg archive if its package ID // doesn't already exist in the database. Returns "installed" or "skipped". -func installBundledPackage(pkgPath, packagesDir string, stores store.Stores, runner *sandbox.Runner) (string, error) { +func installBundledPackage(pkgPath, packagesDir string, stores store.Stores, runner *sandbox.Runner, adminUserID string) (string, error) { ctx := context.Background() // Open as zip @@ -197,8 +201,12 @@ func installBundledPackage(pkgPath, packagesDir string, stores store.Stores, run } } - // Sync permissions — use a minimal gin.Context for the functions that need it + // Sync permissions — use a minimal gin.Context for the functions that need it. + // Set user_id so FK-constrained writes (workflow creation) reference a real user. fakeCtx := newBackgroundGinContext() + if adminUserID != "" { + fakeCtx.Set("user_id", adminUserID) + } declaredPerms := SyncManifestPermissions(fakeCtx, stores, pkgID, manifest) // Bundled packages are trusted — auto-grant all declared permissions @@ -250,7 +258,7 @@ func installBundledPackage(pkgPath, packagesDir string, stores store.Stores, run if (pkgType == "surface" || pkgType == "full") { if raw, err := stores.GlobalConfig.Get(ctx, "default_surface"); err != nil || raw == nil { dflt := models.JSONMap{"id": pkgID} - if setErr := stores.GlobalConfig.Set(ctx, "default_surface", dflt, ""); setErr != nil { + if setErr := stores.GlobalConfig.Set(ctx, "default_surface", dflt, adminUserID); setErr != nil { log.Printf("[bundled] failed to auto-set default_surface to %s: %v", pkgID, setErr) } } @@ -324,6 +332,18 @@ func extractPackageAssets(zr *zip.ReadCloser, packagesDir, pkgID string) error { return nil } +// resolveFirstAdminID looks up the first user (the bootstrap admin). +// BootstrapAdmin always runs before InstallBundledPackages, so the +// first user by creation order is the admin. Returns "" if no users exist. +func resolveFirstAdminID(stores store.Stores) string { + ctx := context.Background() + users, _, err := stores.Users.List(ctx, store.ListOptions{Limit: 1}) + if err != nil || len(users) == 0 { + return "" + } + return users[0].ID +} + // newBackgroundGinContext creates a minimal gin.Context backed by a // background context. Used by startup code that calls functions // originally designed for HTTP handlers.