@agent-native/dispatch 0.40.40 → 0.40.41-beta-dispatch-embed-01a15dc9-20261027050558

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 (45) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/actions/create_embed_session.js +1 -0
  3. package/dist/actions/create_embed_session.js.map +1 -1
  4. package/dist/actions/delete-destination.d.ts +12 -12
  5. package/dist/actions/provider-api-register.d.ts +13 -13
  6. package/dist/components/layout/Layout.d.ts.map +1 -1
  7. package/dist/components/layout/Layout.js +8 -11
  8. package/dist/components/layout/Layout.js.map +1 -1
  9. package/dist/components/workspace-app-host.d.ts.map +1 -1
  10. package/dist/components/workspace-app-host.js +23 -10
  11. package/dist/components/workspace-app-host.js.map +1 -1
  12. package/dist/lib/workspace-apps.d.ts +2 -2
  13. package/dist/lib/workspace-apps.d.ts.map +1 -1
  14. package/dist/lib/workspace-apps.js +35 -5
  15. package/dist/lib/workspace-apps.js.map +1 -1
  16. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  17. package/dist/server/lib/app-creation-store.js +173 -48
  18. package/dist/server/lib/app-creation-store.js.map +1 -1
  19. package/dist/server/lib/community-apps.js +1 -1
  20. package/dist/server/lib/community-apps.js.map +1 -1
  21. package/dist/server/lib/mcp-gateway.d.ts +1 -0
  22. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  23. package/dist/server/lib/mcp-gateway.js +170 -38
  24. package/dist/server/lib/mcp-gateway.js.map +1 -1
  25. package/dist/server/lib/provider-api.d.ts +2 -2
  26. package/dist/shared/workspace-app-route.d.ts +2 -0
  27. package/dist/shared/workspace-app-route.d.ts.map +1 -0
  28. package/dist/shared/workspace-app-route.js +4 -0
  29. package/dist/shared/workspace-app-route.js.map +1 -0
  30. package/package.json +4 -4
  31. package/src/actions/create_embed_session.ts +1 -0
  32. package/src/actions/index.spec.ts +3 -0
  33. package/src/components/layout/Layout.spec.tsx +39 -9
  34. package/src/components/layout/Layout.tsx +19 -40
  35. package/src/components/workspace-app-host.spec.tsx +88 -1
  36. package/src/components/workspace-app-host.tsx +30 -9
  37. package/src/lib/workspace-apps.spec.ts +64 -5
  38. package/src/lib/workspace-apps.ts +36 -6
  39. package/src/routes/pages/apps.$appId.spec.tsx +21 -3
  40. package/src/server/lib/app-creation-store.spec.ts +502 -11
  41. package/src/server/lib/app-creation-store.ts +198 -50
  42. package/src/server/lib/community-apps.ts +1 -1
  43. package/src/server/lib/mcp-gateway.spec.ts +396 -9
  44. package/src/server/lib/mcp-gateway.ts +209 -45
  45. package/src/shared/workspace-app-route.ts +3 -0
@@ -472,6 +472,194 @@ describe("listWorkspaceApps", () => {
472
472
  ]);
473
473
  });
474
474
 
