@agent-native/creative-context 0.1.0 → 0.3.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.
Files changed (105) hide show
  1. package/agent-native.package.json +11 -0
  2. package/dist/actions/continue-context-import.d.ts +16 -1
  3. package/dist/actions/continue-context-import.js +4 -1
  4. package/dist/actions/continue-context-import.js.map +1 -1
  5. package/dist/actions/get-creative-context.d.ts +38 -0
  6. package/dist/actions/get-creative-context.d.ts.map +1 -1
  7. package/dist/actions/get-creative-context.js +59 -23
  8. package/dist/actions/get-creative-context.js.map +1 -1
  9. package/dist/actions/get-google-picker-session.d.ts +10 -0
  10. package/dist/actions/get-google-picker-session.d.ts.map +1 -0
  11. package/dist/actions/get-google-picker-session.js +38 -0
  12. package/dist/actions/get-google-picker-session.js.map +1 -0
  13. package/dist/actions/index.d.ts.map +1 -1
  14. package/dist/actions/index.js +2 -0
  15. package/dist/actions/index.js.map +1 -1
  16. package/dist/actions/start-context-enrichment.d.ts +16 -1
  17. package/dist/actions/start-context-enrichment.js +6 -4
  18. package/dist/actions/start-context-enrichment.js.map +1 -1
  19. package/dist/actions/start-context-import.d.ts +16 -1
  20. package/dist/actions/start-context-import.js +2 -1
  21. package/dist/actions/start-context-import.js.map +1 -1
  22. package/dist/client/CreativeContextPanel.d.ts +1 -59
  23. package/dist/client/CreativeContextPanel.d.ts.map +1 -1
  24. package/dist/client/CreativeContextPanel.js +111 -36
  25. package/dist/client/CreativeContextPanel.js.map +1 -1
  26. package/dist/client/actions.d.ts +8 -0
  27. package/dist/client/actions.d.ts.map +1 -1
  28. package/dist/client/actions.js +4 -0
  29. package/dist/client/actions.js.map +1 -1
  30. package/dist/client/google-slides-picker.d.ts +19 -0
  31. package/dist/client/google-slides-picker.d.ts.map +1 -0
  32. package/dist/client/google-slides-picker.js +106 -0
  33. package/dist/client/google-slides-picker.js.map +1 -0
  34. package/dist/client/index.d.ts +1 -1
  35. package/dist/client/index.d.ts.map +1 -1
  36. package/dist/client/index.js +1 -1
  37. package/dist/client/index.js.map +1 -1
  38. package/dist/client/messages.d.ts +1 -1
  39. package/dist/client/messages.d.ts.map +1 -1
  40. package/dist/client/messages.js +22 -0
  41. package/dist/client/messages.js.map +1 -1
  42. package/dist/connectors/google-slides.d.ts +1 -1
  43. package/dist/connectors/google-slides.d.ts.map +1 -1
  44. package/dist/connectors/google-slides.js +1 -2
  45. package/dist/connectors/google-slides.js.map +1 -1
  46. package/dist/connectors/normalize.d.ts +7 -0
  47. package/dist/connectors/normalize.d.ts.map +1 -1
  48. package/dist/connectors/normalize.js +122 -3
  49. package/dist/connectors/normalize.js.map +1 -1
  50. package/dist/schema/index.d.ts +30 -30
  51. package/dist/server/enrichment.d.ts +1 -0
  52. package/dist/server/enrichment.d.ts.map +1 -1
  53. package/dist/server/enrichment.js +26 -24
  54. package/dist/server/enrichment.js.map +1 -1
  55. package/dist/server/generation-context.d.ts +1 -1
  56. package/dist/server/generation-context.d.ts.map +1 -1
  57. package/dist/server/generation-context.js +1 -1
  58. package/dist/server/generation-context.js.map +1 -1
  59. package/dist/store/content.d.ts +13 -0
  60. package/dist/store/content.d.ts.map +1 -1
  61. package/dist/store/content.js +172 -1
  62. package/dist/store/content.js.map +1 -1
  63. package/dist/store/jobs.d.ts.map +1 -1
  64. package/dist/store/jobs.js +2 -2
  65. package/dist/store/jobs.js.map +1 -1
  66. package/dist/store/purge.d.ts.map +1 -1
  67. package/dist/store/purge.js +9 -8
  68. package/dist/store/purge.js.map +1 -1
  69. package/dist/store/suggestions.d.ts +1 -0
  70. package/dist/store/suggestions.d.ts.map +1 -1
  71. package/dist/store/suggestions.js +56 -38
  72. package/dist/store/suggestions.js.map +1 -1
  73. package/docs/skills/creative-context/SKILL.md +8 -0
  74. package/package.json +3 -3
  75. package/src/actions/continue-context-import.ts +4 -1
  76. package/src/actions/creative-context-a2a.spec.ts +98 -0
  77. package/src/actions/get-creative-context.spec.ts +121 -1
  78. package/src/actions/get-creative-context.ts +78 -29
  79. package/src/actions/get-google-picker-session.spec.ts +62 -0
  80. package/src/actions/get-google-picker-session.ts +45 -0
  81. package/src/actions/index.ts +2 -0
  82. package/src/actions/job-public-serialization.spec.ts +122 -0
  83. package/src/actions/start-context-enrichment.ts +6 -4
  84. package/src/actions/start-context-import.ts +2 -1
  85. package/src/client/CreativeContextPanel.tsx +163 -64
  86. package/src/client/actions.ts +18 -0
  87. package/src/client/google-slides-picker.spec.ts +36 -0
  88. package/src/client/google-slides-picker.ts +146 -0
  89. package/src/client/index.ts +2 -0
  90. package/src/client/messages.ts +35 -0
  91. package/src/client/source-config.spec.ts +24 -10
  92. package/src/connectors/google-slides.ts +1 -2
  93. package/src/connectors/normalize.spec.ts +144 -0
  94. package/src/connectors/normalize.ts +201 -3
  95. package/src/server/enrichment.spec.ts +94 -0
  96. package/src/server/enrichment.ts +27 -24
  97. package/src/server/generation-artifact-access.spec.ts +104 -0
  98. package/src/server/generation-context-a2a.spec.ts +20 -2
  99. package/src/server/generation-context.ts +6 -6
  100. package/src/server/isolated-a2a.spec.ts +21 -1
  101. package/src/store/access.integration.spec.ts +511 -26
  102. package/src/store/content.ts +213 -1
  103. package/src/store/jobs.ts +2 -0
  104. package/src/store/purge.ts +18 -9
  105. package/src/store/suggestions.ts +67 -41
