mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 04:02:18 +01:00
fix(server): flatten feature unavailable responses
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
839bbd67a3
commit
fce1281894
5 files changed
+26
-5
No files matched your search
@@ -642,7 +642,7 @@ The approved term set is:
|
||||
|
||||
Core owns the closed, versioned host-feature keys `apps.named_mcp_gateways` and `apps.access_profiles`. Only the canonical `paperclipai.paperclip-ee` plugin may declare them in `PaperclipPluginManifestV1.hostFeatures`, and only a stored plugin record in `ready` state enables its valid declaration. Missing, installed, disabled, error, upgrade-pending, uninstalled, malformed, or non-canonical providers resolve to an empty feature set.
|
||||
|
||||
The server injects the non-sensitive effective feature snapshot into both static and Vite-served HTML before React starts, and exposes the same snapshot through an authenticated API for lifecycle revalidation. UI visibility is advisory; server routes remain authoritative. Named-gateway management, token, metadata, public, and ID-based MCP routes require `apps.named_mcp_gateways`. Profile administration routes require `apps.access_profiles`. Unavailable management routes return `404` with `code: "feature_unavailable"`.
|
||||
The server injects the non-sensitive effective feature snapshot into both static and Vite-served HTML before React starts, and exposes the same snapshot through an authenticated API for lifecycle revalidation. UI visibility is advisory; server routes remain authoritative. Named-gateway management, token, metadata, public, and ID-based MCP routes require `apps.named_mcp_gateways`. Profile administration routes require `apps.access_profiles`. Unavailable management routes return `404` with the flat body `{ "error": "Feature unavailable", "code": "feature_unavailable", "feature": "<host-feature-key>" }`.
|
||||
|
||||
Core profile list/effective reads, internal tool-gateway sessions, and runtime policy enforcement stay available in all editions. Feature disable and plugin uninstall do not delete gateway, profile, binding, entry, token, or policy rows; returning the canonical provider to `ready` restores the management surfaces over the existing data.
|
||||
|
||||
|
||||
@@ -112,6 +112,25 @@ describe("errorHandler", () => {
|
||||
expect(res.__errorContext).toBeUndefined();
|
||||
});
|
||||
|
||||
it("flattens feature-unavailable metadata into the stable response shape", () => {
|
||||
const req = makeReq();
|
||||
const res = makeRes();
|
||||
const next = vi.fn() as unknown as NextFunction;
|
||||
const err = new HttpError(404, "Feature unavailable", {
|
||||
code: "feature_unavailable",
|
||||
feature: "apps.access_profiles",
|
||||
});
|
||||
|
||||
errorHandler(err, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: "Feature unavailable",
|
||||
code: "feature_unavailable",
|
||||
feature: "apps.access_profiles",
|
||||
});
|
||||
});
|
||||
|
||||
it("records responsible-user denial codes on the active agent run", () => {
|
||||
const db = { marker: "db" };
|
||||
const req = {
|
||||
|
||||
@@ -534,10 +534,10 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
];
|
||||
for (const response of gatedResponses) {
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toMatchObject({
|
||||
expect(response.body).toEqual({
|
||||
error: "Feature unavailable",
|
||||
code: "feature_unavailable",
|
||||
details: { code: "feature_unavailable", feature: "apps.access_profiles" },
|
||||
feature: "apps.access_profiles",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -584,7 +584,7 @@ describeEmbeddedPostgres("tool gateway acceptance", () => {
|
||||
|
||||
for (const response of responses) {
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toMatchObject({
|
||||
expect(response.body).toEqual({
|
||||
error: "Feature unavailable",
|
||||
code: "feature_unavailable",
|
||||
feature: "apps.named_mcp_gateways",
|
||||
|
||||
@@ -91,6 +91,7 @@ export function errorHandler(
|
||||
: null;
|
||||
const redactedSkillPolicyDenial = isRedactedSkillPolicyDenial(details);
|
||||
const workspaceRepairPreconditionFailure = details?.code === "workspace_repair_precondition_failed";
|
||||
const featureUnavailable = details?.code === "feature_unavailable";
|
||||
const structuredConnectionError = new Set([
|
||||
"user_authorization_required",
|
||||
"grant_revoked",
|
||||
@@ -113,6 +114,7 @@ export function errorHandler(
|
||||
res.status(err.status).json({
|
||||
error: err.message,
|
||||
...(typeof details?.code === "string" ? { code: details.code } : {}),
|
||||
...(featureUnavailable && typeof details?.feature === "string" ? { feature: details.feature } : {}),
|
||||
...(redactedSkillPolicyDenial && typeof details?.reason === "string" ? { reason: details.reason } : {}),
|
||||
...(workspaceRepairPreconditionFailure && typeof details?.reason === "string" ? { reason: details.reason } : {}),
|
||||
...(workspaceRepairPreconditionFailure && typeof details?.repairPhase === "string"
|
||||
@@ -124,7 +126,7 @@ export function errorHandler(
|
||||
...(structuredConnectionError && details?.connection ? { connection: details.connection } : {}),
|
||||
...(structuredConnectionError && details?.subject ? { subject: details.subject } : {}),
|
||||
...(structuredConnectionError && typeof details?.grantId === "string" ? { grantId: details.grantId } : {}),
|
||||
...(!redactedSkillPolicyDenial && !workspaceRepairPreconditionFailure && err.details
|
||||
...(!redactedSkillPolicyDenial && !workspaceRepairPreconditionFailure && !featureUnavailable && err.details
|
||||
? { details: err.details }
|
||||
: {}),
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user