fix(sandbox): two sandbox bugs blocking Peter Steinberger beta test
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 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-6
@@ -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
|
||||
|
||||
+9
-2
@@ -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<ServiceResponse> 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
|
||||
|
||||
+1
-1
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user