@@ -248,6 +248,18 @@ describe("get-context-item public-agent boundary", () => {
248
248
  },
249
249
  },
250
250
  },
251
+ edges: [
252
+ {
253
+ id: "edge-child-1",
254
+ fromItemId: "item-1",
255
+ fromItemVersionId: "version-1",
256
+ toItemId: "item-child-1",
257
+ toItemVersionId: "version-child-1",
258
+ toExternalId: "figma-file:child-1",
259
+ relation: "contains-native-child",
260
+ metadata: {},
261
+ },
262
+ ],
251
263
  });
252
264
 
253
265
  const result = (await action.run({
@@ -259,6 +271,18 @@ describe("get-context-item public-agent boundary", () => {
259
271
  dataRole: "untrusted-reference",
260
272
  format: "design-html",
261
273
  content: html,
274
+ retrieval: {
275
+ mode: "manifest-parts",
276
+ root: { itemId: "item-1", itemVersionId: "version-1" },
277
+ cloneAction: "clone-creative-context-design",
278
+ parts: [
279
+ {
280
+ externalId: "figma-file:child-1",
281
+ itemId: "item-child-1",
282
+ itemVersionId: "version-child-1",
283
+ },
284
+ ],
285
+ },
262
286
  });
263
287
  expect(mocks.getCreativeContextItemByExternalId).not.toHaveBeenCalled();
264
288
  });
@@ -305,11 +329,107 @@ describe("get-context-item public-agent boundary", () => {
305
329
  },
306
330
  },
307
331
  },
332
+ edges: [
333
+ {
334
+ id: "edge-child-1",
335
+ fromItemId: "item-1",
336
+ fromItemVersionId: "version-1",
337
+ toItemId: "item-child-1",
338
+ toItemVersionId: "version-child-1",
339
+ toExternalId: "figma-file:child-1",
340
+ relation: "contains-native-child",
341
+ metadata: {},
342
+ },
343
+ ],
308
344
  });
309
345
 
310
346
  const result = (await action.run({ itemId: "item-1" })) as any;
311
347
 
