@agent-native/dispatch 0.15.20 → 0.15.21
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/actions/provider-api-catalog.d.ts +1 -0
- package/dist/actions/provider-api-register.d.ts +14 -14
- package/dist/actions/remix-workspace-template.d.ts +20 -32
- package/dist/actions/set-app-creation-settings.js +2 -2
- package/dist/actions/set-app-creation-settings.js.map +1 -1
- package/dist/actions/start-workspace-app-creation.d.ts +1 -45
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +30 -2
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +1 -1
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +14 -1
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +36 -37
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +71 -6
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/provider-api.d.ts +1 -0
- package/dist/server/lib/provider-api.d.ts.map +1 -1
- package/dist/server/plugins/integrations.js +1 -1
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +2 -2
- package/src/actions/set-app-creation-settings.ts +4 -2
- package/src/components/create-app-popover.spec.tsx +249 -0
- package/src/components/create-app-popover.tsx +77 -10
- package/src/components/layout/Layout.tsx +9 -6
- package/src/components/workspace-template-card.tsx +11 -1
- package/src/server/lib/app-creation-store.spec.ts +252 -0
- package/src/server/lib/app-creation-store.ts +148 -8
- package/src/server/plugins/integrations.ts +1 -1
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
generateWorkspaceAppDescription,
|
|
6
6
|
listAvailableWorkspaceTemplates,
|
|
7
7
|
listWorkspaceApps,
|
|
8
|
+
setAppCreationSettings,
|
|
9
|
+
startWorkspaceAppCreation,
|
|
8
10
|
updateWorkspaceAppMetadata,
|
|
9
11
|
} from "./app-creation-store.js";
|
|
10
12
|
|
|
@@ -28,6 +30,35 @@ const mocks = vi.hoisted(() => {
|
|
|
28
30
|
rows: state.orgRole ? [{ role: state.orgRole }] : [],
|
|
29
31
|
})),
|
|
30
32
|
})),
|
|
33
|
+
resolveBuilderCredentialsDetailed: vi.fn(async () => ({
|
|
34
|
+
privateKey: null as string | null,
|
|
35
|
+
publicKey: null as string | null,
|
|
36
|
+
userId: null as string | null,
|
|
37
|
+
orgName: null,
|
|
38
|
+
orgKind: null,
|
|
39
|
+
subscription: null,
|
|
40
|
+
subscriptionLevel: null,
|
|
41
|
+
subscriptionName: null,
|
|
42
|
+
isEnterprise: null,
|
|
43
|
+
isFreeAccount: null,
|
|
44
|
+
source: null,
|
|
45
|
+
lookupFailed: false,
|
|
46
|
+
})),
|
|
47
|
+
runBuilderAgent: vi.fn(),
|
|
48
|
+
resolveBuilderBranchProjectId: vi.fn(async () => ""),
|
|
49
|
+
getBuilderBranchProjectId: vi.fn(() => ""),
|
|
50
|
+
writeAppSecret: vi.fn(async () => "secret-id"),
|
|
51
|
+
deleteAppSecret: vi.fn(async () => true),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
vi.mock("@agent-native/core/secrets", async (importOriginal) => {
|
|
56
|
+
const actual =
|
|
57
|
+
await importOriginal<typeof import("@agent-native/core/secrets")>();
|
|
58
|
+
return {
|
|
59
|
+
...actual,
|
|
60
|
+
writeAppSecret: (...args: any[]) => mocks.writeAppSecret(...args),
|
|
61
|
+
deleteAppSecret: (...args: any[]) => mocks.deleteAppSecret(...args),
|
|
31
62
|
};
|
|
32
63
|
});
|
|
33
64
|
|
|
@@ -44,6 +75,21 @@ vi.mock("@agent-native/core/settings", () => ({
|
|
|
44
75
|
putSetting: (...args: any[]) => mocks.putSetting(...args),
|
|
45
76
|
}));
|
|
46
77
|
|
|
78
|
+
vi.mock("@agent-native/core/server", async (importOriginal) => {
|
|
79
|
+
const actual =
|
|
80
|
+
await importOriginal<typeof import("@agent-native/core/server")>();
|
|
81
|
+
return {
|
|
82
|
+
...actual,
|
|
83
|
+
resolveBuilderCredentialsDetailed: (...args: any[]) =>
|
|
84
|
+
mocks.resolveBuilderCredentialsDetailed(...args),
|
|
85
|
+
runBuilderAgent: (...args: any[]) => mocks.runBuilderAgent(...args),
|
|
86
|
+
resolveBuilderBranchProjectId: (...args: any[]) =>
|
|
87
|
+
mocks.resolveBuilderBranchProjectId(...args),
|
|
88
|
+
getBuilderBranchProjectId: (...args: any[]) =>
|
|
89
|
+
mocks.getBuilderBranchProjectId(...args),
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
47
93
|
vi.mock("./dispatch-store.js", async (importOriginal) => {
|
|
48
94
|
const actual = await importOriginal<typeof import("./dispatch-store.js")>();
|
|
49
95
|
return {
|
|
@@ -58,6 +104,22 @@ afterEach(() => {
|
|
|
58
104
|
vi.clearAllMocks();
|
|
59
105
|
mocks.settings.clear();
|
|
60
106
|
mocks.state.orgRole = "admin";
|
|
107
|
+
mocks.resolveBuilderCredentialsDetailed.mockResolvedValue({
|
|
108
|
+
privateKey: null,
|
|
109
|
+
publicKey: null,
|
|
110
|
+
userId: null,
|
|
111
|
+
orgName: null,
|
|
112
|
+
orgKind: null,
|
|
113
|
+
subscription: null,
|
|
114
|
+
subscriptionLevel: null,
|
|
115
|
+
subscriptionName: null,
|
|
116
|
+
isEnterprise: null,
|
|
117
|
+
isFreeAccount: null,
|
|
118
|
+
source: null,
|
|
119
|
+
lookupFailed: false,
|
|
120
|
+
});
|
|
121
|
+
mocks.resolveBuilderBranchProjectId.mockResolvedValue("");
|
|
122
|
+
mocks.getBuilderBranchProjectId.mockReturnValue("");
|
|
61
123
|
globalThis.fetch = originalFetch;
|
|
62
124
|
});
|
|
63
125
|
|
|
@@ -377,3 +439,193 @@ describe("listWorkspaceApps", () => {
|
|
|
377
439
|
expect(templates).toEqual([]);
|
|
378
440
|
});
|
|
379
441
|
});
|
|
442
|
+
|
|
443
|
+
describe("startWorkspaceAppCreation", () => {
|
|
444
|
+
const leakedProjectId = "940ebc5a83164aa6a37dde445e494f3a";
|
|
445
|
+
|
|
446
|
+
function stubHostedRuntime() {
|
|
447
|
+
vi.stubEnv("NODE_ENV", "production");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function stubBuilderProjectConfigured() {
|
|
451
|
+
vi.stubEnv("DISPATCH_BUILDER_PROJECT_ID", leakedProjectId);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function credentials(overrides: Record<string, unknown> = {}) {
|
|
455
|
+
return {
|
|
456
|
+
privateKey: null,
|
|
457
|
+
publicKey: null,
|
|
458
|
+
userId: null,
|
|
459
|
+
orgName: null,
|
|
460
|
+
orgKind: null,
|
|
461
|
+
subscription: null,
|
|
462
|
+
subscriptionLevel: null,
|
|
463
|
+
subscriptionName: null,
|
|
464
|
+
isEnterprise: null,
|
|
465
|
+
isFreeAccount: null,
|
|
466
|
+
source: null,
|
|
467
|
+
lookupFailed: false,
|
|
468
|
+
...overrides,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function create(appId = "onboarding") {
|
|
473
|
+
return runWithRequestContext({ userEmail: "dev@example.test" }, () =>
|
|
474
|
+
startWorkspaceAppCreation({ prompt: "Track onboarding tasks", appId }),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
it("returns builder-not-connected without leaking the project id when no Builder credentials are configured", async () => {
|
|
479
|
+
stubHostedRuntime();
|
|
480
|
+
stubBuilderProjectConfigured();
|
|
481
|
+
mocks.resolveBuilderCredentialsDetailed.mockResolvedValue(credentials());
|
|
482
|
+
|
|
483
|
+
const result = (await create()) as any;
|
|
484
|
+
|
|
485
|
+
expect(result.mode).toBe("builder-unavailable");
|
|
486
|
+
expect(result.reason).toBe("builder-not-connected");
|
|
487
|
+
expect(result.message).not.toContain(leakedProjectId);
|
|
488
|
+
expect(mocks.runBuilderAgent).not.toHaveBeenCalled();
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("returns credential-store-unavailable when the credential lookup itself fails", async () => {
|
|
492
|
+
stubHostedRuntime();
|
|
493
|
+
stubBuilderProjectConfigured();
|
|
494
|
+
mocks.resolveBuilderCredentialsDetailed.mockResolvedValue(
|
|
495
|
+
credentials({ lookupFailed: true }),
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
const result = (await create()) as any;
|
|
499
|
+
|
|
500
|
+
expect(result.mode).toBe("builder-unavailable");
|
|
501
|
+
expect(result.reason).toBe("credential-store-unavailable");
|
|
502
|
+
expect(mocks.runBuilderAgent).not.toHaveBeenCalled();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("returns builder-error with the raw failure in detail when runBuilderAgent throws", async () => {
|
|
506
|
+
stubHostedRuntime();
|
|
507
|
+
stubBuilderProjectConfigured();
|
|
508
|
+
mocks.resolveBuilderCredentialsDetailed.mockResolvedValue(
|
|
509
|
+
credentials({
|
|
510
|
+
privateKey: "priv",
|
|
511
|
+
publicKey: "pub",
|
|
512
|
+
userId: "builder-user-1",
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
mocks.runBuilderAgent.mockRejectedValue(
|
|
516
|
+
new Error("Builder keys are not configured"),
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const result = (await create()) as any;
|
|
520
|
+
|
|
521
|
+
expect(result.mode).toBe("builder-unavailable");
|
|
522
|
+
expect(result.reason).toBe("builder-error");
|
|
523
|
+
expect(result.detail).toBe("Builder keys are not configured");
|
|
524
|
+
expect(result.message).not.toContain(leakedProjectId);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("starts the Builder branch and passes the resolved userId through", async () => {
|
|
528
|
+
stubHostedRuntime();
|
|
529
|
+
stubBuilderProjectConfigured();
|
|
530
|
+
mocks.resolveBuilderCredentialsDetailed.mockResolvedValue(
|
|
531
|
+
credentials({
|
|
532
|
+
privateKey: "priv",
|
|
533
|
+
publicKey: "pub",
|
|
534
|
+
userId: "builder-user-42",
|
|
535
|
+
}),
|
|
536
|
+
);
|
|
537
|
+
mocks.runBuilderAgent.mockResolvedValue({
|
|
538
|
+
branchName: "onboarding1",
|
|
539
|
+
url: "https://builder.io/app/projects/project-1/branch/onboarding1",
|
|
540
|
+
status: "processing",
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
const result = (await create()) as any;
|
|
544
|
+
|
|
545
|
+
expect(result.mode).toBe("builder");
|
|
546
|
+
expect(mocks.runBuilderAgent).toHaveBeenCalledWith(
|
|
547
|
+
expect.objectContaining({ userId: "builder-user-42" }),
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("returns coming-soon when no Builder project is configured", async () => {
|
|
552
|
+
stubHostedRuntime();
|
|
553
|
+
|
|
554
|
+
const result = (await create()) as any;
|
|
555
|
+
|
|
556
|
+
expect(result.mode).toBe("coming-soon");
|
|
557
|
+
expect(mocks.resolveBuilderCredentialsDetailed).not.toHaveBeenCalled();
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
describe("setAppCreationSettings", () => {
|
|
562
|
+
const projectId = "274d28fec94b48f2b2d68f2274d390eb";
|
|
563
|
+
const orgId = "builder_io";
|
|
564
|
+
|
|
565
|
+
function save(
|
|
566
|
+
builderProjectId: string | null,
|
|
567
|
+
ctx: { userEmail: string; orgId?: string } = {
|
|
568
|
+
userEmail: "dev@example.test",
|
|
569
|
+
orgId,
|
|
570
|
+
},
|
|
571
|
+
) {
|
|
572
|
+
return runWithRequestContext(ctx, () =>
|
|
573
|
+
setAppCreationSettings({ builderProjectId }),
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
it("stores the project id as an org-scoped credential so member apps resolve it", async () => {
|
|
578
|
+
await save(projectId);
|
|
579
|
+
|
|
580
|
+
expect(mocks.writeAppSecret).toHaveBeenCalledWith(
|
|
581
|
+
expect.objectContaining({
|
|
582
|
+
key: "BUILDER_BRANCH_PROJECT_ID",
|
|
583
|
+
value: projectId,
|
|
584
|
+
scope: "org",
|
|
585
|
+
scopeId: orgId,
|
|
586
|
+
}),
|
|
587
|
+
);
|
|
588
|
+
expect(mocks.deleteAppSecret).not.toHaveBeenCalled();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("scopes the credential to one organization rather than every tenant", async () => {
|
|
592
|
+
await save(projectId);
|
|
593
|
+
|
|
594
|
+
const [args] = mocks.writeAppSecret.mock.calls.at(-1) as [
|
|
595
|
+
{ scope: string; scopeId: string },
|
|
596
|
+
];
|
|
597
|
+
expect(args.scope).not.toBe("user");
|
|
598
|
+
expect(args.scopeId).toBe(orgId);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("falls back to a solo workspace scope when there is no active org", async () => {
|
|
602
|
+
await save(projectId, { userEmail: "dev@example.test" });
|
|
603
|
+
|
|
604
|
+
expect(mocks.writeAppSecret).toHaveBeenCalledWith(
|
|
605
|
+
expect.objectContaining({
|
|
606
|
+
scope: "workspace",
|
|
607
|
+
scopeId: "solo:dev@example.test",
|
|
608
|
+
}),
|
|
609
|
+
);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it("removes the credential when the project id is cleared", async () => {
|
|
613
|
+
await save(null);
|
|
614
|
+
|
|
615
|
+
expect(mocks.deleteAppSecret).toHaveBeenCalledWith({
|
|
616
|
+
key: "BUILDER_BRANCH_PROJECT_ID",
|
|
617
|
+
scope: "org",
|
|
618
|
+
scopeId: orgId,
|
|
619
|
+
});
|
|
620
|
+
expect(mocks.writeAppSecret).not.toHaveBeenCalled();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it("does not save the setting when the credential write fails", async () => {
|
|
624
|
+
mocks.writeAppSecret.mockRejectedValueOnce(
|
|
625
|
+
new Error("credential store down"),
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
await expect(save(projectId)).rejects.toThrow("credential store down");
|
|
629
|
+
expect(mocks.putSetting).not.toHaveBeenCalled();
|
|
630
|
+
});
|
|
631
|
+
});
|
|
@@ -4,12 +4,17 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
import { getDbExec } from "@agent-native/core/db";
|
|
7
|
+
import {
|
|
8
|
+
deleteAppSecret,
|
|
9
|
+
writeAppSecret,
|
|
10
|
+
type SecretScope,
|
|
11
|
+
} from "@agent-native/core/secrets";
|
|
7
12
|
import {
|
|
8
13
|
getBuilderBranchProjectId,
|
|
9
14
|
getRequestContext,
|
|
10
15
|
isIntegrationCallerRequest,
|
|
11
16
|
resolveBuilderBranchProjectId,
|
|
12
|
-
|
|
17
|
+
resolveBuilderCredentialsDetailed,
|
|
13
18
|
runBuilderAgent,
|
|
14
19
|
} from "@agent-native/core/server";
|
|
15
20
|
import { getSetting, putSetting } from "@agent-native/core/settings";
|
|
@@ -30,6 +35,9 @@ import {
|
|
|
30
35
|
} from "./workspace-resources-store.js";
|
|
31
36
|
|
|
32
37
|
const SETTINGS_KEY = "dispatch-app-creation-settings";
|
|
38
|
+
const BUILDER_BRANCH_PROJECT_SECRET_KEY = "BUILDER_BRANCH_PROJECT_ID";
|
|
39
|
+
const BUILDER_BRANCH_PROJECT_SECRET_DESCRIPTION =
|
|
40
|
+
"Builder project for cloud code-change branches (set in Dispatch)";
|
|
33
41
|
const WORKSPACE_APP_METADATA_SETTINGS_KEY = "workspace-app-metadata";
|
|
34
42
|
const WORKSPACE_APPS_ENV_KEY = "AGENT_NATIVE_WORKSPACE_APPS_JSON";
|
|
35
43
|
const WORKSPACE_APPS_MANIFEST_FILE = "workspace-apps.json";
|
|
@@ -209,6 +217,16 @@ function scopedSettingsKey(): string {
|
|
|
209
217
|
return `${SETTINGS_KEY}:user:${currentOwnerEmail()}`;
|
|
210
218
|
}
|
|
211
219
|
|
|
220
|
+
function builderProjectSecretTarget(): {
|
|
221
|
+
scope: Extract<SecretScope, "org" | "workspace">;
|
|
222
|
+
scopeId: string;
|
|
223
|
+
} | null {
|
|
224
|
+
const orgId = currentOrgId();
|
|
225
|
+
if (orgId) return { scope: "org", scopeId: orgId };
|
|
226
|
+
const email = currentOwnerEmail();
|
|
227
|
+
return email ? { scope: "workspace", scopeId: `solo:${email}` } : null;
|
|
228
|
+
}
|
|
229
|
+
|
|
212
230
|
function workspaceAppMetadataSettingsKey(): string {
|
|
213
231
|
const orgId = currentOrgId();
|
|
214
232
|
if (orgId) return `${WORKSPACE_APP_METADATA_SETTINGS_KEY}:org:${orgId}`;
|
|
@@ -1500,6 +1518,29 @@ export async function setAppCreationSettings(input: {
|
|
|
1500
1518
|
await assertCanManageAppCreationSettings();
|
|
1501
1519
|
const builderProjectId = input.builderProjectId?.trim() || null;
|
|
1502
1520
|
const raw = await readSettingsRecord();
|
|
1521
|
+
|
|
1522
|
+
// The credential store, not this settings row, is what
|
|
1523
|
+
// `resolveBuilderBranchProjectId()` reads. Write it first: a saved setting
|
|
1524
|
+
// whose secret never landed reports the project as configured while cloud
|
|
1525
|
+
// code changes stay silently disabled.
|
|
1526
|
+
const secretTarget = builderProjectSecretTarget();
|
|
1527
|
+
if (secretTarget) {
|
|
1528
|
+
const ref = {
|
|
1529
|
+
key: BUILDER_BRANCH_PROJECT_SECRET_KEY,
|
|
1530
|
+
scope: secretTarget.scope,
|
|
1531
|
+
scopeId: secretTarget.scopeId,
|
|
1532
|
+
};
|
|
1533
|
+
if (builderProjectId) {
|
|
1534
|
+
await writeAppSecret({
|
|
1535
|
+
...ref,
|
|
1536
|
+
value: builderProjectId,
|
|
1537
|
+
description: BUILDER_BRANCH_PROJECT_SECRET_DESCRIPTION,
|
|
1538
|
+
});
|
|
1539
|
+
} else {
|
|
1540
|
+
await deleteAppSecret(ref);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1503
1544
|
await putSetting(scopedSettingsKey(), { ...raw, builderProjectId });
|
|
1504
1545
|
await recordAudit({
|
|
1505
1546
|
action: "settings.updated",
|
|
@@ -1649,7 +1690,7 @@ function normalizeBuilderRunResult(result: unknown): {
|
|
|
1649
1690
|
}
|
|
1650
1691
|
|
|
1651
1692
|
async function remoteAppCreationAuthorization(): Promise<
|
|
1652
|
-
{ ok: true } | { ok: false; message: string }
|
|
1693
|
+
{ ok: true } | { ok: false; reason: "identity-not-linked"; message: string }
|
|
1653
1694
|
> {
|
|
1654
1695
|
const ownerEmail = currentOwnerEmail();
|
|
1655
1696
|
const isIntegrationCaller = isIntegrationCallerRequest();
|
|
@@ -1658,6 +1699,7 @@ async function remoteAppCreationAuthorization(): Promise<
|
|
|
1658
1699
|
if (await defaultOwnerAppCreationAllowed()) return { ok: true };
|
|
1659
1700
|
return {
|
|
1660
1701
|
ok: false,
|
|
1702
|
+
reason: "identity-not-linked",
|
|
1661
1703
|
message:
|
|
1662
1704
|
"Messaging-triggered app creation is using the deployment default Dispatch owner. " +
|
|
1663
1705
|
"Link the messaging identity to a Dispatch user with /link, start the app from Dispatch while signed in, or explicitly set ENABLE_BUILDER=true for this deployment.",
|
|
@@ -1670,6 +1712,7 @@ async function remoteAppCreationAuthorization(): Promise<
|
|
|
1670
1712
|
: "Synthetic integration";
|
|
1671
1713
|
return {
|
|
1672
1714
|
ok: false,
|
|
1715
|
+
reason: "identity-not-linked",
|
|
1673
1716
|
message:
|
|
1674
1717
|
`${source} app creation needs a trusted Dispatch owner before Builder can start a branch. ` +
|
|
1675
1718
|
"Link the messaging identity to a Dispatch user with /link, start the app from Dispatch while signed in, or explicitly set ENABLE_BUILDER=true for this deployment.",
|
|
@@ -1788,6 +1831,65 @@ async function grantSelectedWorkspaceResources(input: {
|
|
|
1788
1831
|
await grantWorkspaceResourcesToApp(input);
|
|
1789
1832
|
}
|
|
1790
1833
|
|
|
1834
|
+
/**
|
|
1835
|
+
* Discriminates why `startWorkspaceAppCreation` could not hand off to Builder.
|
|
1836
|
+
* UIs and agents should branch on this instead of parsing `message` text.
|
|
1837
|
+
*/
|
|
1838
|
+
export type AppCreationUnavailableReason =
|
|
1839
|
+
| "identity-not-linked"
|
|
1840
|
+
| "builder-not-connected"
|
|
1841
|
+
| "credential-store-unavailable"
|
|
1842
|
+
| "builder-error";
|
|
1843
|
+
|
|
1844
|
+
export interface AppCreationIdentityUnavailableResult {
|
|
1845
|
+
mode: "builder-unavailable";
|
|
1846
|
+
appId: string;
|
|
1847
|
+
reason: "identity-not-linked";
|
|
1848
|
+
message: string;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
export interface AppCreationBuilderUnavailableResult {
|
|
1852
|
+
mode: "builder-unavailable";
|
|
1853
|
+
appId: string;
|
|
1854
|
+
reason: Exclude<AppCreationUnavailableReason, "identity-not-linked">;
|
|
1855
|
+
message: string;
|
|
1856
|
+
/** Raw underlying error text for agents/operators debugging the deployment. */
|
|
1857
|
+
detail?: string;
|
|
1858
|
+
projectId: string;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
export interface AppCreationLocalAgentResult {
|
|
1862
|
+
mode: "local-agent";
|
|
1863
|
+
appId: string;
|
|
1864
|
+
prompt: string;
|
|
1865
|
+
message: string;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
export interface AppCreationComingSoonResult {
|
|
1869
|
+
mode: "coming-soon";
|
|
1870
|
+
appId: string;
|
|
1871
|
+
message: string;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
export interface AppCreationBuilderResult {
|
|
1875
|
+
mode: "builder";
|
|
1876
|
+
appId: string;
|
|
1877
|
+
path: string;
|
|
1878
|
+
projectId: string;
|
|
1879
|
+
branchName: string;
|
|
1880
|
+
url: string;
|
|
1881
|
+
workspaceUrl: string | null;
|
|
1882
|
+
status: string;
|
|
1883
|
+
message: string;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
export type StartWorkspaceAppCreationResult =
|
|
1887
|
+
| AppCreationIdentityUnavailableResult
|
|
1888
|
+
| AppCreationBuilderUnavailableResult
|
|
1889
|
+
| AppCreationLocalAgentResult
|
|
1890
|
+
| AppCreationComingSoonResult
|
|
1891
|
+
| AppCreationBuilderResult;
|
|
1892
|
+
|
|
1791
1893
|
export async function startWorkspaceAppCreation(input: {
|
|
1792
1894
|
prompt: string;
|
|
1793
1895
|
appId?: string | null;
|
|
@@ -1795,7 +1897,7 @@ export async function startWorkspaceAppCreation(input: {
|
|
|
1795
1897
|
template?: string | null;
|
|
1796
1898
|
secretIds?: string[];
|
|
1797
1899
|
resourceIds?: string[];
|
|
1798
|
-
}) {
|
|
1900
|
+
}): Promise<StartWorkspaceAppCreationResult> {
|
|
1799
1901
|
const initial = buildWorkspaceAppPrompt({
|
|
1800
1902
|
prompt: input.prompt,
|
|
1801
1903
|
appId: input.appId,
|
|
@@ -1811,6 +1913,7 @@ export async function startWorkspaceAppCreation(input: {
|
|
|
1811
1913
|
return {
|
|
1812
1914
|
mode: "builder-unavailable",
|
|
1813
1915
|
appId: initial.appId,
|
|
1916
|
+
reason: authorization.reason,
|
|
1814
1917
|
message: authorization.message,
|
|
1815
1918
|
};
|
|
1816
1919
|
}
|
|
@@ -1866,14 +1969,51 @@ export async function startWorkspaceAppCreation(input: {
|
|
|
1866
1969
|
};
|
|
1867
1970
|
}
|
|
1868
1971
|
|
|
1972
|
+
let builderCreds: Awaited<
|
|
1973
|
+
ReturnType<typeof resolveBuilderCredentialsDetailed>
|
|
1974
|
+
>;
|
|
1975
|
+
try {
|
|
1976
|
+
builderCreds = await resolveBuilderCredentialsDetailed();
|
|
1977
|
+
} catch {
|
|
1978
|
+
return {
|
|
1979
|
+
mode: "builder-unavailable",
|
|
1980
|
+
appId: built.appId,
|
|
1981
|
+
reason: "credential-store-unavailable",
|
|
1982
|
+
projectId: settings.builderProjectId,
|
|
1983
|
+
message:
|
|
1984
|
+
"Could not read your Builder connection just now. Try creating the app again in a moment.",
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
if (builderCreds.lookupFailed) {
|
|
1989
|
+
return {
|
|
1990
|
+
mode: "builder-unavailable",
|
|
1991
|
+
appId: built.appId,
|
|
1992
|
+
reason: "credential-store-unavailable",
|
|
1993
|
+
projectId: settings.builderProjectId,
|
|
1994
|
+
message:
|
|
1995
|
+
"Could not read your Builder connection just now. Try creating the app again in a moment.",
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
if (!builderCreds.privateKey || !builderCreds.publicKey) {
|
|
2000
|
+
return {
|
|
2001
|
+
mode: "builder-unavailable",
|
|
2002
|
+
appId: built.appId,
|
|
2003
|
+
reason: "builder-not-connected",
|
|
2004
|
+
projectId: settings.builderProjectId,
|
|
2005
|
+
message: "Connect your Builder account to create apps from Dispatch.",
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
const builderUserId = builderCreds.userId || undefined;
|
|
2010
|
+
|
|
1869
2011
|
let result: {
|
|
1870
2012
|
branchName: string;
|
|
1871
2013
|
url: string;
|
|
1872
2014
|
status: string;
|
|
1873
2015
|
};
|
|
1874
2016
|
try {
|
|
1875
|
-
const builderCreds = await resolveBuilderCredentials().catch(() => null);
|
|
1876
|
-
const builderUserId = builderCreds?.userId || undefined;
|
|
1877
2017
|
result = normalizeBuilderRunResult(
|
|
1878
2018
|
await runBuilderAgent({
|
|
1879
2019
|
prompt,
|
|
@@ -1891,11 +2031,11 @@ export async function startWorkspaceAppCreation(input: {
|
|
|
1891
2031
|
return {
|
|
1892
2032
|
mode: "builder-unavailable",
|
|
1893
2033
|
appId: built.appId,
|
|
2034
|
+
reason: "builder-error",
|
|
1894
2035
|
projectId: settings.builderProjectId,
|
|
2036
|
+
detail,
|
|
1895
2037
|
message:
|
|
1896
|
-
|
|
1897
|
-
`but it could not start yet: ${detail}. Connect Builder for this user, ` +
|
|
1898
|
-
`link the messaging identity to that user, or configure deployment-managed Builder credentials for this workspace.`,
|
|
2038
|
+
"Builder could not start the app branch. This is usually temporary — try again.",
|
|
1899
2039
|
};
|
|
1900
2040
|
}
|
|
1901
2041
|
|
|
@@ -42,7 +42,7 @@ When a user asks for something:
|
|
|
42
42
|
- If the user asks to create, build, make, scaffold, or generate an "agent" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.
|
|
43
43
|
- If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized fork/copy.
|
|
44
44
|
- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind.
|
|
45
|
-
- If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode "builder", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode "local-agent", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode "coming-soon", say this requires a code change and they can edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like; do not send them to Builder org/beta settings. If it returns mode "builder-unavailable",
|
|
45
|
+
- If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode "builder", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode "local-agent", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode "coming-soon", say this requires a code change and they can edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like; do not send them to Builder org/beta settings. If it returns mode "builder-unavailable", the action also returns a \`reason\` code plus a user-facing \`message\` and (for some reasons) an operator-facing \`detail\`. Relay the \`message\` to the user as-is; only surface \`detail\` when the user is clearly an operator debugging the deployment (e.g. troubleshooting a broken Builder connection for the workspace), never as routine detail for an end user.
|
|
46
46
|
- For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.
|
|
47
47
|
- Keep responses concise and operational — messaging platforms have character limits.
|
|
48
48
|
- Use markdown sparingly (bold and lists are fine, avoid complex formatting).
|