@cosmicdrift/kumiko-bundled-features 0.240.0 → 0.242.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +16 -7
- package/src/admin-shell/__tests__/overview-allowlist.test.ts +30 -0
- package/src/admin-shell/__tests__/overview-screens.boot.test.ts +31 -23
- package/src/admin-shell/changes.json +6 -0
- package/src/admin-shell/feature.ts +60 -6
- package/src/admin-shell/i18n.ts +0 -7
- package/src/admin-shell/web/client-plugin.tsx +1 -11
- package/src/agent-tools/__tests__/agent-doc-lint.test.ts +40 -0
- package/src/agent-tools/__tests__/agent-manifest.test.ts +36 -0
- package/src/agent-tools/__tests__/tool-catalog.test.ts +51 -0
- package/src/agent-tools/agent-doc-lint.ts +14 -3
- package/src/agent-tools/agent-manifest.ts +16 -3
- package/src/agent-tools/feature.ts +1 -1
- package/src/audit/__tests__/audit-screens.boot.test.ts +26 -6
- package/src/audit/changes.json +7 -0
- package/src/audit/feature.ts +57 -7
- package/src/audit/handlers/list.query.ts +50 -14
- package/src/audit/i18n.ts +3 -18
- package/src/audit/web/client-plugin.tsx +1 -7
- package/src/audit/web/index.ts +0 -2
- package/src/config/__tests__/config.integration.test.ts +31 -0
- package/src/config/handlers/readiness.query.ts +8 -1
- package/src/jobs/__tests__/jobs-screens.boot.test.ts +7 -2
- package/src/jobs/__tests__/jobs-security.integration.test.ts +41 -2
- package/src/jobs/changes.json +7 -0
- package/src/jobs/feature.ts +101 -9
- package/src/jobs/handlers/list.query.ts +21 -4
- package/src/jobs/handlers/trigger.write.ts +16 -3
- package/src/jobs/i18n.ts +3 -25
- package/src/jobs/index.ts +1 -0
- package/src/jobs/web/client-plugin.tsx +1 -7
- package/src/jobs/web/index.ts +0 -2
- package/src/tenant/__tests__/tenant.integration.test.ts +32 -0
- package/src/tenant/handlers/list.query.ts +1 -0
- package/src/tier-engine/__tests__/drift.test.ts +1 -1
- package/src/tier-engine/__tests__/tier-engine-screens.boot.test.ts +82 -0
- package/src/tier-engine/changes.json +9 -1
- package/src/tier-engine/constants.ts +5 -5
- package/src/tier-engine/feature.ts +36 -10
- package/src/tier-engine/i18n.ts +19 -22
- package/src/tier-engine/web/client-plugin.tsx +5 -11
- package/src/tier-engine/web/index.ts +0 -1
- package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +99 -0
- package/src/user-data-rights/changes.json +7 -0
- package/src/user-data-rights/constants.ts +20 -6
- package/src/user-data-rights/feature.ts +113 -14
- package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +29 -71
- package/src/user-data-rights/web/client-plugin.tsx +19 -21
- package/src/user-data-rights/web/i18n.ts +6 -11
- package/src/user-data-rights/web/index.ts +1 -1
- package/src/user-data-rights/web/privacy-center-screen.tsx +32 -245
- package/src/user-profile/__tests__/profile-screen.boot.test.ts +97 -0
- package/src/user-profile/__tests__/profile-screen.test.tsx +140 -104
- package/src/user-profile/changes.json +7 -0
- package/src/user-profile/constants.ts +15 -7
- package/src/user-profile/feature.ts +96 -9
- package/src/user-profile/i18n.ts +8 -12
- package/src/user-profile/web/client-plugin.ts +22 -5
- package/src/user-profile/web/index.ts +1 -1
- package/src/user-profile/web/profile-screen.tsx +122 -265
- package/src/admin-shell/web/overview-layout.tsx +0 -69
- package/src/admin-shell/web/overview-query.ts +0 -15
- package/src/admin-shell/web/platform-overview-screen.tsx +0 -91
- package/src/admin-shell/web/tenant-overview-screen.tsx +0 -91
- package/src/audit/__tests__/audit-log-screen.test.tsx +0 -302
- package/src/audit/web/audit-log-detail-screen.tsx +0 -169
- package/src/audit/web/audit-log-screen.tsx +0 -290
- package/src/jobs/web/__tests__/job-runs-screen.test.tsx +0 -225
- package/src/jobs/web/job-run-detail-screen.tsx +0 -181
- package/src/jobs/web/job-runs-screen.tsx +0 -290
- package/src/tier-engine/web/tier-admin-screen.tsx +0 -168
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.242.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -130,12 +130,12 @@
|
|
|
130
130
|
"./workflow-runner": "./src/workflow-runner/index.ts"
|
|
131
131
|
},
|
|
132
132
|
"dependencies": {
|
|
133
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
134
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
135
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
136
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
137
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
138
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
133
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.242.0",
|
|
134
|
+
"@cosmicdrift/kumiko-framework": "0.242.0",
|
|
135
|
+
"@cosmicdrift/kumiko-headless": "0.242.0",
|
|
136
|
+
"@cosmicdrift/kumiko-renderer": "0.242.0",
|
|
137
|
+
"@cosmicdrift/kumiko-renderer-web": "0.242.0",
|
|
138
|
+
"@cosmicdrift/kumiko-types": "0.242.0",
|
|
139
139
|
"@mollie/api-client": "^4.5.0",
|
|
140
140
|
"@node-rs/argon2": "^2.0.2",
|
|
141
141
|
"@types/mailparser": "^3.4.6",
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@testing-library/user-event": "^14.6.1",
|
|
166
166
|
"@types/qrcode": "^1.5.5",
|
|
167
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
168
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
167
|
+
"@cosmicdrift/kumiko-locale-de": "0.242.0",
|
|
168
|
+
"@cosmicdrift/kumiko-locale-es": "0.242.0"
|
|
169
169
|
}
|
|
170
170
|
}
|
|
@@ -39,12 +39,10 @@ import {
|
|
|
39
39
|
ADMIN_SHELL_FEATURE,
|
|
40
40
|
DEFAULT_PLATFORM_WORKSPACE_ID,
|
|
41
41
|
DEFAULT_TENANT_WORKSPACE_ID,
|
|
42
|
+
TENANT_OVERVIEW_SCREEN_ID,
|
|
42
43
|
} from "../constants";
|
|
43
44
|
import { createAdminShellFeature } from "../feature";
|
|
44
|
-
import {
|
|
45
|
-
TENANT_OVERVIEW_ALLOWED_QUERIES,
|
|
46
|
-
TENANT_OVERVIEW_FORBIDDEN_QUERIES,
|
|
47
|
-
} from "../overview-allowlist";
|
|
45
|
+
import { isOverviewQueryAllowed, TENANT_OVERVIEW_FORBIDDEN_QUERIES } from "../overview-allowlist";
|
|
48
46
|
|
|
49
47
|
let stack: TestStack;
|
|
50
48
|
let TENANT_ID: TenantId;
|
|
@@ -175,9 +173,20 @@ describe("SystemAdmin platform queries", () => {
|
|
|
175
173
|
});
|
|
176
174
|
|
|
177
175
|
describe("tenant overview query allowlist", () => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
// The tenant-overview screen dispatches only the queries baked into its
|
|
177
|
+
// own dashboard panels (no client-side allowlist gate anymore, fw#2312) —
|
|
178
|
+
// this checks the actual screen definition against the allowlist instead
|
|
179
|
+
// of the allowlist against itself.
|
|
180
|
+
test("tenant-overview screen panels stay inside the allowlist and off the forbidden list", () => {
|
|
181
|
+
const screen = createAdminShellFeature().screens[TENANT_OVERVIEW_SCREEN_ID];
|
|
182
|
+
if (screen?.type !== "dashboard")
|
|
183
|
+
throw new Error("expected tenant-overview to be a dashboard screen");
|
|
184
|
+
for (const panel of screen.panels) {
|
|
185
|
+
if (panel.kind !== "stat") throw new Error(`expected stat panel, got ${panel.kind}`);
|
|
186
|
+
expect(isOverviewQueryAllowed("tenant", panel.query)).toBe(true);
|
|
187
|
+
expect((TENANT_OVERVIEW_FORBIDDEN_QUERIES as readonly string[]).includes(panel.query)).toBe(
|
|
188
|
+
false,
|
|
189
|
+
);
|
|
181
190
|
}
|
|
182
191
|
});
|
|
183
192
|
|
|
@@ -2,6 +2,8 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { JobQueries } from "../../jobs/constants";
|
|
3
3
|
import { TenantQueries } from "../../tenant/constants";
|
|
4
4
|
import { UserQueries } from "../../user/constants";
|
|
5
|
+
import { PLATFORM_OVERVIEW_SCREEN_ID, TENANT_OVERVIEW_SCREEN_ID } from "../constants";
|
|
6
|
+
import { createAdminShellFeature } from "../feature";
|
|
5
7
|
import {
|
|
6
8
|
isOverviewQueryAllowed,
|
|
7
9
|
PLATFORM_OVERVIEW_ALLOWED_QUERIES,
|
|
@@ -9,6 +11,19 @@ import {
|
|
|
9
11
|
TENANT_OVERVIEW_FORBIDDEN_QUERIES,
|
|
10
12
|
} from "../overview-allowlist";
|
|
11
13
|
|
|
14
|
+
// Every screen dispatches only the queries baked into its own panel
|
|
15
|
+
// definitions (no client-side allowlist gate anymore, fw#2312) — the
|
|
16
|
+
// allowlist's regression value now lives in this static cross-check instead
|
|
17
|
+
// of a runtime guard.
|
|
18
|
+
function statPanelQueries(screenId: string): readonly string[] {
|
|
19
|
+
const screen = createAdminShellFeature().screens[screenId];
|
|
20
|
+
if (screen?.type !== "dashboard") throw new Error(`expected dashboard screen: ${screenId}`);
|
|
21
|
+
return screen.panels.map((panel) => {
|
|
22
|
+
if (panel.kind !== "stat") throw new Error(`expected stat panel on ${screenId}: ${panel.kind}`);
|
|
23
|
+
return panel.query;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
describe("overview query allowlist", () => {
|
|
13
28
|
test("tenant allowlist excludes platform-only queries", () => {
|
|
14
29
|
for (const forbidden of TENANT_OVERVIEW_FORBIDDEN_QUERIES) {
|
|
@@ -41,3 +56,18 @@ describe("overview query allowlist", () => {
|
|
|
41
56
|
expect(isOverviewQueryAllowed("tenant", UserQueries.list)).toBe(false);
|
|
42
57
|
});
|
|
43
58
|
});
|
|
59
|
+
|
|
60
|
+
describe("overview screen panels vs allowlist (fw#2312 regression)", () => {
|
|
61
|
+
test("tenant-overview panels use only allowlisted, never forbidden, queries", () => {
|
|
62
|
+
for (const query of statPanelQueries(TENANT_OVERVIEW_SCREEN_ID)) {
|
|
63
|
+
expect(isOverviewQueryAllowed("tenant", query)).toBe(true);
|
|
64
|
+
expect((TENANT_OVERVIEW_FORBIDDEN_QUERIES as readonly string[]).includes(query)).toBe(false);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("platform-overview panels use only allowlisted queries", () => {
|
|
69
|
+
for (const query of statPanelQueries(PLATFORM_OVERVIEW_SCREEN_ID)) {
|
|
70
|
+
expect(isOverviewQueryAllowed("platform", query)).toBe(true);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -1,35 +1,43 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { access
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { tierEngineFeature } from "../../tier-engine/feature";
|
|
8
|
-
import { createUserFeature } from "../../user/feature";
|
|
2
|
+
import { access } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { ConfigQueries } from "../../config/constants";
|
|
4
|
+
import { JobQueries } from "../../jobs/constants";
|
|
5
|
+
import { TenantQueries } from "../../tenant/constants";
|
|
6
|
+
import { UserQueries } from "../../user/constants";
|
|
9
7
|
import { PLATFORM_OVERVIEW_SCREEN_ID, TENANT_OVERVIEW_SCREEN_ID } from "../constants";
|
|
10
8
|
import { createAdminShellFeature } from "../feature";
|
|
11
9
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
const adminShell = createAdminShellFeature();
|
|
11
|
+
|
|
12
|
+
function statPanelQueries(screenId: string): readonly string[] {
|
|
13
|
+
const screen = adminShell.screens[screenId];
|
|
14
|
+
if (screen?.type !== "dashboard") throw new Error(`expected dashboard screen: ${screenId}`);
|
|
15
|
+
return screen.panels.map((panel) => {
|
|
16
|
+
if (panel.kind !== "stat") throw new Error(`expected stat panel on ${screenId}: ${panel.kind}`);
|
|
17
|
+
return panel.query;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
21
20
|
|
|
22
21
|
describe("overview screens boot", () => {
|
|
23
|
-
test("tenant-overview screen is access.admin", () => {
|
|
24
|
-
const
|
|
25
|
-
const screen = registry.getScreen(`admin-shell:screen:${TENANT_OVERVIEW_SCREEN_ID}`);
|
|
22
|
+
test("tenant-overview screen is access.admin, dashboard with 3 stat panels", () => {
|
|
23
|
+
const screen = adminShell.screens[TENANT_OVERVIEW_SCREEN_ID];
|
|
26
24
|
expect(screen?.access).toEqual({ roles: access.admin });
|
|
27
|
-
expect(screen?.type).toBe("
|
|
25
|
+
expect(screen?.type).toBe("dashboard");
|
|
26
|
+
expect(statPanelQueries(TENANT_OVERVIEW_SCREEN_ID)).toEqual([
|
|
27
|
+
TenantQueries.invitations,
|
|
28
|
+
TenantQueries.members,
|
|
29
|
+
ConfigQueries.readiness,
|
|
30
|
+
]);
|
|
28
31
|
});
|
|
29
32
|
|
|
30
|
-
test("platform-overview screen is SystemAdmin-only", () => {
|
|
31
|
-
const
|
|
32
|
-
const screen = registry.getScreen(`admin-shell:screen:${PLATFORM_OVERVIEW_SCREEN_ID}`);
|
|
33
|
+
test("platform-overview screen is SystemAdmin-only, dashboard with 3 stat panels", () => {
|
|
34
|
+
const screen = adminShell.screens[PLATFORM_OVERVIEW_SCREEN_ID];
|
|
33
35
|
expect(screen?.access).toEqual({ roles: access.systemAdmin });
|
|
36
|
+
expect(screen?.type).toBe("dashboard");
|
|
37
|
+
expect(statPanelQueries(PLATFORM_OVERVIEW_SCREEN_ID)).toEqual([
|
|
38
|
+
TenantQueries.list,
|
|
39
|
+
UserQueries.list,
|
|
40
|
+
JobQueries.list,
|
|
41
|
+
]);
|
|
34
42
|
});
|
|
35
43
|
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.241.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "tenant-overview/platform-overview dashboards no longer show \"undefined\" tiles; both converted to declarative dashboard screens (fw#2312).",
|
|
6
|
+
"detail": "Both overview screens sent `totalCount: true` to `tenant:query:list`/`jobs:query:list`, but neither handler's Zod schema declared the field, so it was stripped before the handler saw it and the tile rendered the literal string \"undefined\". Both handlers now accept `totalCount: boolean` and return `total` when set — `jobs:query:list`'s total is a real count (via `countWhere`), not `rows.length`, so it isn't capped by `limit`. `config:query:readiness` gains `missingCount`/`missingTone` alongside the existing `missing` array. `PlatformOverviewScreen`/`TenantOverviewScreen` (never public exports) and their `overview-layout`/`overview-query` support modules are gone — both screens are now `type: \"dashboard\"` (`kind: \"stat\"` panels) rendered by the generic renderer, using the new `DashboardStatPanel.params` (see framework changelog) to pin the failed-jobs tile's `status: \"failed\"` filter server-side. The `overview-allowlist` exports (`isOverviewQueryAllowed`, `overviewAllowedQueries`, the allow/forbidden-list constants) are unchanged and still importable — they're now checked against the screen definitions in tests instead of gating a deleted client-side dispatch call."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.125.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
defineFeature,
|
|
8
8
|
type FeatureDefinition,
|
|
9
9
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
10
|
+
import { ConfigQueries } from "../config";
|
|
11
|
+
import { JobQueries } from "../jobs";
|
|
12
|
+
import { TenantQueries } from "../tenant";
|
|
13
|
+
import { UserQueries } from "../user";
|
|
10
14
|
import {
|
|
11
15
|
ADMIN_SHELL_FEATURE,
|
|
12
16
|
DEFAULT_PLATFORM_WORKSPACE_ID,
|
|
@@ -71,14 +75,40 @@ export function createAdminShellFeature(options: CreateAdminShellOptions = {}):
|
|
|
71
75
|
...(includeTierAdmin ? (["admin-shell:nav:tier-admin"] as const) : []),
|
|
72
76
|
];
|
|
73
77
|
|
|
74
|
-
// kumiko-lint-ignore app-feature-structure Phase-3 conversion tracked in #2312
|
|
75
78
|
r.screen({
|
|
76
79
|
id: TENANT_OVERVIEW_SCREEN_ID,
|
|
77
|
-
type: "
|
|
78
|
-
renderer: { react: { __component: "TenantOverviewScreen" } },
|
|
80
|
+
type: "dashboard",
|
|
79
81
|
access: { roles: access.admin },
|
|
80
82
|
description:
|
|
81
83
|
"Landing page of the tenant-admin workspace, showing pending invitation, member and missing-config counts for the caller's own tenant so an operator sees what needs attention there.",
|
|
84
|
+
panels: [
|
|
85
|
+
{
|
|
86
|
+
kind: "stat",
|
|
87
|
+
id: "pending-invitations",
|
|
88
|
+
label: "admin-shell:overview.pendingInvitations",
|
|
89
|
+
query: TenantQueries.invitations,
|
|
90
|
+
// tenant:query:invitations returns a bare array, not a paged
|
|
91
|
+
// envelope — "length" reads record["length"], i.e. the array's
|
|
92
|
+
// own .length. Not a typo.
|
|
93
|
+
valueField: "length",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
kind: "stat",
|
|
97
|
+
id: "members",
|
|
98
|
+
label: "admin-shell:overview.members",
|
|
99
|
+
query: TenantQueries.members,
|
|
100
|
+
// Same array-shaped response as invitations above.
|
|
101
|
+
valueField: "length",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
kind: "stat",
|
|
105
|
+
id: "missing-config",
|
|
106
|
+
label: "admin-shell:overview.missingConfig",
|
|
107
|
+
query: ConfigQueries.readiness,
|
|
108
|
+
valueField: "missingCount",
|
|
109
|
+
toneField: "missingTone",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
82
112
|
});
|
|
83
113
|
r.nav({
|
|
84
114
|
id: "tenant-overview",
|
|
@@ -88,14 +118,38 @@ export function createAdminShellFeature(options: CreateAdminShellOptions = {}):
|
|
|
88
118
|
order: 1,
|
|
89
119
|
});
|
|
90
120
|
|
|
91
|
-
// kumiko-lint-ignore app-feature-structure Phase-3 conversion tracked in #2312
|
|
92
121
|
r.screen({
|
|
93
122
|
id: PLATFORM_OVERVIEW_SCREEN_ID,
|
|
94
|
-
type: "
|
|
95
|
-
renderer: { react: { __component: "PlatformOverviewScreen" } },
|
|
123
|
+
type: "dashboard",
|
|
96
124
|
access: { roles: access.systemAdmin },
|
|
97
125
|
description:
|
|
98
126
|
"Landing page of the platform-admin workspace, showing installation-wide tenant, user and failed-job counts so a system admin sees the health of the whole deployment at a glance.",
|
|
127
|
+
panels: [
|
|
128
|
+
{
|
|
129
|
+
kind: "stat",
|
|
130
|
+
id: "tenants",
|
|
131
|
+
label: "admin-shell:overview.tenants",
|
|
132
|
+
query: TenantQueries.list,
|
|
133
|
+
params: { totalCount: true },
|
|
134
|
+
valueField: "total",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
kind: "stat",
|
|
138
|
+
id: "users",
|
|
139
|
+
label: "admin-shell:overview.users",
|
|
140
|
+
query: UserQueries.list,
|
|
141
|
+
params: { totalCount: true },
|
|
142
|
+
valueField: "total",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
kind: "stat",
|
|
146
|
+
id: "failed-jobs",
|
|
147
|
+
label: "admin-shell:overview.failedJobs",
|
|
148
|
+
query: JobQueries.list,
|
|
149
|
+
params: { status: "failed", totalCount: true },
|
|
150
|
+
valueField: "total",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
99
153
|
});
|
|
100
154
|
r.nav({
|
|
101
155
|
id: "platform-overview",
|
package/src/admin-shell/i18n.ts
CHANGED
|
@@ -15,9 +15,6 @@ export const ADMIN_SHELL_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
|
15
15
|
"admin-shell:nav.tierAdmin": { en: "Assign tier" },
|
|
16
16
|
"admin-shell:nav.myCaps": { en: "Plans & Caps" },
|
|
17
17
|
"admin-shell:nav.tenantCaps": { en: "Plans & Caps" },
|
|
18
|
-
"admin-shell:overview.tenantTitle": { en: "Administration" },
|
|
19
|
-
"admin-shell:overview.platformTitle": { en: "Platform" },
|
|
20
|
-
"admin-shell:overview.loading": { en: "Loading…" },
|
|
21
18
|
"admin-shell:overview.pendingInvitations": {
|
|
22
19
|
en: "Pending invitations",
|
|
23
20
|
},
|
|
@@ -25,11 +22,7 @@ export const ADMIN_SHELL_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
|
25
22
|
"admin-shell:overview.missingConfig": {
|
|
26
23
|
en: "Missing configuration",
|
|
27
24
|
},
|
|
28
|
-
"admin-shell:overview.missingConfigHint": {
|
|
29
|
-
en: "Check required settings",
|
|
30
|
-
},
|
|
31
25
|
"admin-shell:overview.tenants": { en: "Tenants" },
|
|
32
26
|
"admin-shell:overview.users": { en: "Users" },
|
|
33
27
|
"admin-shell:overview.failedJobs": { en: "Failed jobs" },
|
|
34
|
-
"admin-shell:overview.failedJobsHint": { en: "Review job runs" },
|
|
35
28
|
};
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
// @runtime client
|
|
2
2
|
import { mergeTranslations, type TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
3
3
|
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
4
|
-
import {
|
|
5
|
-
ADMIN_SHELL_FEATURE,
|
|
6
|
-
PLATFORM_OVERVIEW_SCREEN_ID,
|
|
7
|
-
TENANT_OVERVIEW_SCREEN_ID,
|
|
8
|
-
} from "../constants";
|
|
4
|
+
import { ADMIN_SHELL_FEATURE } from "../constants";
|
|
9
5
|
import { defaultTranslations } from "./i18n";
|
|
10
|
-
import { PlatformOverviewScreen } from "./platform-overview-screen";
|
|
11
|
-
import { TenantOverviewScreen } from "./tenant-overview-screen";
|
|
12
6
|
|
|
13
7
|
export type AdminShellClientOptions = {
|
|
14
8
|
readonly translations?: TranslationsByLocale;
|
|
@@ -18,9 +12,5 @@ export function adminShellClient(options?: AdminShellClientOptions): ClientFeatu
|
|
|
18
12
|
return {
|
|
19
13
|
name: ADMIN_SHELL_FEATURE,
|
|
20
14
|
translations: mergeTranslations(defaultTranslations, options?.translations ?? {}),
|
|
21
|
-
components: {
|
|
22
|
-
[TENANT_OVERVIEW_SCREEN_ID]: TenantOverviewScreen,
|
|
23
|
-
[PLATFORM_OVERVIEW_SCREEN_ID]: PlatformOverviewScreen,
|
|
24
|
-
},
|
|
25
15
|
};
|
|
26
16
|
}
|
|
@@ -128,6 +128,46 @@ describe("findAgentDocGaps", () => {
|
|
|
128
128
|
expect(findAgentDocGaps([feature])).toHaveLength(0);
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
+
test("R2: custom screen with agent.expose:false opt-out -> no gap", () => {
|
|
132
|
+
const feature = defineFeature("doc-gap-demo", (r) => {
|
|
133
|
+
r.screen({
|
|
134
|
+
id: "sysadmin-secrets",
|
|
135
|
+
type: "custom",
|
|
136
|
+
renderer: { react: "stub" },
|
|
137
|
+
agent: { expose: false },
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(findAgentDocGaps([feature])).toHaveLength(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("R2: custom screen with agent.expose:true but no description -> still a gap", () => {
|
|
145
|
+
const feature = defineFeature("doc-gap-demo", (r) => {
|
|
146
|
+
r.screen({
|
|
147
|
+
id: "widget-editor",
|
|
148
|
+
type: "custom",
|
|
149
|
+
renderer: { react: "stub" },
|
|
150
|
+
agent: { expose: true },
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const gaps = findAgentDocGaps([feature]);
|
|
155
|
+
expect(gaps).toHaveLength(1);
|
|
156
|
+
expect(gaps[0]?.qn).toBe("doc-gap-demo:screen:widget-editor");
|
|
157
|
+
expect(gaps[0]?.kind).toBe(AgentDocGapKinds.customScreenWithoutDescription);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("R2: the custom-screen gap message names both ways out", () => {
|
|
161
|
+
const feature = defineFeature("doc-gap-demo", (r) => {
|
|
162
|
+
r.screen({ id: "widget-editor", type: "custom", renderer: { react: "stub" } });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const gaps = findAgentDocGaps([feature]);
|
|
166
|
+
expect(gaps).toHaveLength(1);
|
|
167
|
+
expect(formatAgentDocGap(gaps[0]!)).toContain("agent: { expose: false }");
|
|
168
|
+
expect(formatAgentDocGap(gaps[0]!)).toContain("`description`");
|
|
169
|
+
});
|
|
170
|
+
|
|
131
171
|
test("R2: non-custom screen without description -> no gap (only custom screens are linted)", () => {
|
|
132
172
|
const feature = defineFeature("doc-gap-demo", (r) => {
|
|
133
173
|
r.entity("widget", { fields: {}, description: "A widget." });
|
|
@@ -183,4 +183,40 @@ describe("buildAgentManifest", () => {
|
|
|
183
183
|
const entityNames = manifestA.entities.map((e) => e.name);
|
|
184
184
|
expect(entityNames).toEqual([...entityNames].sort());
|
|
185
185
|
});
|
|
186
|
+
|
|
187
|
+
test("a nav pointing at an opted-out screen is dropped too, so the screen id never leaks via navs", () => {
|
|
188
|
+
const feature = defineFeature("nav-leak-test", (r) => {
|
|
189
|
+
r.screen({
|
|
190
|
+
id: "sysadmin-secrets",
|
|
191
|
+
type: "custom",
|
|
192
|
+
renderer: { react: "stub" },
|
|
193
|
+
description: "Webhook secrets.",
|
|
194
|
+
agent: { expose: false },
|
|
195
|
+
});
|
|
196
|
+
r.screen({
|
|
197
|
+
id: "public-board",
|
|
198
|
+
type: "custom",
|
|
199
|
+
renderer: { react: "stub" },
|
|
200
|
+
description: "Public board.",
|
|
201
|
+
});
|
|
202
|
+
r.nav({
|
|
203
|
+
id: "sysadmin-secrets",
|
|
204
|
+
label: "nav-leak-test:nav:sysadmin-secrets",
|
|
205
|
+
screen: "nav-leak-test:screen:sysadmin-secrets",
|
|
206
|
+
});
|
|
207
|
+
r.nav({
|
|
208
|
+
id: "public-board",
|
|
209
|
+
label: "nav-leak-test:nav:public-board",
|
|
210
|
+
screen: "nav-leak-test:screen:public-board",
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const manifest = buildAgentManifest(createRegistry([feature]), {
|
|
215
|
+
locale: "en",
|
|
216
|
+
roles: ["admin"],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(manifest.navs.map((n) => n.screen)).toEqual(["nav-leak-test:screen:public-board"]);
|
|
220
|
+
expect(manifest.screens.map((s) => s.id)).toEqual(["nav-leak-test:screen:public-board"]);
|
|
221
|
+
});
|
|
186
222
|
});
|
|
@@ -303,6 +303,57 @@ describe("buildToolCatalog — client tools", () => {
|
|
|
303
303
|
expect(descriptor.screenIds.has("catalog-test:screen:widget-approve-form")).toBe(true);
|
|
304
304
|
});
|
|
305
305
|
|
|
306
|
+
test("a screen with agent.expose:false is absent from the navigate enum and from the manifest", () => {
|
|
307
|
+
const feature = defineFeature("exposure-test", (r) => {
|
|
308
|
+
r.screen({
|
|
309
|
+
id: "sysadmin-secrets",
|
|
310
|
+
type: "custom",
|
|
311
|
+
renderer: { react: "stub" },
|
|
312
|
+
description: "Webhook secrets.",
|
|
313
|
+
agent: { expose: false },
|
|
314
|
+
});
|
|
315
|
+
r.screen({
|
|
316
|
+
id: "public-board",
|
|
317
|
+
type: "custom",
|
|
318
|
+
renderer: { react: "stub" },
|
|
319
|
+
description: "Public board.",
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
const registry = createRegistry([feature]);
|
|
323
|
+
const manifest = buildAgentManifest(registry, { locale: "en", roles: ["Admin"] });
|
|
324
|
+
const catalog = buildToolCatalog(registry, manifest, { mode: "edit" });
|
|
325
|
+
|
|
326
|
+
expect(manifest.screens.map((s) => s.id)).toContain("exposure-test:screen:public-board");
|
|
327
|
+
expect(manifest.screens.map((s) => s.id)).not.toContain(
|
|
328
|
+
"exposure-test:screen:sysadmin-secrets",
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const descriptor = catalog.dispatchTable.get("navigate");
|
|
332
|
+
if (descriptor?.kind !== "client" || descriptor.op !== "navigate")
|
|
333
|
+
throw new Error("wrong kind");
|
|
334
|
+
expect(descriptor.screenIds.has("exposure-test:screen:sysadmin-secrets")).toBe(false);
|
|
335
|
+
expect(descriptor.screenIds.has("exposure-test:screen:public-board")).toBe(true);
|
|
336
|
+
|
|
337
|
+
const navTool = catalog.tools.find((t) => t.name === "navigate");
|
|
338
|
+
const schema = navTool?.inputSchema ?? {};
|
|
339
|
+
const properties = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
340
|
+
const screenIdSchema = isRecord(properties["screenId"]) ? properties["screenId"] : {};
|
|
341
|
+
const enumValues = Array.isArray(screenIdSchema["enum"]) ? screenIdSchema["enum"] : [];
|
|
342
|
+
expect(enumValues).toContain("exposure-test:screen:public-board");
|
|
343
|
+
expect(enumValues).not.toContain("exposure-test:screen:sysadmin-secrets");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("a screen without an agent slot stays in the navigate enum", () => {
|
|
347
|
+
const catalog = buildCatalog(ADMIN);
|
|
348
|
+
const descriptor = catalog.dispatchTable.get("navigate");
|
|
349
|
+
if (descriptor?.kind !== "client" || descriptor.op !== "navigate")
|
|
350
|
+
throw new Error("wrong kind");
|
|
351
|
+
// None of catalog-test's screens carry a description either — this is the
|
|
352
|
+
// regression guard against a screen without an `agent` slot silently
|
|
353
|
+
// fail-closing (the way a handler without a description would).
|
|
354
|
+
expect(descriptor.screenIds.has("catalog-test:screen:widget-detail")).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
|
|
306
357
|
test("open_form maps the actionForm handler and the entityEdit create/update handlers", () => {
|
|
307
358
|
const catalog = buildCatalog(ADMIN);
|
|
308
359
|
const descriptor = catalog.dispatchTable.get("open_form");
|
|
@@ -3,7 +3,13 @@ import type {
|
|
|
3
3
|
QueryHandlerDef,
|
|
4
4
|
WriteHandlerDef,
|
|
5
5
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
isAgentVisibleScreen,
|
|
8
|
+
QnTypes,
|
|
9
|
+
qn,
|
|
10
|
+
resolveAgentExposure,
|
|
11
|
+
toKebab,
|
|
12
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
13
|
|
|
8
14
|
export const AgentDocGapKinds = {
|
|
9
15
|
handlerWithoutDescription: "handler-without-description",
|
|
@@ -56,13 +62,18 @@ function handlerDocGaps(
|
|
|
56
62
|
function screenDocGaps(feature: FeatureDefinition): readonly AgentDocGap[] {
|
|
57
63
|
const gaps: AgentDocGap[] = [];
|
|
58
64
|
for (const [shortId, screen] of Object.entries(feature.screens ?? {})) {
|
|
59
|
-
if (
|
|
65
|
+
if (
|
|
66
|
+
screen.type !== CUSTOM_SCREEN_TYPE ||
|
|
67
|
+
screen.description !== undefined ||
|
|
68
|
+
!isAgentVisibleScreen(screen)
|
|
69
|
+
)
|
|
70
|
+
continue;
|
|
60
71
|
gaps.push({
|
|
61
72
|
qn: qn(toKebab(feature.name), QnTypes.screen, toKebab(shortId)),
|
|
62
73
|
feature: feature.name,
|
|
63
74
|
kind: AgentDocGapKinds.customScreenWithoutDescription,
|
|
64
75
|
message:
|
|
65
|
-
"This custom screen has no description, so the AI agent can't tell what it's for — set `description` to explain it.",
|
|
76
|
+
"This custom screen has no description, so the AI agent can't tell what it's for — set `description` to explain it, or `agent: { expose: false }` to hide it from the agent deliberately.",
|
|
66
77
|
});
|
|
67
78
|
}
|
|
68
79
|
return gaps;
|
|
@@ -10,7 +10,11 @@ import type {
|
|
|
10
10
|
WorkspaceDefinition,
|
|
11
11
|
WriteHandlerDef,
|
|
12
12
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
hasAccess,
|
|
15
|
+
isAgentVisibleScreen,
|
|
16
|
+
resolveAgentExposure,
|
|
17
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
14
18
|
import { z } from "zod";
|
|
15
19
|
import type {
|
|
16
20
|
AgentManifest,
|
|
@@ -204,10 +208,15 @@ function buildNavs(
|
|
|
204
208
|
workspaces: ReadonlyMap<string, WorkspaceDefinition>,
|
|
205
209
|
translations: TranslationKeys,
|
|
206
210
|
roles: readonly string[],
|
|
211
|
+
screens: ReadonlyMap<string, ScreenDefinition>,
|
|
207
212
|
): readonly AgentManifestNav[] {
|
|
208
213
|
const result: AgentManifestNav[] = [];
|
|
209
214
|
for (const [, nav] of navs) {
|
|
210
215
|
if (!uiVisible(nav.access, roles)) continue;
|
|
216
|
+
// A nav pointing at an opted-out screen would leak its id and label back
|
|
217
|
+
// into the manifest the screen was just removed from.
|
|
218
|
+
const target = nav.screen !== undefined ? screens.get(nav.screen) : undefined;
|
|
219
|
+
if (target !== undefined && !isAgentVisibleScreen(target)) continue;
|
|
211
220
|
const visibleWorkspaces = visibleWorkspaceIds(nav.workspaces, workspaces, roles);
|
|
212
221
|
const hasWorkspaces = nav.workspaces !== undefined && nav.workspaces.length > 0;
|
|
213
222
|
if (hasWorkspaces && visibleWorkspaces.length === 0) continue;
|
|
@@ -239,6 +248,9 @@ function buildScreens(
|
|
|
239
248
|
const result: AgentManifestScreen[] = [];
|
|
240
249
|
|
|
241
250
|
for (const [, screen] of screens) {
|
|
251
|
+
// An opted-out screen must not reach the manifest at all — the tool catalog
|
|
252
|
+
// builds `navigate`'s screen-id enum straight from `manifest.screens`.
|
|
253
|
+
if (!isAgentVisibleScreen(screen)) continue;
|
|
242
254
|
const matchingNavs = allNavs.filter((nav) => nav.screen === screen.id);
|
|
243
255
|
const accessibleNavs = matchingNavs.filter((nav) => uiVisible(nav.access, roles));
|
|
244
256
|
|
|
@@ -312,8 +324,9 @@ export function buildAgentManifest(
|
|
|
312
324
|
roles,
|
|
313
325
|
(qn) => registry.getHandlerEntity(qn),
|
|
314
326
|
);
|
|
315
|
-
const
|
|
316
|
-
const
|
|
327
|
+
const screenMap = registry.getAllScreens();
|
|
328
|
+
const navs = buildNavs(navMap, workspaceMap, translations, roles, screenMap);
|
|
329
|
+
const screens = buildScreens(screenMap, navMap, workspaceMap, translations, roles);
|
|
317
330
|
const workspaces = buildWorkspaces(workspaceMap, translations, roles);
|
|
318
331
|
|
|
319
332
|
return {
|
|
@@ -6,7 +6,7 @@ export const AGENT_TOOLS_FEATURE_NAME = "agent-tools";
|
|
|
6
6
|
export function createAgentToolsFeature(): FeatureDefinition {
|
|
7
7
|
return defineFeature(AGENT_TOOLS_FEATURE_NAME, (r) => {
|
|
8
8
|
r.describe(
|
|
9
|
-
"Builds a tool catalog and an agent manifest from the mounted registry so an LLM agent can call handlers and understand the app's shape. A handler
|
|
9
|
+
"Builds a tool catalog and an agent manifest from the mounted registry so an LLM agent can call handlers and understand the app's shape. A handler or entity without a `description` stays invisible to the agent by construction (see `resolveAgentExposure`), while a screen stays visible unless it opts out with `agent: { expose: false }` (see `isAgentVisibleScreen`) — this feature surfaces the resulting gaps at boot and via `kumiko agent lint` so an app author notices before an agent silently can't see a feature.",
|
|
10
10
|
);
|
|
11
11
|
r.uiHints({
|
|
12
12
|
displayLabel: "AI Agent Tools",
|