312
- expect(result.version.nativeCode).toBeNull();
348
+ expect(result.version.nativeCode).toEqual({
349
+ dataRole: "untrusted-reference",
350
+ format: "design-html",
351
+ content: null,
352
+ oversized: true,
353
+ byteLength: Buffer.byteLength(html, "utf8"),
354
+ maxInlineBytes: 128 * 1024,
355
+ retrieval: {
356
+ mode: "manifest-parts",
357
+ root: { itemId: "item-1", itemVersionId: "version-1" },
358
+ cloneAction: "clone-creative-context-design",
359
+ parts: [
360
+ {
361
+ externalId: "figma-file:child-1",
362
+ itemId: "item-child-1",
363
+ itemVersionId: "version-child-1",
364
+ },
365
+ ],
366
+ },
367
+ instruction: expect.stringContaining("exact clone action"),
368
+ });
369
+ expect(result.version.content).toContain(
370
+ "Oversized native code is omitted",
371
+ );
372
+ expect(JSON.stringify(result)).not.toContain("x".repeat(1_000));
313
373
  expect(mocks.getCreativeContextItemByExternalId).not.toHaveBeenCalled();
314
374
  });
375
+
376
+ it("returns an explicit exact-clone contract instead of oversized flat HTML", async () => {
377
+ const context = await mocks.getCreativeContextItem();
378
+ const html = `<div class="fmd-slide google-slides-native" data-source-slide-id="slide-1" style="position:relative;width:960px;height:540px"><p>${"x".repeat(128 * 1024)}</p></div>`;
379
+ mocks.getCreativeContextItem.mockResolvedValue({
380
+ ...context,
381
+ item: {
382
+ ...context.item,
383
+ mimeType: "text/html",
384
+ provenance: {
385
+ compiler: "@agent-native/creative-context:google-slides-native",
386
+ },
387
+ },
388
+ version: {
389
+ ...context.version,
390
+ mimeType: "text/html",
391
+ content: html,
392
+ metadata: {
393
+ nativeArtifact: {
394
+ schemaVersion: 1,
395
+ app: "slides",
396
+ format: "slides-html",
397
+ rootExternalId: "deck-1:slide-1",
398
+ fidelityReport: {
399
+ exact: { count: 1 },
400
+ approximated: { count: 0, reasons: [] },
401
+ imageFallback: { count: 0, reasons: [] },
402
+ },
403
+ },
404
+ },
405
+ },
406
+ edges: [],
407
+ });
408
+
409
+ const result = (await action.run({
410
+ itemId: "item-1",
411
+ itemVersionId: "version-1",
412
+ })) as any;
413
+
414
+ expect(result.version.nativeCode).toMatchObject({
415
+ dataRole: "untrusted-reference",
416
+ format: "slides-html",
417
+ content: null,
418
+ oversized: true,
419
+ byteLength: Buffer.byteLength(html, "utf8"),
420
+ maxInlineBytes: 128 * 1024,
421
+ retrieval: {
422
+ mode: "exact-clone-only",
423
+ root: { itemId: "item-1", itemVersionId: "version-1" },
424
+ cloneAction: "clone-context-slide",
425
+ parts: [],
426
+ },
427
+ instruction: expect.stringContaining("never concatenate"),
428
+ });
429
+ expect(result.version.nativeCode.content).toBeNull();
430
+ expect(result.version.content).toContain(
431
+ "Oversized native code is omitted",
432
+ );
433
+ expect(JSON.stringify(result)).not.toContain("x".repeat(1_000));
434
+ });
315
435
  });
@@ -5,7 +5,10 @@ import {
5
5
  reassembleNativeCreativeArtifact,
6
6
  validateCompiledNativeHtml,
7
7
  } from "../native-artifact-reassembly.js";