475
+ it("keeps healthy gateway registry apps when one app URL is malformed", async () => {
476
+ const fetchMock = vi.fn(async () => {
477
+ return new Response(
478
+ JSON.stringify({
479
+ apps: [
480
+ {
481
+ id: "invalid-url",
482
+ name: "Invalid URL",
483
+ path: "/invalid-url",
484
+ url: "not-a-url",
485
+ },
486
+ {
487
+ id: "healthy-app",
488
+ name: "Healthy App",
489
+ path: "/healthy-app",
490
+ url: "https://agent-workspace.builder.io/healthy-app",
491
+ },
492
+ ],
493
+ }),
494
+ { headers: { "content-type": "application/json" } },
495
+ );
496
+ });
497
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
498
+ try {
499
+ vi.stubGlobal("fetch", fetchMock);
500
+ vi.stubEnv("A2A_SECRET", "test-a2a-secret");
501
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
502
+
503
+ const apps = await runWithRequestContext(
504
+ {
505
+ userEmail: "dev@example.test",
506
+ requestOrigin: "https://beta.dispatch.agent-native.com",
507
+ },
508
+ () => listWorkspaceApps({ includeAgentCards: false }),
509
+ );
510
+
511
+ expect(apps.map((app) => app.id)).toEqual(["healthy-app"]);
512
+ expect(warnSpy).toHaveBeenCalledWith(
513
+ "[dispatch] Could not resolve Workspace app invalid-url; skipping app",
514
+ expect.any(Error),
515
+ );
516
+ expect(fetchMock).toHaveBeenCalledTimes(1);
517
+ } finally {
518
+ warnSpy.mockRestore();
519
+ }
520
+ });
521
+
522
+ it("rejects a gateway registry when every app URL is invalid", async () => {
523
+ const fetchMock = vi.fn(async () => {
524
+ return new Response(
525
+ JSON.stringify({
526
+ apps: [
527
+ {
528
+ id: "invalid-url",
529
+ name: "Invalid URL",
530
+ path: "/invalid-url",
531
+ url: "not-a-url",
532
+ },
533
+ ],
534
+ }),
535
+ { headers: { "content-type": "application/json" } },
536
+ );
537
+ });
538
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
539
+ try {
540
+ vi.stubGlobal("fetch", fetchMock);
541
+ vi.stubEnv("A2A_SECRET", "test-a2a-secret");
542
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
543
+
544
+ await expect(
545
+ runWithRequestContext(
546
+ {
547
+ userEmail: "dev@example.test",
548
+ requestOrigin: "https://beta.dispatch.agent-native.com",
549
+ },
550
+ () => listWorkspaceApps({ includeAgentCards: false }),
551
+ ),
552
+ ).rejects.toMatchObject({
553
+ statusCode: 503,
554
+ errorCode: "workspace_gateway_invalid_response",
555
+ });
556
+ expect(warnSpy).toHaveBeenCalledWith(
557
+ "[dispatch] Could not resolve Workspace app invalid-url; skipping app",
558
+ expect.any(Error),
559
+ );
560
+ expect(fetchMock).toHaveBeenCalledTimes(1);
561
+ } finally {
562
+ warnSpy.mockRestore();
563
+ }
564
+ });
565
+
566
+ it("accepts a genuinely empty gateway registry", async () => {
567
+ const fetchMock = vi.fn(async () => {
568
+ return new Response(JSON.stringify({ apps: [] }), {
569
+ headers: { "content-type": "application/json" },
570
+ });
571
+ });
572
+ vi.stubGlobal("fetch", fetchMock);
573
+ vi.stubEnv("A2A_SECRET", "test-a2a-secret");
574
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
575
+
576
+ const apps = await runWithRequestContext(
577
+ {
578
+ userEmail: "dev@example.test",
579
+ requestOrigin: "https://beta.dispatch.agent-native.com",
580
+ },
581
+ () => listWorkspaceApps({ includeAgentCards: false }),
582
+ );
583
+
584
+ expect(apps).toEqual([]);
585
+ expect(fetchMock).toHaveBeenCalledTimes(1);
586
+ });
587
+
588
+ it.each([
589
+ [
590
+ "gateway mount path",
591
+ "https://beta.agent-workspace.builder.io/dispatch",
592
+ "",
593
+ ],
594
+ ["APP_BASE_PATH", "https://beta.agent-workspace.builder.io", "/dispatch"],
595
+ ] as const)(
596
+ "keeps mounted Workspace prefixes on gateway app URLs with %s",
597
+ async (_mountConfig, gatewayUrl, appBasePath) => {
598
+ const fetchMock = vi.fn(async () => {
599
+ return new Response(
600
+ JSON.stringify({
601
+ apps: [
602
+ {
603
+ id: "customer-ticket-triage",
604
+ name: "Customer Ticket Triage",
605
+ path: "/customer-ticket-triage",
606
+ url: "https://beta.agent-workspace.builder.io/customer-ticket-triage",
607
+ },
608
+ ],
609
+ }),
610
+ { headers: { "content-type": "application/json" } },
611
+ );
612
+ });
613
+ vi.stubGlobal("fetch", fetchMock);
614
+ vi.stubEnv("A2A_SECRET", "test-a2a-secret");
615
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", gatewayUrl);
616
+ vi.stubEnv("APP_BASE_PATH", appBasePath);
617
+ vi.stubEnv("VITE_APP_BASE_PATH", "");
618
+
619
+ const apps = await runWithRequestContext(
620
+ {
621
+ userEmail: "dev@example.test",
622
+ requestOrigin: "https://beta.dispatch.agent-native.com",
623
+ },
624
+ () => listWorkspaceApps({ includeAgentCards: false }),
625
+ );
626
+
627
+ expect(apps.find((app) => app.id === "customer-ticket-triage")?.url).toBe(
628
+ "https://beta.agent-workspace.builder.io/dispatch/apps/customer-ticket-triage",
629
+ );
630
+ },
631
+ );
632
+
633
+ it("applies APP_BASE_PATH to locally resolved workspace app URLs", async () => {
634
+ stubNoPendingContext();
635
+ stubManifest([
636
+ {
637
+ id: "customer-ticket-triage",
638
+ name: "Customer Ticket Triage",
639
+ path: "/customer-ticket-triage",
640
+ },
641
+ ]);
642
+ vi.stubEnv("APP_URL", "https://beta.agent-workspace.builder.io");
643
+ vi.stubEnv("APP_BASE_PATH", "/dispatch");
644
+ vi.stubEnv("VITE_APP_BASE_PATH", "");
645
+ vi.stubEnv(
646
+ "WORKSPACE_GATEWAY_URL",
647
+ "https://beta.agent-workspace.builder.io",
648
+ );
649
+
650
+ const apps = await runWithRequestContext(
651
+ {
652
+ userEmail: "dev@example.test",
653
+ requestOrigin: "https://beta.agent-workspace.builder.io",
654
+ },
655
+ () => listWorkspaceApps({ includeAgentCards: false }),
656
+ );
657
+
658
+ expect(apps.find((app) => app.id === "customer-ticket-triage")?.url).toBe(
659
+ "https://beta.agent-workspace.builder.io/dispatch/apps/customer-ticket-triage",
660
+ );
661
+ });
662
+
475
663
  it("lists only ready org-visible apps for an organization principal", async () => {
476
664
  stubNoPendingContext();
477
665
  stubManifest([
@@ -581,7 +769,7 @@ describe("listWorkspaceApps", () => {
581
769
  );
582
770
 
583
771
  expect(apps.find((app) => app.id === "todo")?.url).toBe(
584
- "https://workspace-preview.vercel.app/todo",
772
+ "https://workspace-preview.vercel.app/apps/todo",
585
773
  );
586
774
  });
587
775
 
@@ -633,7 +821,7 @@ describe("listWorkspaceApps", () => {
633
821
  }),
634
822
  );
635
823
  expect(apps.map((app) => app.id)).toEqual(["atlas"]);
636
- expect(apps[0]?.url).toBe("https://agent-workspace.builder.io/atlas");
824
+ expect(apps[0]?.url).toBe("https://agent-workspace.builder.io/apps/atlas");
637
825
  });
