@agent-native/creative-context 0.7.25 → 0.8.0-nightly-20260915020720
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/dist/client/actions.d.ts +3 -1
- package/dist/client/actions.d.ts.map +1 -1
- package/dist/client/actions.js +2 -2
- package/dist/client/actions.js.map +1 -1
- package/dist/client/agent-tab.d.ts.map +1 -1
- package/dist/client/agent-tab.js +5 -7
- package/dist/client/agent-tab.js.map +1 -1
- package/dist/client/application-state.d.ts +3 -1
- package/dist/client/application-state.d.ts.map +1 -1
- package/dist/client/application-state.js +9 -2
- package/dist/client/application-state.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/useCreativeContextLab.d.ts +8 -0
- package/dist/client/useCreativeContextLab.d.ts.map +1 -0
- package/dist/client/useCreativeContextLab.js +9 -0
- package/dist/client/useCreativeContextLab.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/labs.d.ts +2 -0
- package/dist/labs.d.ts.map +1 -0
- package/dist/labs.js +6 -0
- package/dist/labs.js.map +1 -0
- package/dist/server/context.d.ts +1 -0
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js +2 -0
- package/dist/server/context.js.map +1 -1
- package/dist/server/generation-context.d.ts +1 -1
- package/dist/server/generation-context.d.ts.map +1 -1
- package/dist/server/generation-context.js +35 -8
- package/dist/server/generation-context.js.map +1 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +5 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/labs.d.ts +5 -0
- package/dist/server/labs.d.ts.map +1 -0
- package/dist/server/labs.js +33 -0
- package/dist/server/labs.js.map +1 -0
- package/dist/server/media.d.ts.map +1 -1
- package/dist/server/media.js +4 -0
- package/dist/server/media.js.map +1 -1
- package/dist/server/native-context-clone.d.ts +7 -0
- package/dist/server/native-context-clone.d.ts.map +1 -0
- package/dist/server/native-context-clone.js +7 -0
- package/dist/server/native-context-clone.js.map +1 -0
- package/dist/server/prompt-provider.d.ts.map +1 -1
- package/dist/server/prompt-provider.js +8 -2
- package/dist/server/prompt-provider.js.map +1 -1
- package/package.json +5 -5
- package/src/client/actions.ts +5 -1
- package/src/client/agent-tab.tsx +6 -7
- package/src/client/application-state.spec.ts +55 -2
- package/src/client/application-state.ts +10 -2
- package/src/client/index.ts +4 -0
- package/src/client/useCreativeContextLab.ts +11 -0
- package/src/index.ts +1 -0
- package/src/labs.ts +6 -0
- package/src/server/context.ts +3 -0
- package/src/server/generation-artifact-record-role.spec.ts +6 -0
- package/src/server/generation-context-a2a.spec.ts +37 -0
- package/src/server/generation-context-off.spec.ts +71 -0
- package/src/server/generation-context.ts +40 -14
- package/src/server/index.ts +9 -2
- package/src/server/labs.spec.ts +116 -0
- package/src/server/labs.ts +53 -0
- package/src/server/media.spec.ts +56 -1
- package/src/server/media.ts +9 -0
- package/src/server/native-context-clone.spec.ts +55 -0
- package/src/server/native-context-clone.ts +9 -0
- package/src/server/prompt-provider.spec.ts +55 -0
- package/src/server/prompt-provider.ts +13 -2
|
@@ -15,7 +15,9 @@ const mocks = vi.hoisted(() => ({
|
|
|
15
15
|
hasA2A: vi.fn(),
|
|
16
16
|
callA2A: vi.fn(),
|
|
17
17
|
getRequestOrgId: vi.fn(),
|
|
18
|
+
getRequestUserEmail: vi.fn(() => "user@example.test"),
|
|
18
19
|
createArtifactCapability: vi.fn(),
|
|
20
|
+
isCreativeContextLabAvailable: vi.fn(),
|
|
19
21
|
}));
|
|
20
22
|
|
|
21
23
|
vi.mock("@agent-native/core/application-state", () => ({
|
|
@@ -24,6 +26,7 @@ vi.mock("@agent-native/core/application-state", () => ({
|
|
|
24
26
|
|
|
25
27
|
vi.mock("@agent-native/core/server/request-context", () => ({
|
|
26
28
|
getRequestOrgId: mocks.getRequestOrgId,
|
|
29
|
+
getRequestUserEmail: mocks.getRequestUserEmail,
|
|
27
30
|
}));
|
|
28
31
|
|
|
29
32
|
vi.mock("../store/index.js", () => ({
|
|
@@ -60,6 +63,14 @@ vi.mock("./isolated-a2a.js", () => ({
|
|
|
60
63
|
})),
|
|
61
64
|
}));
|
|
62
65
|
|
|
66
|
+
vi.mock("./labs.js", () => ({
|
|
67
|
+
isCreativeContextLabAvailable: mocks.isCreativeContextLabAvailable,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
vi.mock("./context.js", () => ({
|
|
71
|
+
getCreativeContext: () => ({ labKey: "creative-context.library" }),
|
|
72
|
+
}));
|
|
73
|
+
|
|
63
74
|
vi.mock("./generation-artifact-access.js", () => ({
|
|
64
75
|
assertGenerationArtifactAccess: vi.fn(),
|
|
65
76
|
createGenerationArtifactAccessCapability: mocks.createArtifactCapability,
|
|
@@ -88,6 +99,7 @@ describe("generation context isolated A2A routing", () => {
|
|
|
88
99
|
mocks.createArtifactCapability.mockImplementation(
|
|
89
100
|
async (_identity, _target, operation) => `cap-${operation}`,
|
|
90
101
|
);
|
|
102
|
+
mocks.isCreativeContextLabAvailable.mockResolvedValue(true);
|
|
91
103
|
mocks.callA2A.mockResolvedValue(emptyRemoteContext);
|
|
92
104
|
mocks.listCreativeContexts.mockResolvedValue({ contexts: [] });
|
|
93
105
|
mocks.getCreativeContextById.mockResolvedValue(null);
|
|
@@ -147,6 +159,31 @@ describe("generation context isolated A2A routing", () => {
|
|
|
147
159
|
expect(mocks.getLocal).not.toHaveBeenCalled();
|
|
148
160
|
});
|
|
149
161
|
|
|
162
|
+
it.each([
|
|
163
|
+
{ path: "isolated A2A", hasA2A: true },
|
|
164
|
+
{ path: "local storage", hasA2A: false },
|
|
165
|
+
])(
|
|
166
|
+
"does not record through $path when Labs is disabled",
|
|
167
|
+
async ({ hasA2A }) => {
|
|
168
|
+
mocks.isCreativeContextLabAvailable.mockResolvedValue(false);
|
|
169
|
+
mocks.hasA2A.mockReturnValue(hasA2A);
|
|
170
|
+
|
|
171
|
+
await expect(
|
|
172
|
+
recordGenerationCreativeContext({
|
|
173
|
+
appId: "slides",
|
|
174
|
+
artifactType: "deck",
|
|
175
|
+
artifactId: "deck-1",
|
|
176
|
+
contextMode: "auto",
|
|
177
|
+
contextPackId: null,
|
|
178
|
+
reuseLabels: [],
|
|
179
|
+
}),
|
|
180
|
+
).resolves.toBeNull();
|
|
181
|
+
|
|
182
|
+
expect(mocks.callA2A).not.toHaveBeenCalled();
|
|
183
|
+
expect(mocks.recordLocal).not.toHaveBeenCalled();
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
150
187
|
it("keeps the in-process path as the zero-configuration default", async () => {
|
|
151
188
|
mocks.hasA2A.mockReturnValue(false);
|
|
152
189
|
mocks.performCreativeContextSearch.mockResolvedValue({
|
|
@@ -7,6 +7,8 @@ const mocks = vi.hoisted(() => ({
|
|
|
7
7
|
listAccessibleSearchDocuments: vi.fn(),
|
|
8
8
|
performCreativeContextSearch: vi.fn(),
|
|
9
9
|
callIsolatedCreativeContextA2A: vi.fn(),
|
|
10
|
+
isCreativeContextLabAvailable: vi.fn(),
|
|
11
|
+
getGenerationCreativeContextLocal: vi.fn(),
|
|
10
12
|
}));
|
|
11
13
|
|
|
12
14
|
vi.mock("@agent-native/core/application-state", () => ({
|
|
@@ -19,6 +21,11 @@ vi.mock("../store/index.js", () => ({
|
|
|
19
21
|
listAccessibleSearchDocuments: mocks.listAccessibleSearchDocuments,
|
|
20
22
|
}));
|
|
21
23
|
|
|
24
|
+
vi.mock("../store/generation.js", () => ({
|
|
25
|
+
getGenerationCreativeContext: mocks.getGenerationCreativeContextLocal,
|
|
26
|
+
recordGenerationCreativeContext: vi.fn(),
|
|
27
|
+
}));
|
|
28
|
+
|
|
22
29
|
vi.mock("./retrieval.js", () => ({
|
|
23
30
|
performCreativeContextSearch: mocks.performCreativeContextSearch,
|
|
24
31
|
}));
|
|
@@ -29,7 +36,16 @@ vi.mock("./isolated-a2a.js", () => ({
|
|
|
29
36
|
isolatedResolvePayload: vi.fn((input) => input),
|
|
30
37
|
}));
|
|
31
38
|
|
|
39
|
+
vi.mock("./labs.js", () => ({
|
|
40
|
+
isCreativeContextLabAvailable: mocks.isCreativeContextLabAvailable,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock("./context.js", () => ({
|
|
44
|
+
getCreativeContext: () => ({ labKey: "creative-context.library" }),
|
|
45
|
+
}));
|
|
46
|
+
|
|
32
47
|
import {
|
|
48
|
+
getGenerationCreativeContext,
|
|
33
49
|
resolveGenerationCreativeContext,
|
|
34
50
|
validateGenerationCreativeContext,
|
|
35
51
|
} from "./generation-context.js";
|
|
@@ -37,6 +53,7 @@ import {
|
|
|
37
53
|
describe("creative context structural opt-out", () => {
|
|
38
54
|
beforeEach(() => {
|
|
39
55
|
vi.clearAllMocks();
|
|
56
|
+
mocks.isCreativeContextLabAvailable.mockResolvedValue(true);
|
|
40
57
|
mocks.readAppState.mockResolvedValue({ contextMode: "off" });
|
|
41
58
|
});
|
|
42
59
|
|
|
@@ -52,6 +69,60 @@ describe("creative context structural opt-out", () => {
|
|
|
52
69
|
expect(mocks.callIsolatedCreativeContextA2A).not.toHaveBeenCalled();
|
|
53
70
|
});
|
|
54
71
|
|
|
72
|
+
it("does not resolve automatic context while its Lab is disabled", async () => {
|
|
73
|
+
mocks.isCreativeContextLabAvailable.mockResolvedValue(false);
|
|
74
|
+
|
|
75
|
+
await expect(
|
|
76
|
+
resolveGenerationCreativeContext({ role: "design", query: "dashboard" }),
|
|
77
|
+
).resolves.toMatchObject({ contextMode: "off", results: [] });
|
|
78
|
+
expect(mocks.readAppState).not.toHaveBeenCalled();
|
|
79
|
+
expect(mocks.callIsolatedCreativeContextA2A).not.toHaveBeenCalled();
|
|
80
|
+
expect(mocks.performCreativeContextSearch).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not read inherited generation context while its Lab is disabled", async () => {
|
|
84
|
+
mocks.isCreativeContextLabAvailable.mockResolvedValue(false);
|
|
85
|
+
|
|
86
|
+
await expect(
|
|
87
|
+
getGenerationCreativeContext({
|
|
88
|
+
appId: "slides",
|
|
89
|
+
artifactType: "deck",
|
|
90
|
+
artifactId: "deck-1",
|
|
91
|
+
}),
|
|
92
|
+
).resolves.toBeNull();
|
|
93
|
+
expect(mocks.readAppState).not.toHaveBeenCalled();
|
|
94
|
+
expect(mocks.callIsolatedCreativeContextA2A).not.toHaveBeenCalled();
|
|
95
|
+
expect(mocks.getGenerationCreativeContextLocal).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("does not treat an unreadable Lab as permission to read inherited context", async () => {
|
|
99
|
+
mocks.isCreativeContextLabAvailable.mockRejectedValue(
|
|
100
|
+
new Error("settings unavailable"),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
await expect(
|
|
104
|
+
getGenerationCreativeContext({
|
|
105
|
+
appId: "slides",
|
|
106
|
+
artifactType: "deck",
|
|
107
|
+
artifactId: "deck-1",
|
|
108
|
+
}),
|
|
109
|
+
).rejects.toThrow("settings unavailable");
|
|
110
|
+
expect(mocks.callIsolatedCreativeContextA2A).not.toHaveBeenCalled();
|
|
111
|
+
expect(mocks.getGenerationCreativeContextLocal).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("fails closed when Labs state cannot be read", async () => {
|
|
115
|
+
mocks.isCreativeContextLabAvailable.mockRejectedValue(
|
|
116
|
+
new Error("settings unavailable"),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
resolveGenerationCreativeContext({ role: "design", query: "dashboard" }),
|
|
121
|
+
).rejects.toThrow("settings unavailable");
|
|
122
|
+
expect(mocks.callIsolatedCreativeContextA2A).not.toHaveBeenCalled();
|
|
123
|
+
expect(mocks.performCreativeContextSearch).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
55
126
|
it("rejects an explicit pack before validating final-write provenance", async () => {
|
|
56
127
|
await expect(
|
|
57
128
|
validateGenerationCreativeContext({
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { readAppState } from "@agent-native/core/application-state";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getRequestOrgId,
|
|
4
|
+
getRequestUserEmail,
|
|
5
|
+
} from "@agent-native/core/server/request-context";
|
|
3
6
|
|
|
4
7
|
import {
|
|
5
8
|
getGenerationCreativeContext as getGenerationCreativeContextLocal,
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
sanitizeUntrustedReference,
|
|
24
27
|
UNTRUSTED_REFERENCE_ROLE,
|
|
25
28
|
} from "../untrusted-reference.js";
|
|
29
|
+
import { getCreativeContext } from "./context.js";
|
|
26
30
|
import {
|
|
27
31
|
assertGenerationArtifactAccess,
|
|
28
32
|
createGenerationArtifactAccessCapability,
|
|
@@ -35,6 +39,7 @@ import {
|
|
|
35
39
|
isolatedResolvePayload,
|
|
36
40
|
type IsolatedRecordPayload,
|
|
37
41
|
} from "./isolated-a2a.js";
|
|
42
|
+
import { isCreativeContextLabAvailable } from "./labs.js";
|
|
38
43
|
import { performCreativeContextSearch } from "./retrieval.js";
|
|
39
44
|
|
|
40
45
|
export type CreativeGenerationRole =
|
|
@@ -56,6 +61,13 @@ const SPECIALTY_STOP_WORDS = new Set([
|
|
|
56
61
|
"with",
|
|
57
62
|
]);
|
|
58
63
|
|
|
64
|
+
async function creativeContextLabEnabled() {
|
|
65
|
+
return isCreativeContextLabAvailable(
|
|
66
|
+
getRequestUserEmail(),
|
|
67
|
+
getCreativeContext().labKey,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
function specialtyTokens(value: string) {
|
|
60
72
|
return new Set(
|
|
61
73
|
value
|
|
@@ -249,16 +261,23 @@ export async function resolveGenerationCreativeContext(
|
|
|
249
261
|
if (input.contextModeOverride === "off") {
|
|
250
262
|
return resolveGenerationCreativeContextLocal(input);
|
|
251
263
|
}
|
|
264
|
+
if (!(await creativeContextLabEnabled())) {
|
|
265
|
+
return resolveGenerationCreativeContextLocal({
|
|
266
|
+
...input,
|
|
267
|
+
contextModeOverride: "off",
|
|
268
|
+
});
|
|
269
|
+
}
|
|
252
270
|
if (hasIsolatedCreativeContextA2A()) {
|
|
253
|
-
const state = (await readAppState("creative-context")
|
|
254
|
-
() => null,
|
|
255
|
-
)) as {
|
|
271
|
+
const state = (await readAppState("creative-context")) as {
|
|
256
272
|
contextMode?: "auto" | "off";
|
|
257
273
|
pinnedPackId?: string | null;
|
|
258
274
|
selectedContextId?: string | null;
|
|
259
275
|
} | null;
|
|
260
276
|
if (state?.contextMode === "off") {
|
|
261
|
-
return resolveGenerationCreativeContextLocal(
|
|
277
|
+
return resolveGenerationCreativeContextLocal({
|
|
278
|
+
...input,
|
|
279
|
+
contextModeOverride: "off",
|
|
280
|
+
});
|
|
262
281
|
}
|
|
263
282
|
return callIsolatedCreativeContextA2A("resolve", {
|
|
264
283
|
...isolatedResolvePayload(input),
|
|
@@ -295,7 +314,7 @@ export async function resolveGenerationCreativeContextLocal(
|
|
|
295
314
|
results: [],
|
|
296
315
|
};
|
|
297
316
|
}
|
|
298
|
-
const state = (await readAppState("creative-context")
|
|
317
|
+
const state = (await readAppState("creative-context")) as {
|
|
299
318
|
contextMode?: "auto" | "off";
|
|
300
319
|
pinnedPackId?: string | null;
|
|
301
320
|
selectedContextId?: string | null;
|
|
@@ -449,14 +468,21 @@ export async function validateGenerationCreativeContext(
|
|
|
449
468
|
if (input.contextModeOverride === "off") {
|
|
450
469
|
return validateGenerationCreativeContextLocal(input);
|
|
451
470
|
}
|
|
471
|
+
if (!(await creativeContextLabEnabled())) {
|
|
472
|
+
return validateGenerationCreativeContextLocal({
|
|
473
|
+
...input,
|
|
474
|
+
contextModeOverride: "off",
|
|
475
|
+
});
|
|
476
|
+
}
|
|
452
477
|
if (hasIsolatedCreativeContextA2A()) {
|
|
453
|
-
const state = (await readAppState("creative-context")
|
|
454
|
-
() => null,
|
|
455
|
-
)) as {
|
|
478
|
+
const state = (await readAppState("creative-context")) as {
|
|
456
479
|
contextMode?: "auto" | "off";
|
|
457
480
|
} | null;
|
|
458
481
|
if (state?.contextMode === "off") {
|
|
459
|
-
return validateGenerationCreativeContextLocal(
|
|
482
|
+
return validateGenerationCreativeContextLocal({
|
|
483
|
+
...input,
|
|
484
|
+
contextModeOverride: "off",
|
|
485
|
+
});
|
|
460
486
|
}
|
|
461
487
|
return callIsolatedCreativeContextA2A("validate", {
|
|
462
488
|
contextPackId: input.contextPackId,
|
|
@@ -500,7 +526,7 @@ export async function validateGenerationCreativeContextLocal(
|
|
|
500
526
|
results: [],
|
|
501
527
|
};
|
|
502
528
|
}
|
|
503
|
-
const state = (await readAppState("creative-context")
|
|
529
|
+
const state = (await readAppState("creative-context")) as {
|
|
504
530
|
contextMode?: "auto" | "off";
|
|
505
531
|
} | null;
|
|
506
532
|
if (state?.contextMode === "off") {
|
|
@@ -557,6 +583,7 @@ export async function recordGenerationCreativeContext(
|
|
|
557
583
|
input: IsolatedRecordPayload,
|
|
558
584
|
options: { db?: any; artifactAccess?: GenerationArtifactAccessTarget } = {},
|
|
559
585
|
) {
|
|
586
|
+
if (!(await creativeContextLabEnabled())) return null;
|
|
560
587
|
const artifactAccessTarget = collaborativeArtifactTarget(
|
|
561
588
|
input,
|
|
562
589
|
options.artifactAccess,
|
|
@@ -602,14 +629,13 @@ export async function getGenerationCreativeContext(
|
|
|
602
629
|
db?: any;
|
|
603
630
|
} = {},
|
|
604
631
|
) {
|
|
632
|
+
if (!(await creativeContextLabEnabled())) return null;
|
|
605
633
|
const artifactAccessTarget = collaborativeArtifactTarget(
|
|
606
634
|
input,
|
|
607
635
|
options.artifactAccess,
|
|
608
636
|
);
|
|
609
637
|
if (!options.db && hasIsolatedCreativeContextA2A()) {
|
|
610
|
-
const state = (await readAppState("creative-context")
|
|
611
|
-
() => null,
|
|
612
|
-
)) as {
|
|
638
|
+
const state = (await readAppState("creative-context")) as {
|
|
613
639
|
contextMode?: "auto" | "off";
|
|
614
640
|
} | null;
|
|
615
641
|
if (state?.contextMode !== "off") {
|
package/src/server/index.ts
CHANGED
|
@@ -23,10 +23,16 @@ import {
|
|
|
23
23
|
getCreativeContext,
|
|
24
24
|
type CreativeContextServerContext,
|
|
25
25
|
} from "./context.js";
|
|
26
|
+
import { gateCreativeContextActions } from "./labs.js";
|
|
26
27
|
import { createCreativeContextMediaPlugin } from "./media.js";
|
|
27
28
|
import { registerCreativeContextPromptProvider } from "./prompt-provider.js";
|
|
28
29
|
import { getCreativeContextResourcePath } from "./resource-paths.js";
|
|
29
30
|
|
|
31
|
+
export {
|
|
32
|
+
assertCreativeContextLabEnabled,
|
|
33
|
+
isCreativeContextLabAvailable,
|
|
34
|
+
} from "./labs.js";
|
|
35
|
+
|
|
30
36
|
export interface CreativeContextSetupOptions extends Partial<
|
|
31
37
|
Omit<CreativeContextServerContext, "connectorContext">
|
|
32
38
|
> {
|
|
@@ -35,7 +41,7 @@ export interface CreativeContextSetupOptions extends Partial<
|
|
|
35
41
|
continuationDispatcher?: CreativeContextImportContinuationDispatcher;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
registerPackageActions(creativeContextActions);
|
|
44
|
+
registerPackageActions(gateCreativeContextActions(creativeContextActions));
|
|
39
45
|
registerWorkspaceConnectionLifecycleListener(async (event) => {
|
|
40
46
|
await handleWorkspaceConnectionLifecycle(event);
|
|
41
47
|
});
|
|
@@ -103,6 +109,7 @@ export function setupCreativeContext(
|
|
|
103
109
|
configureCreativeContext({
|
|
104
110
|
getDb: options.getDb,
|
|
105
111
|
schema: options.schema,
|
|
112
|
+
labKey: options.labKey,
|
|
106
113
|
vectorAdapter: options.vectorAdapter,
|
|
107
114
|
connectors: options.connectors,
|
|
108
115
|
projections: options.projections,
|
|
@@ -155,7 +162,7 @@ export {
|
|
|
155
162
|
type RenderedDesignExtractionStatus,
|
|
156
163
|
} from "../connectors/rendered-design.js";
|
|
157
164
|
export { serializePrivateBlobHandle } from "../connectors/private-artifacts.js";
|
|
158
|
-
export { resolveNativeContextCloneReference } from "
|
|
165
|
+
export { resolveNativeContextCloneReference } from "./native-context-clone.js";
|
|
159
166
|
export {
|
|
160
167
|
CREATIVE_CONTEXT_BACKGROUND_PROCESSOR_ROUTE,
|
|
161
168
|
CREATIVE_CONTEXT_IMPORT_PROCESSOR_ROUTE,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ActionEntry } from "@agent-native/core/server";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({
|
|
5
|
+
getUserLabs: vi.fn(),
|
|
6
|
+
getRequestUserEmail: vi.fn(),
|
|
7
|
+
getCreativeContext: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("@agent-native/core/labs/server", () => mocks);
|
|
11
|
+
vi.mock("@agent-native/core/server/request-context", () => mocks);
|
|
12
|
+
vi.mock("./context.js", () => mocks);
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
assertCreativeContextLabEnabled,
|
|
16
|
+
gateCreativeContextActions,
|
|
17
|
+
isCreativeContextLabAvailable,
|
|
18
|
+
} from "./labs.js";
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
mocks.getCreativeContext.mockReturnValue({
|
|
23
|
+
labKey: "content.creative-context",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("isCreativeContextLabAvailable", () => {
|
|
28
|
+
it("does not expose the library without an authenticated user", async () => {
|
|
29
|
+
await expect(isCreativeContextLabAvailable(undefined)).resolves.toBe(false);
|
|
30
|
+
expect(mocks.getUserLabs).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("uses the shared Creative Context Lab by default", async () => {
|
|
34
|
+
mocks.getUserLabs.mockResolvedValue({ "creative-context.library": true });
|
|
35
|
+
|
|
36
|
+
await expect(
|
|
37
|
+
isCreativeContextLabAvailable("user@example.com"),
|
|
38
|
+
).resolves.toBe(true);
|
|
39
|
+
expect(mocks.getUserLabs).toHaveBeenCalledWith("user@example.com");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("supports an app's existing Creative Context Lab key", async () => {
|
|
43
|
+
mocks.getUserLabs.mockResolvedValue({ "content.creative-context": true });
|
|
44
|
+
|
|
45
|
+
await expect(
|
|
46
|
+
isCreativeContextLabAvailable(
|
|
47
|
+
"user@example.com",
|
|
48
|
+
"content.creative-context",
|
|
49
|
+
),
|
|
50
|
+
).resolves.toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("preserves unreadable lab state as an error", async () => {
|
|
54
|
+
mocks.getUserLabs.mockRejectedValue(new Error("settings unavailable"));
|
|
55
|
+
|
|
56
|
+
await expect(
|
|
57
|
+
isCreativeContextLabAvailable("user@example.com"),
|
|
58
|
+
).rejects.toThrow("settings unavailable");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("requires the configured app Lab before Creative Context operations", async () => {
|
|
62
|
+
mocks.getUserLabs.mockResolvedValue({ "content.creative-context": true });
|
|
63
|
+
await expect(
|
|
64
|
+
assertCreativeContextLabEnabled("user@example.com"),
|
|
65
|
+
).resolves.toBeUndefined();
|
|
66
|
+
expect(mocks.getUserLabs).toHaveBeenCalledWith("user@example.com");
|
|
67
|
+
expect(mocks.getCreativeContext).toHaveBeenCalled();
|
|
68
|
+
|
|
69
|
+
mocks.getUserLabs.mockResolvedValue({ "content.creative-context": false });
|
|
70
|
+
await expect(
|
|
71
|
+
assertCreativeContextLabEnabled("user@example.com"),
|
|
72
|
+
).rejects.toThrow("Creative Context is disabled in Labs");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("gates package actions with the configured app Lab", async () => {
|
|
76
|
+
const run = vi.fn().mockResolvedValue({ ok: true });
|
|
77
|
+
const actions = gateCreativeContextActions({
|
|
78
|
+
"manage-creative-context": {
|
|
79
|
+
tool: {} as ActionEntry["tool"],
|
|
80
|
+
run,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const action = actions["manage-creative-context"];
|
|
84
|
+
|
|
85
|
+
mocks.getUserLabs.mockResolvedValue({ "content.creative-context": true });
|
|
86
|
+
await expect(
|
|
87
|
+
action.run({}, { caller: "tool", userEmail: "user@example.test" }),
|
|
88
|
+
).resolves.toEqual({ ok: true });
|
|
89
|
+
expect(mocks.getUserLabs).toHaveBeenCalledWith("user@example.test");
|
|
90
|
+
expect(mocks.getCreativeContext).toHaveBeenCalled();
|
|
91
|
+
expect(run).toHaveBeenCalledOnce();
|
|
92
|
+
|
|
93
|
+
mocks.getUserLabs.mockResolvedValue({ "content.creative-context": false });
|
|
94
|
+
await expect(
|
|
95
|
+
action.run({}, { caller: "tool", userEmail: "user@example.test" }),
|
|
96
|
+
).rejects.toThrow("Creative Context is disabled in Labs");
|
|
97
|
+
expect(run).toHaveBeenCalledOnce();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("keeps approved purge cleanup runnable after disabling the Lab", async () => {
|
|
101
|
+
const run = vi.fn().mockResolvedValue({ ok: true });
|
|
102
|
+
const actions = gateCreativeContextActions({
|
|
103
|
+
"process-context-purge": {
|
|
104
|
+
tool: {} as ActionEntry["tool"],
|
|
105
|
+
run,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
mocks.getUserLabs.mockRejectedValue(new Error("settings unavailable"));
|
|
109
|
+
|
|
110
|
+
await expect(actions["process-context-purge"].run({})).resolves.toEqual({
|
|
111
|
+
ok: true,
|
|
112
|
+
});
|
|
113
|
+
expect(mocks.getUserLabs).not.toHaveBeenCalled();
|
|
114
|
+
expect(run).toHaveBeenCalledOnce();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getUserLabs } from "@agent-native/core/labs/server";
|
|
2
|
+
import type { ActionEntry } from "@agent-native/core/server";
|
|
3
|
+
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
4
|
+
|
|
5
|
+
import { CREATIVE_CONTEXT_LIBRARY_LAB } from "../labs.js";
|
|
6
|
+
import { getCreativeContext } from "./context.js";
|
|
7
|
+
|
|
8
|
+
export async function isCreativeContextLabAvailable(
|
|
9
|
+
userEmail: string | undefined,
|
|
10
|
+
labKey = CREATIVE_CONTEXT_LIBRARY_LAB.key,
|
|
11
|
+
): Promise<boolean> {
|
|
12
|
+
if (!userEmail) return false;
|
|
13
|
+
const labs = await getUserLabs(userEmail);
|
|
14
|
+
return labs[labKey] === true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function assertCreativeContextLabEnabled(
|
|
18
|
+
userEmail = getRequestUserEmail(),
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
if (
|
|
21
|
+
!(await isCreativeContextLabAvailable(
|
|
22
|
+
userEmail,
|
|
23
|
+
getCreativeContext().labKey,
|
|
24
|
+
))
|
|
25
|
+
) {
|
|
26
|
+
throw new Error("Creative Context is disabled in Labs");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function gateCreativeContextAction(action: ActionEntry): ActionEntry {
|
|
31
|
+
return {
|
|
32
|
+
...action,
|
|
33
|
+
async run(args, context) {
|
|
34
|
+
await assertCreativeContextLabEnabled(
|
|
35
|
+
context?.userEmail ?? getRequestUserEmail(),
|
|
36
|
+
);
|
|
37
|
+
return action.run(args, context);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function gateCreativeContextActions(
|
|
43
|
+
actions: Record<string, ActionEntry>,
|
|
44
|
+
): Record<string, ActionEntry> {
|
|
45
|
+
return Object.fromEntries(
|
|
46
|
+
Object.entries(actions).map(([name, action]) => [
|
|
47
|
+
name,
|
|
48
|
+
name === "process-context-purge"
|
|
49
|
+
? action
|
|
50
|
+
: gateCreativeContextAction(action),
|
|
51
|
+
]),
|
|
52
|
+
);
|
|
53
|
+
}
|
package/src/server/media.spec.ts
CHANGED
|
@@ -7,6 +7,8 @@ const mocks = vi.hoisted(() => ({
|
|
|
7
7
|
readPrivateArtifact: vi.fn(async () => new Uint8Array([1, 2, 3])),
|
|
8
8
|
getCreativeContextItem: vi.fn(),
|
|
9
9
|
readPendingCreativeContextMedia: vi.fn(),
|
|
10
|
+
isCreativeContextLabAvailable: vi.fn(),
|
|
11
|
+
labKey: "creative-context.library",
|
|
10
12
|
}));
|
|
11
13
|
|
|
12
14
|
vi.mock("@agent-native/core/server", () => ({
|
|
@@ -36,7 +38,14 @@ vi.mock("../store/index.js", () => ({
|
|
|
36
38
|
}));
|
|
37
39
|
|
|
38
40
|
vi.mock("./context.js", () => ({
|
|
39
|
-
getCreativeContext: vi.fn(() => ({
|
|
41
|
+
getCreativeContext: vi.fn(() => ({
|
|
42
|
+
connectorContext: {},
|
|
43
|
+
labKey: mocks.labKey,
|
|
44
|
+
})),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock("./labs.js", () => ({
|
|
48
|
+
isCreativeContextLabAvailable: mocks.isCreativeContextLabAvailable,
|
|
40
49
|
}));
|
|
41
50
|
|
|
42
51
|
const { createCreativeContextMediaPlugin } = await import("./media.js");
|
|
@@ -56,6 +65,9 @@ describe("creative context media route", () => {
|
|
|
56
65
|
mocks.handler = null;
|
|
57
66
|
mocks.getSession.mockReset();
|
|
58
67
|
mocks.runWithRequestContext.mockClear();
|
|
68
|
+
mocks.readPrivateArtifact.mockClear();
|
|
69
|
+
mocks.isCreativeContextLabAvailable.mockReset().mockResolvedValue(true);
|
|
70
|
+
mocks.labKey = "creative-context.library";
|
|
59
71
|
mocks.getCreativeContextItem.mockReset().mockResolvedValue({
|
|
60
72
|
item: {
|
|
61
73
|
id: "item-1",
|
|
@@ -74,6 +86,45 @@ describe("creative context media route", () => {
|
|
|
74
86
|
expect(response.status).toBe(401);
|
|
75
87
|
expect(mocks.runWithRequestContext).not.toHaveBeenCalled();
|
|
76
88
|
expect(mocks.getCreativeContextItem).not.toHaveBeenCalled();
|
|
89
|
+
expect(mocks.isCreativeContextLabAvailable).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("does not read private media when its app-specific Lab is disabled", async () => {
|
|
93
|
+
mocks.getSession.mockResolvedValue({
|
|
94
|
+
email: "Alice@Example.test ",
|
|
95
|
+
orgId: "org-1",
|
|
96
|
+
});
|
|
97
|
+
mocks.labKey = "content.creative-context";
|
|
98
|
+
mocks.isCreativeContextLabAvailable.mockResolvedValue(false);
|
|
99
|
+
|
|
100
|
+
const response = await mocks.handler!(event());
|
|
101
|
+
|
|
102
|
+
expect(response.status).toBe(404);
|
|
103
|
+
expect(mocks.isCreativeContextLabAvailable).toHaveBeenCalledWith(
|
|
104
|
+
"alice@example.test",
|
|
105
|
+
"content.creative-context",
|
|
106
|
+
);
|
|
107
|
+
expect(mocks.runWithRequestContext).not.toHaveBeenCalled();
|
|
108
|
+
expect(mocks.getCreativeContextItem).not.toHaveBeenCalled();
|
|
109
|
+
expect(mocks.readPendingCreativeContextMedia).not.toHaveBeenCalled();
|
|
110
|
+
expect(mocks.readPrivateArtifact).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("fails closed when the app Labs setting cannot be read", async () => {
|
|
114
|
+
mocks.getSession.mockResolvedValue({
|
|
115
|
+
email: "alice@example.test",
|
|
116
|
+
orgId: "org-1",
|
|
117
|
+
});
|
|
118
|
+
mocks.isCreativeContextLabAvailable.mockRejectedValue(
|
|
119
|
+
new Error("Labs settings unavailable"),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
await expect(mocks.handler!(event())).rejects.toThrow(
|
|
123
|
+
"Labs settings unavailable",
|
|
124
|
+
);
|
|
125
|
+
expect(mocks.runWithRequestContext).not.toHaveBeenCalled();
|
|
126
|
+
expect(mocks.getCreativeContextItem).not.toHaveBeenCalled();
|
|
127
|
+
expect(mocks.readPrivateArtifact).not.toHaveBeenCalled();
|
|
77
128
|
});
|
|
78
129
|
|
|
79
130
|
it("runs access-scoped reads under the authenticated request context", async () => {
|
|
@@ -91,6 +142,10 @@ describe("creative context media route", () => {
|
|
|
91
142
|
"item-1",
|
|
92
143
|
"version-1",
|
|
93
144
|
);
|
|
145
|
+
expect(mocks.isCreativeContextLabAvailable).toHaveBeenCalledWith(
|
|
146
|
+
"alice@example.test",
|
|
147
|
+
"creative-context.library",
|
|
148
|
+
);
|
|
94
149
|
});
|
|
95
150
|
|
|
96
151
|
it("uses the narrow pending-submission path only after generic item access fails", async () => {
|
package/src/server/media.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
readPendingCreativeContextMedia,
|
|
17
17
|
} from "../store/index.js";
|
|
18
18
|
import { getCreativeContext } from "./context.js";
|
|
19
|
+
import { isCreativeContextLabAvailable } from "./labs.js";
|
|
19
20
|
|
|
20
21
|
const SAFE_MIME_TYPES = new Set([
|
|
21
22
|
"image/png",
|
|
@@ -38,6 +39,14 @@ export function createCreativeContextMediaPlugin(): NitroPluginDef {
|
|
|
38
39
|
if (!userEmail) {
|
|
39
40
|
return new Response("Authentication required", { status: 401 });
|
|
40
41
|
}
|
|
42
|
+
if (
|
|
43
|
+
!(await isCreativeContextLabAvailable(
|
|
44
|
+
userEmail,
|
|
45
|
+
getCreativeContext().labKey,
|
|
46
|
+
))
|
|
47
|
+
) {
|
|
48
|
+
return new Response("Not found", { status: 404 });
|
|
49
|
+
}
|
|
41
50
|
const orgId = session?.orgId;
|
|
42
51
|
const url = new URL(
|
|
43
52
|
event.req.url,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
assertCreativeContextLabEnabled: vi.fn(),
|
|
5
|
+
resolveFromStore: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("./labs.js", () => ({
|
|
9
|
+
assertCreativeContextLabEnabled: mocks.assertCreativeContextLabEnabled,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("../store/contexts.js", () => ({
|
|
13
|
+
resolveNativeContextCloneReference: mocks.resolveFromStore,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { resolveNativeContextCloneReference } from "./native-context-clone.js";
|
|
17
|
+
|
|
18
|
+
describe("resolveNativeContextCloneReference", () => {
|
|
19
|
+
beforeEach(() => vi.clearAllMocks());
|
|
20
|
+
|
|
21
|
+
it("checks Labs before reading a clone reference", async () => {
|
|
22
|
+
const input = {
|
|
23
|
+
appId: "design",
|
|
24
|
+
resourceType: "design",
|
|
25
|
+
resourceId: "design-1",
|
|
26
|
+
contextId: "context-1",
|
|
27
|
+
artifactKey: "design:design:design-1",
|
|
28
|
+
};
|
|
29
|
+
const reference = { publishedItemVersionId: "version-1" };
|
|
30
|
+
mocks.resolveFromStore.mockResolvedValue(reference);
|
|
31
|
+
|
|
32
|
+
await expect(resolveNativeContextCloneReference(input)).resolves.toBe(
|
|
33
|
+
reference,
|
|
34
|
+
);
|
|
35
|
+
expect(mocks.assertCreativeContextLabEnabled).toHaveBeenCalledOnce();
|
|
36
|
+
expect(mocks.resolveFromStore).toHaveBeenCalledWith(input);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("does not read the reference when Labs is disabled or unreadable", async () => {
|
|
40
|
+
mocks.assertCreativeContextLabEnabled.mockRejectedValue(
|
|
41
|
+
new Error("Creative Context is disabled in Labs"),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await expect(
|
|
45
|
+
resolveNativeContextCloneReference({
|
|
46
|
+
appId: "design",
|
|
47
|
+
resourceType: "design",
|
|
48
|
+
resourceId: "design-1",
|
|
49
|
+
contextId: "context-1",
|
|
50
|
+
artifactKey: "design:design:design-1",
|
|
51
|
+
}),
|
|
52
|
+
).rejects.toThrow("Creative Context is disabled in Labs");
|
|
53
|
+
expect(mocks.resolveFromStore).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
});
|