8
- import { nativeCreativeArtifactFromMetadata } from "../native-artifact.js";
8
+ import {
9
+ nativeCreativeArtifactFromMetadata,
10
+ type NativeCreativeArtifact,
11
+ } from "../native-artifact.js";
9
12
  import {
10
13
  sanitizePublicString,
11
14
  serializePublicContextDetail,
@@ -21,7 +24,7 @@ import {
21
24
  getCreativeContextItemByExternalId,
22
25
  } from "../store/index.js";
23
26
 
24
- const MAX_STORED_NATIVE_CODE_BYTES = 128 * 1024;
27
+ const MAX_PUBLIC_NATIVE_CODE_BYTES = 128 * 1024;
25
28
 
26
29
  export default defineAction({
27
30
  description:
@@ -44,17 +47,11 @@ export default defineAction({
44
47
  context.version.metadata,
45
48
  );
46
49
  const nativeCode = nativeArtifact
47
- ? nativeArtifact.manifest
48
- ? boundedStoredNativeCode(context.version.content, nativeArtifact)
49
- : ((
50
- await reassembleNativeCreativeArtifact({
51
- root: context,
52
- app: nativeArtifact.app,
53
- format: nativeArtifact.format,
54
- resolveChild: getCreativeContextItemByExternalId,
55
- }).catch(() => null)
56
- )?.html ?? null)
50
+ ? await publicNativeCode(context, nativeArtifact)
57
51
  : null;
52
+ const nativeCodeOversized = Boolean(
53
+ nativeCode && "oversized" in nativeCode && nativeCode.oversized,
54
+ );
58
55
  return {
59
56
  ...publicContext,
60
57
  pendingJobId: await ensureContextItemHydration(context.item.id),
@@ -75,16 +72,11 @@ export default defineAction({
75
72
  sanitizePublicString(context.version.title),
76
73
  ),
77
74
  content: delimitUntrustedReference(
78
- sanitizePublicString(context.version.content),
75
+ nativeCodeOversized
76
+ ? "Oversized native code is omitted from this public result. Use version.nativeCode.retrieval."
77
+ : sanitizePublicString(context.version.content),
79
78
  ),
80
- nativeCode:
81
- nativeArtifact && nativeCode
82
- ? {
83
- dataRole: UNTRUSTED_REFERENCE_ROLE,
84
- format: nativeArtifact.format,
85
- content: nativeCode,
86
- }
87
- : null,
79
+ nativeCode: nativeArtifact && nativeCode ? nativeCode : null,
88
80
  summary: context.version.summary
89
81
  ? delimitUntrustedReference(
90
82
  sanitizePublicString(context.version.summary),
@@ -132,17 +124,74 @@ export default defineAction({
132
124
  },
133
125
  });
134
126
 
135
- function boundedStoredNativeCode(
136
- content: string,
137
- artifact: Parameters<typeof validateCompiledNativeHtml>[1],
138
- ): string | null {
139
- if (Buffer.byteLength(content, "utf8") > MAX_STORED_NATIVE_CODE_BYTES) {
140
- return null;
141
- }
127
+ async function publicNativeCode(
128
+ context: NonNullable<Awaited<ReturnType<typeof getCreativeContextItem>>>,
129
+ artifact: NativeCreativeArtifact,
130
+ ) {
142
131
  try {
132
+ const content = artifact.manifest
133
+ ? context.version.content
134
+ : (
135
+ await reassembleNativeCreativeArtifact({
136
+ root: context,
137
+ app: artifact.app,
138
+ format: artifact.format,
139
+ resolveChild: getCreativeContextItemByExternalId,
140
+ })
141
+ ).html;
143
142
  validateCompiledNativeHtml(content, artifact);
144
- return content;
143
+ const byteLength = Buffer.byteLength(content, "utf8");
144
+ if (byteLength <= MAX_PUBLIC_NATIVE_CODE_BYTES) {
145
+ return {
146
+ dataRole: UNTRUSTED_REFERENCE_ROLE,
147
+ format: artifact.format,
148
+ content,
149
+ ...(artifact.manifest
150
+ ? { retrieval: nativeCodeRetrieval(context, artifact) }
151
+ : {}),
152
+ };
153
+ }
154
+ return {
155
+ dataRole: UNTRUSTED_REFERENCE_ROLE,
156
+ format: artifact.format,
157
+ content: null,
158
+ oversized: true,
159
+ byteLength,
160
+ maxInlineBytes: MAX_PUBLIC_NATIVE_CODE_BYTES,
161
+ retrieval: nativeCodeRetrieval(context, artifact),
162
+ instruction:
163
+ "Use the exact clone action for the complete artifact. For a manifest artifact, inspect individually pinned parts with get-context-item; never concatenate a truncated HTML fragment.",
164
+ };
145
165
  } catch {
146
166
  return null;
147
167
  }
148
168
  }
169
+
170
+ function nativeCodeRetrieval(
171
+ context: NonNullable<Awaited<ReturnType<typeof getCreativeContextItem>>>,
172
+ artifact: NativeCreativeArtifact,
173
+ ) {
174
+ return {
175
+ mode: artifact.manifest ? "manifest-parts" : "exact-clone-only",
176
+ root: {
177
+ itemId: context.item.id,
178
+ itemVersionId: context.version.id,
179
+ },
180
+ cloneAction:
181
+ artifact.app === "slides"
182
+ ? "clone-context-slide"
183
+ : "clone-creative-context-design",
184
+ parts: (artifact.manifest?.children ?? []).map((child) => {
185
+ const edge = context.edges.find(
186
+ (candidate) =>
187
+ candidate.relation === "contains-native-child" &&
188
+ candidate.toExternalId === child.externalId,
189
+ );
190
+ return {
191
+ externalId: sanitizePublicString(child.externalId),
192
+ itemId: edge?.toItemId ?? null,
193
+ itemVersionId: edge?.toItemVersionId ?? null,
194
+ };
195
+ }),
196
+ };
197
+ }
@@ -0,0 +1,62 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const resolveProviderApiOAuthAccessToken = vi.fn();
4
+ const resolveSecret = vi.fn();
5
+
6
+ vi.mock("@agent-native/core/provider-api", () => ({
7
+ resolveProviderApiOAuthAccessToken,
8
+ }));
9
+ vi.mock("@agent-native/core/server", () => ({ resolveSecret }));
10
+ vi.mock("../server/context.js", () => ({
11
+ getCreativeContext: () => ({ connectorContext: { appId: "slides" } }),
12
+ }));
13
+
14
+ const { default: action } = await import("./get-google-picker-session.js");
15
+
16
+ describe("get-google-picker-session", () => {
17
+ beforeEach(() => {
18
+ resolveProviderApiOAuthAccessToken.mockReset();
19
+ resolveSecret.mockReset();
20
+ resolveProviderApiOAuthAccessToken.mockResolvedValue({
21
+ accessToken: "short-lived-access-token",
22
+ accountLabel: "Work Google",
23
+ });
24
+ resolveSecret.mockImplementation(async (key: string) =>
25
+ key === "GOOGLE_PICKER_API_KEY"
26
+ ? "browser-restricted-key"
27
+ : key === "GOOGLE_PICKER_APP_ID"
28
+ ? "123456789"
29
+ : null,
30
+ );
31
+ });
32
+
33
+ it("is UI-only and returns a Picker session for an app-scoped connection", async () => {
34
+ expect(action.agentTool).toBe(false);
35
+ expect(action.toolCallable).toBe(false);
36
+ expect(action.requiresAuth).toBe(true);
37
+
38
+ await expect(
39
+ action.run({ connectionId: "drive-connection" }),
40
+ ).resolves.toEqual({
41
+ accessToken: "short-lived-access-token",
42
+ accountLabel: "Work Google",
43
+ apiKey: "browser-restricted-key",
44
+ appId: "123456789",
45
+ });
46
+ expect(resolveProviderApiOAuthAccessToken).toHaveBeenCalledWith(
47
+ { provider: "google_drive", connectionId: "drive-connection" },
48
+ expect.objectContaining({
49
+ appId: "slides",
50
+ providerIds: ["google_drive"],
51
+ }),
52
+ );
53
+ });
54
+
55
+ it("fails closed when the browser-restricted Picker config is absent", async () => {
56
+ resolveSecret.mockResolvedValue(null);
57
+
58
+ await expect(
59
+ action.run({ connectionId: "drive-connection" }),
60
+ ).rejects.toThrow(/Google Picker is not configured/);
61
+ });
62
+ });
@@ -0,0 +1,45 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { resolveProviderApiOAuthAccessToken } from "@agent-native/core/provider-api";
3
+ import { resolveSecret } from "@agent-native/core/server";
4
+ import { z } from "zod";
5
+
6
+ import { getCreativeContext } from "../server/context.js";
7
+
8
+ export default defineAction({
9
+ description:
10
+ "Return a short-lived Google Picker session for the signed-in Library UI.",
11
+ schema: z.object({
12
+ connectionId: z.string().min(1),
13
+ }),
14
+ http: { method: "GET" },
15
+ readOnly: true,
16
+ requiresAuth: true,
17
+ agentTool: false,
18
+ toolCallable: false,
19
+ run: async ({ connectionId }) => {
20
+ const { connectorContext } = getCreativeContext();
21
+ const [oauth, apiKey, appId] = await Promise.all([
22
+ resolveProviderApiOAuthAccessToken(
23
+ { provider: "google_drive", connectionId },
24
+ {
25
+ appId: connectorContext.appId,
26
+ providerIds: ["google_drive"],
27
+ localCredentialSource: "creative_context_picker",
28
+ },
29
+ ),
30
+ resolveSecret("GOOGLE_PICKER_API_KEY"),
31
+ resolveSecret("GOOGLE_PICKER_APP_ID"),
32
+ ]);
33
+ if (!apiKey || !appId) {
34
+ throw new Error(
35
+ "Google Picker is not configured. Set GOOGLE_PICKER_API_KEY and GOOGLE_PICKER_APP_ID.",
36
+ );
37
+ }
38
+ return {
39
+ accessToken: oauth.accessToken,
40
+ accountLabel: oauth.accountLabel,
41
+ apiKey,
42
+ appId,
43
+ };
44
+ },
45
+ });
@@ -7,6 +7,7 @@ import getBrandProfile from "./get-brand-profile.js";
7
7
  import getContextImportStatus from "./get-context-import-status.js";
8
8
  import getContextItem from "./get-context-item.js";
9
9
  import getContextPack from "./get-context-pack.js";
10
+ import getGooglePickerSession from "./get-google-picker-session.js";
10
11
  import inferBrandDna from "./infer-brand-dna.js";
11
12
  import listCanonicalLogoCandidates from "./list-canonical-logo-candidates.js";
12
13
  import listContextConnections from "./list-context-connections.js";
@@ -39,6 +40,7 @@ export const creativeContextActions: Record<string, ActionEntry> = {
39
40
  "get-context-import-status": getContextImportStatus,
40
41
  "get-context-item": getContextItem,
41
42
  "get-context-pack": getContextPack,
43
+ "get-google-picker-session": getGooglePickerSession,
42
44
  "infer-brand-dna": inferBrandDna,
43
45
  "list-canonical-logo-candidates": listCanonicalLogoCandidates,
44
46
  "list-context-packs": listContextPacks,
@@ -0,0 +1,122 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ continueJob: vi.fn(),
5
+ createJob: vi.fn(),
6
+ getContextSource: vi.fn(),
7
+ dispatchCreativeContextImportJob: vi.fn(async () => undefined),
8
+ }));
9
+
10
+ vi.mock("../store/index.js", () => ({
11
+ continueJob: mocks.continueJob,
12
+ createJob: mocks.createJob,
13
+ getContextSource: mocks.getContextSource,
14
+ }));
15
+
16
+ vi.mock("../jobs/index.js", () => ({
17
+ dispatchCreativeContextImportJob: mocks.dispatchCreativeContextImportJob,
18
+ }));
19
+
20
+ vi.mock("../server/context.js", () => ({
21
+ getCreativeContext: () => ({ connectorContext: { appId: "slides" } }),
22
+ }));
23
+
24
+ import continueImport from "./continue-context-import.js";
25
+ import startEnrichment from "./start-context-enrichment.js";
26
+ import startImport from "./start-context-import.js";
27
+
28
+ const privateJob = {
29
+ id: "job-1",
30
+ ownerEmail: "owner@example.test",
31
+ orgId: "org-secret",
32
+ sourceId: "source-1",
33
+ kind: "import",
34
+ status: "running",
35
+ mode: "incremental",
36
+ progressCurrent: 2,
37
+ progressTotal: 4,
38
+ attempts: 1,
39
+ leaseOwner: "worker-private",
40
+ leaseToken: "lease-private",
41
+ leaseExpiresAt: "2026-07-16T02:00:00.000Z",
42
+ nextResumeAt: null,
43
+ budget: { token: "budget-private" },
44
+ checkpoint: {
45
+ blobRef: "creative-context-blob:v1:checkpoint-private",
46
+ warning:
47
+ "Fetched https://provider.example/private?X-Amz-Signature=checkpoint-private",
48
+ },
49
+ request: {
50
+ providerUrl:
51
+ "https://provider.example/private?X-Amz-Signature=request-private",
52
+ },
53
+ result: {
54
+ processed: 2,
55
+ warning:
56
+ "Failed https://provider.example/private?X-Amz-Signature=result-private",
57
+ },
58
+ error:
59
+ "Provider https://provider.example/private?X-Amz-Signature=error-private failed",
60
+ createdAt: "2026-07-16T00:00:00.000Z",
61
+ startedAt: "2026-07-16T00:01:00.000Z",
62
+ completedAt: null,
63
+ };
64
+
65
+ function expectPublicJob(result: unknown) {
66
+ expect(result).toMatchObject({
67
+ job: {
68
+ id: "job-1",
69
+ sourceId: "source-1",
70
+ result: { processed: 2, warning: "Failed [redacted]" },
71
+ error: "Provider [redacted] failed",
72
+ },
73
+ });
74
+ const serialized = JSON.stringify(result);
75
+ expect(serialized).not.toMatch(
76
+ /owner@example|org-secret|worker-private|lease-private|checkpoint-private|request-private|result-private|error-private|creative-context-blob|X-Amz-Signature/i,
77
+ );
78
+ expect(serialized).not.toMatch(
79
+ /ownerEmail|orgId|leaseOwner|leaseToken|leaseExpiresAt|checkpoint|request|budget/i,
80
+ );
81
+ }
82
+
83
+ describe("creative context job action serialization", () => {
84
+ beforeEach(() => {
85
+ vi.clearAllMocks();
86
+ mocks.continueJob.mockResolvedValue(privateJob);
87
+ mocks.createJob.mockResolvedValue(privateJob);
88
+ mocks.getContextSource.mockResolvedValue({
89
+ id: "source-1",
90
+ ownerEmail: "owner@example.test",
91
+ });
92
+ });
93
+
94
+ it("redacts continuation job capabilities and provider URLs", async () => {
95
+ expectPublicJob(await continueImport.run({ jobId: "job-1" }));
96
+ });
97
+
98
+ it("redacts newly queued import job capabilities and provider URLs", async () => {
99
+ expectPublicJob(
100
+ await startImport.run({
101
+ sourceId: "source-1",
102
+ mode: "incremental",
103
+ }),
104
+ );
105
+ expect(mocks.dispatchCreativeContextImportJob).toHaveBeenCalledWith({
106
+ jobId: "job-1",
107
+ ownerEmail: "owner@example.test",
108
+ orgId: "org-secret",
109
+ appId: "slides",
110
+ });
111
+ });
112
+
113
+ it("redacts newly queued enrichment job capabilities and provider URLs", async () => {
114
+ expectPublicJob(
115
+ await startEnrichment.run({
116
+ sourceId: "source-1",
117
+ operation: "enrich-media",
118
+ eagerLimit: 25,
119
+ }),
120
+ );
121
+ });
122
+ });
@@ -1,6 +1,7 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
2
  import { z } from "zod";
3
3
 
4
+ import { serializePublicJob } from "../server/public-serialization.js";
4
5
  import { createJob } from "../store/index.js";
5
6
 
6
7
  const schema = z.object({
@@ -24,8 +25,8 @@ export default defineAction({
24
25
  schema,
25
26
  needsApproval: true,
26
27
  publicAgent: { expose: true, readOnly: false, requiresAuth: true },
27
- run: async (args) => ({
28
- job: await createJob({
28
+ run: async (args) => {
29
+ const job = await createJob({
29
30
  sourceId: args.sourceId,
30
31
  kind:
31
32
  args.operation === "enrich-media"
@@ -45,6 +46,7 @@ export default defineAction({
45
46
  eagerLimit: args.eagerLimit,
46
47
  remainingMode: "pending-on-demand",
47
48
  },
48
- }),
49
- }),
49
+ });
50
+ return { job: serializePublicJob(job) };
51
+ },
50
52
  });
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
 
4
4
  import { dispatchCreativeContextImportJob } from "../jobs/index.js";
5
5
  import { getCreativeContext } from "../server/context.js";
6
+ import { serializePublicJob } from "../server/public-serialization.js";
6
7
  import { createJob } from "../store/index.js";
7
8
  import { getContextSource } from "../store/index.js";
8
9
 
@@ -32,6 +33,6 @@ export default defineAction({
32
33
  orgId: job.orgId,
33
34
  appId: getCreativeContext().connectorContext.appId,
34
35
  });
35
- return { job };
36
+ return { job: serializePublicJob(job) };
36
37
  },
37
38
  });