638
826
 
639
827
  it("signs the hosted registry request as the member with the deployment secret", async () => {
@@ -1152,7 +1340,7 @@ describe("listWorkspaceApps", () => {
1152
1340
  "https://beta.agent-workspace.builder.io/_agent-native/actions/list-workspace-apps?includeAgentCards=false&audience=all",
1153
1341
  );
1154
1342
  expect(apps[0]?.url).toBe(
1155
- "https://beta.agent-workspace.builder.io/private-app",
1343
+ "https://beta.agent-workspace.builder.io/apps/private-app",
1156
1344
  );
1157
1345
  });
1158
1346
 
@@ -1229,19 +1417,277 @@ describe("listWorkspaceApps", () => {
1229
1417
  expect(tokenPayload).not.toHaveProperty("org_id");
1230
1418
  });
1231
1419
 
1232
- it("falls back to local discovery when the gateway URL is malformed", async () => {
1420
+ it("surfaces malformed gateway config while resolving manifest app URLs", async () => {
1233
1421
  stubManifest();
1234
- vi.stubEnv("WORKSPACE_GATEWAY_URL", "not-a-url");
1235
- const fetchMock = vi.fn();
1422
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", "http://127.0.0.1:8080");
1423
+ const fetchMock = vi.fn(async () => {
1424
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", "not-a-url");
1425
+ return new Response("not found", { status: 404 });
1426
+ });
1236
1427
  vi.stubGlobal("fetch", fetchMock);
1237
1428
 
1238
- const apps = await runWithRequestContext(
1239
- { userEmail: "dev@example.test" },
1240
- () => listWorkspaceApps({ includeAgentCards: false }),
1429
+ await expect(
1430
+ runWithRequestContext(
1431
+ {
1432
+ userEmail: "dev@example.test",
1433
+ requestOrigin: "http://127.0.0.1:8080",
1434
+ },
1435
+ () => listWorkspaceApps({ includeAgentCards: false }),
1436
+ ),
1437
+ ).rejects.toMatchObject({ name: "WorkspaceGatewayConfigurationError" });
1438
+
1439
+ expect(fetchMock).toHaveBeenCalledTimes(1);
1440
+ });
1441
+
1442
+ it("rejects unsupported gateway schemes before beta environment rewriting", async () => {
1443
+ vi.stubEnv("A2A_SECRET", "test-a2a-secret");
1444
+ vi.stubEnv(
1445
+ "WORKSPACE_GATEWAY_URL",
1446
+ "ftp://agent-workspace.builder.io/dispatch",
1241
1447
  );
1448
+ const fetchMock = vi.fn();
1449
+ vi.stubGlobal("fetch", fetchMock);
1450
+
1451
+ await expect(
1452
+ runWithRequestContext(
1453
+ {
1454
+ userEmail: "dev@example.test",
1455
+ requestOrigin: "https://beta.dispatch.agent-native.com",
1456
+ },
1457
+ () => listWorkspaceApps({ includeAgentCards: false }),
1458
+ ),
1459
+ ).rejects.toMatchObject({ name: "WorkspaceGatewayConfigurationError" });
1242
1460
 
1243
1461
  expect(fetchMock).not.toHaveBeenCalled();
1244
- expect(apps.map((app) => app.id)).toEqual(["dispatch"]);
1462
+ });
1463
+
1464
+ it("surfaces malformed gateway config when normalizing absolute app URLs", async () => {
1465
+ const workspaceRoot = fs.mkdtempSync(
1466
+ path.join(os.tmpdir(), "dispatch-workspace-"),
1467
+ );
1468
+ const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(workspaceRoot);
1469
+ try {
1470
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", "http://127.0.0.1:8080");
1471
+ vi.stubEnv("AGENT_NATIVE_WORKSPACE_APPS_JSON", "");
1472
+ const fetchMock = vi.fn(async () => {
1473
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", "not-a-url");
1474
+ return new Response("not found", { status: 404 });
1475
+ });
1476
+ vi.stubGlobal("fetch", fetchMock);
1477
+ const manifestDirectory = path.join(workspaceRoot, ".agent-native");
1478
+ fs.mkdirSync(manifestDirectory, { recursive: true });
1479
+ fs.writeFileSync(
1480
+ path.join(manifestDirectory, "workspace-apps.json"),
1481
+ JSON.stringify([
1482
+ {
1483
+ id: "customer-ticket-triage",
1484
+ name: "Customer Ticket Triage",
1485
+ path: "/customer-ticket-triage",
1486
+ url: "https://beta.agent-workspace.builder.io/customer-ticket-triage",
1487
+ },
1488
+ ]),
1489
+ );
1490
+
1491
+ await expect(
1492
+ runWithRequestContext(
1493
+ {
1494
+ userEmail: "dev@example.test",
1495
+ requestOrigin: "http://127.0.0.1:8080",
1496
+ },
1497
+ () => listWorkspaceApps({ includeAgentCards: false }),
1498
+ ),
1499
+ ).rejects.toMatchObject({ name: "WorkspaceGatewayConfigurationError" });
1500
+ expect(fetchMock).toHaveBeenCalledTimes(1);
1501
+ } finally {
1502
+ cwdSpy.mockRestore();
1503
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
1504
+ }
1505
+ });
1506
+
1507
+ it("surfaces malformed gateway config for filesystem-discovered apps", async () => {
1508
+ const workspaceRoot = fs.mkdtempSync(
1509
+ path.join(os.tmpdir(), "dispatch-workspace-"),
1510
+ );
1511
+ const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(workspaceRoot);
1512
+ try {
1513
+ stubNoPendingContext();
1514
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", "http://127.0.0.1:8080");
1515
+ vi.stubEnv("AGENT_NATIVE_WORKSPACE_APPS_JSON", "");
1516
+ const fetchMock = vi.fn(async () => {
1517
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", "not-a-url");
1518
+ return new Response("not found", { status: 404 });
1519
+ });
1520
+ vi.stubGlobal("fetch", fetchMock);
1521
+ fs.writeFileSync(
1522
+ path.join(workspaceRoot, "package.json"),
1523
+ JSON.stringify({
1524
+ name: "test-workspace",
1525
+ "agent-native": { workspaceCore: "workspace-core" },
1526
+ }),
1527
+ );
1528
+ const appDir = path.join(workspaceRoot, "apps", "healthy");
1529
+ fs.mkdirSync(appDir, { recursive: true });
1530
+ fs.writeFileSync(
1531
+ path.join(appDir, "package.json"),
1532
+ JSON.stringify({ name: "healthy", displayName: "Healthy" }),
1533
+ );
1534
+
1535
+ await expect(
1536
+ runWithRequestContext(
1537
+ {
1538
+ userEmail: "dev@example.test",
1539
+ requestOrigin: "http://127.0.0.1:8080",
1540
+ },
1541
+ () => listWorkspaceApps({ includeAgentCards: false }),
1542
+ ),
1543
+ ).rejects.toMatchObject({ name: "WorkspaceGatewayConfigurationError" });
1544
+ expect(fetchMock).toHaveBeenCalledTimes(1);
1545
+ } finally {
1546
+ cwdSpy.mockRestore();
1547
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
1548
+ }
1549
+ });
1550
+
1551
+ it("skips an invalid absolute app URL without discarding healthy manifest apps", async () => {
1552
+ const workspaceRoot = fs.mkdtempSync(
1553
+ path.join(os.tmpdir(), "dispatch-workspace-"),
1554
+ );
1555
+ const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(workspaceRoot);
1556
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1557
+ try {
1558
+ vi.stubEnv(
1559
+ "WORKSPACE_GATEWAY_URL",
1560
+ "https://beta.agent-workspace.builder.io",
1561
+ );
1562
+ vi.stubEnv("AGENT_NATIVE_WORKSPACE_APPS_JSON", "");
1563
+ vi.stubEnv("A2A_SECRET", "test-a2a-secret");
1564
+ vi.stubGlobal(
1565
+ "fetch",
1566
+ vi.fn(async () => new Response("not found", { status: 404 })),
1567
+ );
1568
+ const manifestDirectory = path.join(workspaceRoot, ".agent-native");
1569
+ fs.mkdirSync(manifestDirectory, { recursive: true });
1570
+ fs.writeFileSync(
1571
+ path.join(manifestDirectory, "workspace-apps.json"),
1572
+ JSON.stringify([
1573
+ {
1574
+ id: "invalid-url",
1575
+ name: "Invalid URL",
1576
+ path: "/invalid-url",
1577
+ url: "not-a-url",
1578
+ },
1579
+ {
1580
+ id: "healthy-app",
1581
+ name: "Healthy App",
1582
+ path: "/healthy-app",
1583
+ url: "https://apps.example.test/healthy-app",
1584
+ },
1585
+ ]),
1586
+ );
1587
+
1588
+ const apps = await runWithRequestContext(
1589
+ {
1590
+ userEmail: "dev@example.test",
1591
+ requestOrigin: "https://beta.dispatch.agent-native.com",
1592
+ },
1593
+ () => listWorkspaceApps({ includeAgentCards: false }),
1594
+ );
1595
+
1596
+ expect(apps.map((app) => app.id)).toContain("healthy-app");
1597
+ expect(apps.map((app) => app.id)).not.toContain("invalid-url");
1598
+ expect(warnSpy).toHaveBeenCalledWith(
1599
+ "[dispatch] Could not resolve Workspace app invalid-url; skipping app",
1600
+ expect.any(Error),
1601
+ );
1602
+ } finally {
1603
+ warnSpy.mockRestore();
1604
+ cwdSpy.mockRestore();
1605
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
1606
+ }
1607
+ });
1608
+
1609
+ it("reports a malformed env manifest and continues to a file manifest", async () => {
1610
+ const workspaceRoot = fs.mkdtempSync(
1611
+ path.join(os.tmpdir(), "dispatch-workspace-"),
1612
+ );
1613
+ const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(workspaceRoot);
1614
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1615
+ try {
1616
+ vi.stubEnv("WORKSPACE_GATEWAY_URL", "");
1617
+ vi.stubEnv("AGENT_NATIVE_WORKSPACE_APPS_JSON", "{");
1618
+ const manifestPath = path.join(
1619
+ workspaceRoot,
1620
+ ".agent-native",
1621
+ "workspace-apps.json",
1622
+ );
1623
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
1624
+ fs.writeFileSync(
1625
+ manifestPath,
1626
+ JSON.stringify([
1627
+ {
1628
+ id: "healthy-app",
1629
+ name: "Healthy App",
1630
+ path: "/healthy-app",
1631
+ },
1632
+ ]),
1633
+ );
1634
+
1635
+ const apps = await runWithRequestContext(
1636
+ { userEmail: "dev@example.test" },
1637
+ () => listWorkspaceApps({ includeAgentCards: false }),
1638
+ );
1639
+
1640
+ expect(apps.map((app) => app.id)).toContain("healthy-app");
1641
+ expect(warnSpy).toHaveBeenCalledWith(
1642
+ "[dispatch] Could not read Workspace app manifest from AGENT_NATIVE_WORKSPACE_APPS_JSON; skipping source",
1643
+ );
1644
+ } finally {
1645
+ warnSpy.mockRestore();
1646
+ cwdSpy.mockRestore();
1647
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
1648
+ }
1649
+ });
1650
+
1651
+ it("skips a malformed manifest file and continues to the next candidate", async () => {
1652
+ const workspaceRoot = fs.mkdtempSync(
1653
+ path.join(os.tmpdir(), "dispatch-workspace-"),
1654
+ );
1655
+ const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(workspaceRoot);
1656
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1657
+ const malformedManifest = path.join(
1658
+ workspaceRoot,
1659
+ ".agent-native",
1660
+ "workspace-apps.json",
1661
+ );
1662
+ try {
1663
+ vi.stubEnv("AGENT_NATIVE_WORKSPACE_APPS_JSON", "");
1664
+ fs.mkdirSync(path.dirname(malformedManifest), { recursive: true });
1665
+ fs.writeFileSync(malformedManifest, "{");
1666
+ fs.writeFileSync(
1667
+ path.join(workspaceRoot, "workspace-apps.json"),
1668
+ JSON.stringify([
1669
+ {
1670
+ id: "healthy-app",
1671
+ name: "Healthy App",
1672
+ path: "/healthy-app",
1673
+ },
1674
+ ]),
1675
+ );
1676
+
1677
+ const apps = await runWithRequestContext(
1678
+ { userEmail: "dev@example.test" },
1679
+ () => listWorkspaceApps({ includeAgentCards: false }),
1680
+ );
1681
+
1682
+ expect(apps.map((app) => app.id)).toContain("healthy-app");
1683
+ expect(warnSpy).toHaveBeenCalledWith(
1684
+ `[dispatch] Could not read Workspace app manifest ${malformedManifest}; skipping source`,
1685
+ );
1686
+ } finally {
1687
+ warnSpy.mockRestore();
1688
+ cwdSpy.mockRestore();
1689
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
1690
+ }
1245
1691
  });
1246
1692
 
1247
1693
  it("keeps healthy filesystem apps discoverable when config or routes cannot load", async () => {
@@ -1262,12 +1708,15 @@ describe("listWorkspaceApps", () => {
1262
1708
  "healthy",
1263
1709
  "config-broken",
1264
1710
  "routes-broken",
1711
+ "package-broken",
1265
1712
  ]) {
1266
1713
  const appDir = path.join(workspaceRoot, "apps", app);
1267
1714
  fs.mkdirSync(appDir, { recursive: true });
1268
1715
  fs.writeFileSync(
1269
1716
  path.join(appDir, "package.json"),
1270
- JSON.stringify({ name: app, displayName: app }),
1717
+ app === "package-broken"
1718
+ ? "{"
1719
+ : JSON.stringify({ name: app, displayName: app }),
1271
1720
  );
1272
1721
  if (app === "routes-broken") {
1273
1722
  const brokenRoutes = path.join(appDir, "app", "routes");
@@ -1299,6 +1748,48 @@ describe("listWorkspaceApps", () => {
1299
1748
  }
1300
1749
  });
1301
1750
 
1751
+ it("continues workspace root discovery past malformed ancestor package metadata", async () => {
1752
+ const workspaceRoot = fs.mkdtempSync(
1753
+ path.join(os.tmpdir(), "dispatch-workspace-"),
1754
+ );
1755
+ const cwd = path.join(workspaceRoot, "worker");
1756
+ fs.mkdirSync(cwd);
1757
+ const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(cwd);
1758
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1759
+ try {
1760
+ stubNoPendingContext();
1761
+ fs.writeFileSync(
1762
+ path.join(workspaceRoot, "package.json"),
1763
+ JSON.stringify({
1764
+ name: "test-workspace",
1765
+ "agent-native": { workspaceCore: "workspace-core" },
1766
+ }),
1767
+ );
1768
+ fs.writeFileSync(path.join(cwd, "package.json"), "{");
1769
+ const appDir = path.join(workspaceRoot, "apps", "healthy");
1770
+ fs.mkdirSync(appDir, { recursive: true });
1771
+ fs.writeFileSync(
1772
+ path.join(appDir, "package.json"),
1773
+ JSON.stringify({ name: "healthy", displayName: "Healthy" }),
1774
+ );
1775
+
1776
+ const apps = await runWithRequestContext(
1777
+ { userEmail: "dev@example.test" },
1778
+ () => listWorkspaceApps({ includeAgentCards: false }),
1779
+ );
1780
+
1781
+ expect(apps.map((app) => app.id)).toContain("healthy");
1782
+ expect(warnSpy).toHaveBeenCalledWith(
1783
+ `[dispatch] Could not read workspace package metadata in ${cwd}; continuing parent search`,
1784
+ expect.any(Error),
1785
+ );
1786
+ } finally {
1787
+ warnSpy.mockRestore();
1788
+ cwdSpy.mockRestore();
1789
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
1790
+ }
1791
+ });
1792
+
1302
1793
  it("keeps legacy apps organization-visible when the org default cannot be read", async () => {
1303
1794
  stubManifest([
1304
1795
  { id: "dispatch", name: "Dispatch", path: "/dispatch" },