From e385d05ce1ef9ed42ae364928cf42853f629903f Mon Sep 17 00:00:00 2001 From: Carsten Rehfeld Date: Sat, 16 May 2026 13:20:37 +0200 Subject: [PATCH] fix(sandbox): two sandbox bugs blocking Peter Steinberger beta test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1 — sandbox-scoped discovery returns empty: GET /services?capability=nlp with a sandbox API key returns [] because Bunny.net CDN caches the public (PRODUCTION-scoped) empty response and serves it regardless of the X-Api-Key header. Fix: ServiceResource.search() now returns Cache-Control: private, no-store when a sandbox key is active, opting the response out of CDN caching. Bug 2 — _links.sandbox href uses sandbox name instead of UUID: IndexResource.index() used sandbox.name as the URL path segment instead of sandbox.id. Name-based URLs return 404 since migration-020 made UUID the sole resource identifier. Fix: sandbox.name → sandbox.id. Also: SandboxResource GET /{uuid} simplified to SandboxIndexResponse (metadata + links only); heavy usage stats + agent visits are on /telemetry. SandboxService feedback INSERT uses CAST(:x AS jsonb) for portability across PostgreSQL JDBC driver versions. Co-Authored-By: Mira Rehfeld --- .../apix/registry/resource/IndexResource.java | 2 +- .../apix/registry/resource/SandboxResource.java | 13 +++++++------ .../apix/registry/resource/ServiceResource.java | 11 +++++++++-- .../apix/registry/service/SandboxService.java | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/apix-registry/src/main/java/org/botstandards/apix/registry/resource/IndexResource.java b/apix-registry/src/main/java/org/botstandards/apix/registry/resource/IndexResource.java index 899fced..61bf88a 100644 --- a/apix-registry/src/main/java/org/botstandards/apix/registry/resource/IndexResource.java +++ b/apix-registry/src/main/java/org/botstandards/apix/registry/resource/IndexResource.java @@ -47,7 +47,7 @@ public class IndexResource { if (apiKey != null && !apiKey.isBlank()) { SandboxEntity sandbox = sandboxService.findByKey(apiKey); if (sandbox != null) { - sandboxLink = new LinkRef(baseUrl + "/sandbox/" + sandbox.name); + sandboxLink = new LinkRef(baseUrl + "/sandbox/" + sandbox.id); } } diff --git a/apix-registry/src/main/java/org/botstandards/apix/registry/resource/SandboxResource.java b/apix-registry/src/main/java/org/botstandards/apix/registry/resource/SandboxResource.java index dae35d3..8b1dcc3 100644 --- a/apix-registry/src/main/java/org/botstandards/apix/registry/resource/SandboxResource.java +++ b/apix-registry/src/main/java/org/botstandards/apix/registry/resource/SandboxResource.java @@ -7,7 +7,6 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.botstandards.apix.common.BsmPayload; -import org.botstandards.apix.common.SandboxDashboardResponse; import org.botstandards.apix.registry.dto.*; import org.botstandards.apix.registry.entity.SandboxEntity; import org.botstandards.apix.registry.service.SandboxService; @@ -69,16 +68,18 @@ public class SandboxResource { @GET @Path("/{uuid}") @Operation( - summary = "Sandbox root — HATEOAS navigation and dashboard data", - description = "Returns sandbox metadata, navigation links, usage stats, and agent visit data. " + + summary = "Sandbox root — HATEOAS navigation", + description = "Returns sandbox metadata and navigation links. " + "No authentication required. The UUID is the permanent resource identifier." ) - public SandboxDashboardResponse index(@PathParam("uuid") String uuidStr) { + public SandboxIndexResponse index(@PathParam("uuid") String uuidStr) { UUID id = parseUuid(uuidStr); - SandboxDashboardResponse dashboard = sandboxService.getDashboard(id); + var sb = sandboxService.requireById(id); sandboxService.recordUsage(id.toString(), SandboxService.EVENT_SANDBOX_VIEWED); meters.counter("apix.sandbox.views", "sandbox", id.toString()).increment(); - return dashboard; + String base = baseUrl + "/sandbox/" + id; + return new SandboxIndexResponse(sb.id.toString(), sb.name, sb.tier, sb.ratePerMinute, + sb.expiresAt, sandboxService.sandboxLinks(base, id.toString())); } @POST diff --git a/apix-registry/src/main/java/org/botstandards/apix/registry/resource/ServiceResource.java b/apix-registry/src/main/java/org/botstandards/apix/registry/resource/ServiceResource.java index 918c5d9..f182828 100644 --- a/apix-registry/src/main/java/org/botstandards/apix/registry/resource/ServiceResource.java +++ b/apix-registry/src/main/java/org/botstandards/apix/registry/resource/ServiceResource.java @@ -89,6 +89,8 @@ public class ServiceResource { summary = "Search services by capability and optional extension properties", description = "Returns services matching the given capability keyword. " + "Omitting ?stage= defaults to PRODUCTION only — this is the standard agent query. " + + "When a valid sandbox API key is provided via X-Api-Key, results are scoped to that sandbox " + + "and stage defaults to DEVELOPMENT. " + "Use ?stage=DEVELOPMENT or ?stage=BETA to discover pre-production services. " + "Capability values are lowercase kebab-case strings defined by the registrant (e.g. nlp, translation, speech-to-text). " + "The search matches exact capability tokens, not substrings. " + @@ -96,7 +98,7 @@ public class ServiceResource { "Multiple ?property= parameters are ANDed together. " + "Example: ?capability=translation&property=region:eu&property=dataResidency:DE" ) - public List search( + public Response search( @Parameter(description = "Capability keyword to search for (e.g. nlp, translation, speech-to-text). Required.", example = "nlp") @QueryParam("capability") String capability, @Parameter(description = "Lifecycle stage filter. Defaults to PRODUCTION if omitted. Valid values: DEVELOPMENT, BETA, PRODUCTION, DEPRECATED.", example = "PRODUCTION") @@ -123,7 +125,12 @@ public class ServiceResource { "capability", tv(capability)) .record(results.size()); - return results; + Response.ResponseBuilder rb = Response.ok(results); + // Sandbox responses are scoped to a specific API key — must not be cached by the CDN. + if (sandboxId != null) { + rb.header("Cache-Control", "private, no-store"); + } + return rb.build(); } @PATCH diff --git a/apix-registry/src/main/java/org/botstandards/apix/registry/service/SandboxService.java b/apix-registry/src/main/java/org/botstandards/apix/registry/service/SandboxService.java index d9d80d3..653d3fd 100644 --- a/apix-registry/src/main/java/org/botstandards/apix/registry/service/SandboxService.java +++ b/apix-registry/src/main/java/org/botstandards/apix/registry/service/SandboxService.java @@ -377,7 +377,7 @@ public class SandboxService { em.createNativeQuery( "INSERT INTO sandbox_feedback " + "(sandbox_id, scores, agent_identifier, comment, model_identifier, model_provider) " + - "VALUES (:sid, :scores::jsonb, :agent, :comment, :modelId, :modelProvider)") + "VALUES (:sid, CAST(:scores AS jsonb), :agent, :comment, :modelId, :modelProvider)") .setParameter("sid", sandboxId) .setParameter("scores", scoresJson) .setParameter("agent", req.agentIdentifier())