@agent-native/dispatch 0.36.0-nightly-20260909183543 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/apply-dream-proposal.js +2 -1
- package/dist/actions/apply-dream-proposal.js.map +1 -1
- package/dist/actions/approve-dispatch-change.js +2 -0
- package/dist/actions/approve-dispatch-change.js.map +1 -1
- package/dist/actions/create-pylon-ticket.js +2 -0
- package/dist/actions/create-pylon-ticket.js.map +1 -1
- package/dist/actions/create-workspace-resource-grant.js +2 -0
- package/dist/actions/create-workspace-resource-grant.js.map +1 -1
- package/dist/actions/create-workspace-resource.js +2 -0
- package/dist/actions/create-workspace-resource.js.map +1 -1
- package/dist/actions/delete-destination.js +2 -0
- package/dist/actions/delete-destination.js.map +1 -1
- package/dist/actions/delete-workspace-resource.js +2 -0
- package/dist/actions/delete-workspace-resource.js.map +1 -1
- package/dist/actions/ensure-dream-job.js +2 -0
- package/dist/actions/ensure-dream-job.js.map +1 -1
- package/dist/actions/grant-workspace-resources-to-app.js +2 -0
- package/dist/actions/grant-workspace-resources-to-app.js.map +1 -1
- package/dist/actions/import-agent-pack.d.ts.map +1 -1
- package/dist/actions/import-agent-pack.js +2 -0
- package/dist/actions/import-agent-pack.js.map +1 -1
- package/dist/actions/import-agent.d.ts.map +1 -1
- package/dist/actions/import-agent.js +2 -0
- package/dist/actions/import-agent.js.map +1 -1
- package/dist/actions/reject-dispatch-change.js +2 -0
- package/dist/actions/reject-dispatch-change.js.map +1 -1
- package/dist/actions/reject-dream-proposal.js +2 -1
- package/dist/actions/reject-dream-proposal.js.map +1 -1
- package/dist/actions/restore-starter-workspace-resources.js +2 -0
- package/dist/actions/restore-starter-workspace-resources.js.map +1 -1
- package/dist/actions/revoke-workspace-resource-grant.js +2 -0
- package/dist/actions/revoke-workspace-resource-grant.js.map +1 -1
- package/dist/actions/send-platform-message.js +2 -0
- package/dist/actions/send-platform-message.js.map +1 -1
- package/dist/actions/set-dispatch-approval-policy.js +2 -0
- package/dist/actions/set-dispatch-approval-policy.js.map +1 -1
- package/dist/actions/set-dream-settings.js +2 -0
- package/dist/actions/set-dream-settings.js.map +1 -1
- package/dist/actions/update-workspace-resource.js +2 -0
- package/dist/actions/update-workspace-resource.js.map +1 -1
- package/dist/actions/upsert-destination.d.ts +12 -12
- package/dist/actions/upsert-destination.js +2 -0
- package/dist/actions/upsert-destination.js.map +1 -1
- package/dist/components/dispatch-access.d.ts +1 -1
- package/dist/components/dispatch-access.d.ts.map +1 -1
- package/dist/components/dispatch-access.js +0 -9
- package/dist/components/dispatch-access.js.map +1 -1
- package/dist/server/lib/app-creation-store.js +1 -1
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/app-roles.d.ts +7 -0
- package/dist/server/lib/app-roles.d.ts.map +1 -0
- package/dist/server/lib/app-roles.js +45 -0
- package/dist/server/lib/app-roles.js.map +1 -0
- package/dist/server/lib/dispatch-store.d.ts.map +1 -1
- package/dist/server/lib/dispatch-store.js +1 -1
- package/dist/server/lib/dispatch-store.js.map +1 -1
- package/dist/server/lib/dreams-store.d.ts +4 -0
- package/dist/server/lib/dreams-store.d.ts.map +1 -1
- package/dist/server/lib/dreams-store.js +22 -1
- package/dist/server/lib/dreams-store.js.map +1 -1
- package/package.json +4 -4
- package/src/actions/apply-dream-proposal.ts +5 -1
- package/src/actions/approve-dispatch-change.ts +2 -0
- package/src/actions/create-pylon-ticket.ts +2 -0
- package/src/actions/create-workspace-resource-grant.ts +2 -0
- package/src/actions/create-workspace-resource.ts +2 -0
- package/src/actions/delete-destination.ts +2 -0
- package/src/actions/delete-workspace-resource.ts +2 -0
- package/src/actions/dispatch-admin-authorization.spec.ts +114 -0
- package/src/actions/ensure-dream-job.ts +2 -0
- package/src/actions/grant-workspace-resources-to-app.ts +2 -0
- package/src/actions/import-agent-pack.ts +2 -0
- package/src/actions/import-agent.ts +2 -0
- package/src/actions/reject-dispatch-change.ts +2 -0
- package/src/actions/reject-dream-proposal.ts +5 -1
- package/src/actions/restore-starter-workspace-resources.ts +2 -0
- package/src/actions/revoke-workspace-resource-grant.ts +2 -0
- package/src/actions/send-platform-message.spec.ts +4 -0
- package/src/actions/send-platform-message.ts +2 -0
- package/src/actions/set-dispatch-approval-policy.ts +2 -0
- package/src/actions/set-dream-settings.spec.ts +5 -0
- package/src/actions/set-dream-settings.ts +2 -0
- package/src/actions/update-workspace-resource.ts +2 -0
- package/src/actions/upsert-destination.ts +2 -0
- package/src/components/dispatch-access.spec.tsx +1 -63
- package/src/components/dispatch-access.tsx +0 -8
- package/src/server/lib/app-creation-store.spec.ts +21 -22
- package/src/server/lib/app-creation-store.ts +1 -1
- package/src/server/lib/app-roles.spec.ts +120 -0
- package/src/server/lib/app-roles.ts +67 -0
- package/src/server/lib/approval-fencing.spec.ts +39 -0
- package/src/server/lib/dispatch-store.ts +4 -1
- package/src/server/lib/dreams-store.spec.ts +78 -0
- package/src/server/lib/dreams-store.ts +37 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
assertAny: vi.fn(),
|
|
5
|
+
getRequestOrgId: vi.fn(),
|
|
6
|
+
getRequestUserEmail: vi.fn(),
|
|
7
|
+
isStandaloneDispatchRuntime: vi.fn(),
|
|
8
|
+
validateFederatedOrganizationMembershipForCurrentRequest: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("@agent-native/core/org", () => ({
|
|
12
|
+
defineAppRoles: () => ({ assertAny: mocks.assertAny }),
|
|
13
|
+
isMissingOrganizationTableError: (error: unknown) =>
|
|
14
|
+
/(?:organizations|org_members).*does not exist/i.test(String(error)),
|
|
15
|
+
isStandaloneDispatchRuntime: mocks.isStandaloneDispatchRuntime,
|
|
16
|
+
validateFederatedOrganizationMembershipForCurrentRequest:
|
|
17
|
+
mocks.validateFederatedOrganizationMembershipForCurrentRequest,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("@agent-native/core/server", () => ({
|
|
21
|
+
getRequestOrgId: mocks.getRequestOrgId,
|
|
22
|
+
getRequestUserEmail: mocks.getRequestUserEmail,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
import { ForbiddenError } from "@agent-native/core/sharing";
|
|
26
|
+
|
|
27
|
+
import { authorizeDispatchAdmin } from "./app-roles.js";
|
|
28
|
+
|
|
29
|
+
const context = {
|
|
30
|
+
caller: "http" as const,
|
|
31
|
+
orgId: "org-1",
|
|
32
|
+
userEmail: "member@example.test",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe("authorizeDispatchAdmin", () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
mocks.validateFederatedOrganizationMembershipForCurrentRequest.mockResolvedValue(
|
|
39
|
+
{ active: true, role: "member" },
|
|
40
|
+
);
|
|
41
|
+
mocks.assertAny.mockResolvedValue("admin");
|
|
42
|
+
mocks.getRequestOrgId.mockReturnValue(undefined);
|
|
43
|
+
mocks.getRequestUserEmail.mockReturnValue(undefined);
|
|
44
|
+
mocks.isStandaloneDispatchRuntime.mockReturnValue(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("denies an organization member without the Dispatch admin role", async () => {
|
|
48
|
+
mocks.assertAny.mockRejectedValue(
|
|
49
|
+
new ForbiddenError("Requires dispatch role admin"),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
await expect(authorizeDispatchAdmin({}, context)).rejects.toThrow(
|
|
53
|
+
"Requires dispatch role admin",
|
|
54
|
+
);
|
|
55
|
+
expect(
|
|
56
|
+
mocks.validateFederatedOrganizationMembershipForCurrentRequest,
|
|
57
|
+
).toHaveBeenCalledWith({
|
|
58
|
+
orgId: "org-1",
|
|
59
|
+
email: "member@example.test",
|
|
60
|
+
});
|
|
61
|
+
expect(mocks.assertAny).toHaveBeenCalledWith(["admin"], {
|
|
62
|
+
orgId: "org-1",
|
|
63
|
+
userEmail: "member@example.test",
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("allows an organization admin without an app-role assignment", async () => {
|
|
68
|
+
mocks.validateFederatedOrganizationMembershipForCurrentRequest.mockResolvedValue(
|
|
69
|
+
{ active: true, role: "admin" },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await expect(authorizeDispatchAdmin({}, context)).resolves.toBeUndefined();
|
|
73
|
+
expect(mocks.assertAny).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("denies a stale linked organization admin", async () => {
|
|
77
|
+
mocks.validateFederatedOrganizationMembershipForCurrentRequest.mockResolvedValue(
|
|
78
|
+
{ active: false, role: null },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await expect(authorizeDispatchAdmin({}, context)).rejects.toThrow(
|
|
82
|
+
"active organization membership",
|
|
83
|
+
);
|
|
84
|
+
expect(mocks.assertAny).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("allows standalone administration when the org schema is absent", async () => {
|
|
88
|
+
mocks.isStandaloneDispatchRuntime.mockReturnValue(true);
|
|
89
|
+
mocks.validateFederatedOrganizationMembershipForCurrentRequest.mockRejectedValue(
|
|
90
|
+
new Error('relation "org_members" does not exist'),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
await expect(authorizeDispatchAdmin({}, context)).resolves.toBeUndefined();
|
|
94
|
+
expect(mocks.assertAny).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("allows a member with the Dispatch admin role", async () => {
|
|
98
|
+
await expect(authorizeDispatchAdmin({}, context)).resolves.toBeUndefined();
|
|
99
|
+
expect(mocks.assertAny).toHaveBeenCalledWith(["admin"], {
|
|
100
|
+
orgId: "org-1",
|
|
101
|
+
userEmail: "member@example.test",
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("allows authenticated personal-mode administration", async () => {
|
|
106
|
+
await expect(
|
|
107
|
+
authorizeDispatchAdmin({}, { ...context, orgId: null }),
|
|
108
|
+
).resolves.toBeUndefined();
|
|
109
|
+
expect(
|
|
110
|
+
mocks.validateFederatedOrganizationMembershipForCurrentRequest,
|
|
111
|
+
).not.toHaveBeenCalled();
|
|
112
|
+
expect(mocks.assertAny).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("denies an unauthenticated caller", async () => {
|
|
116
|
+
await expect(
|
|
117
|
+
authorizeDispatchAdmin({}, { ...context, userEmail: undefined }),
|
|
118
|
+
).rejects.toBeInstanceOf(ForbiddenError);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ActionRunContext } from "@agent-native/core/action";
|
|
2
|
+
import {
|
|
3
|
+
defineAppRoles,
|
|
4
|
+
isMissingOrganizationTableError,
|
|
5
|
+
isStandaloneDispatchRuntime,
|
|
6
|
+
type AppRoles,
|
|
7
|
+
validateFederatedOrganizationMembershipForCurrentRequest,
|
|
8
|
+
} from "@agent-native/core/org";
|
|
9
|
+
import {
|
|
10
|
+
getRequestOrgId,
|
|
11
|
+
getRequestUserEmail,
|
|
12
|
+
} from "@agent-native/core/server";
|
|
13
|
+
import { ForbiddenError } from "@agent-native/core/sharing";
|
|
14
|
+
|
|
15
|
+
import { dispatchAccessDescriptor } from "../../shared/app-roles.js";
|
|
16
|
+
|
|
17
|
+
let dispatchAccess: AppRoles<"admin"> | undefined;
|
|
18
|
+
|
|
19
|
+
function getDispatchAccess(): AppRoles<"admin"> {
|
|
20
|
+
return (dispatchAccess ??= defineAppRoles(dispatchAccessDescriptor));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Keep the Dispatch shell open to every signed-in member while protecting
|
|
25
|
+
* workspace-wide administration operations for org or Dispatch admins.
|
|
26
|
+
*/
|
|
27
|
+
export async function authorizeDispatchAdmin(
|
|
28
|
+
_args: unknown,
|
|
29
|
+
ctx?: ActionRunContext,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const email =
|
|
32
|
+
ctx?.userEmail !== undefined ? ctx.userEmail : getRequestUserEmail();
|
|
33
|
+
const orgId = ctx?.orgId !== undefined ? ctx.orgId : getRequestOrgId();
|
|
34
|
+
if (!email?.trim()) {
|
|
35
|
+
throw new ForbiddenError(
|
|
36
|
+
"Dispatch administration requires an authenticated user.",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (!orgId?.trim()) return;
|
|
40
|
+
let membership;
|
|
41
|
+
try {
|
|
42
|
+
membership = await validateFederatedOrganizationMembershipForCurrentRequest(
|
|
43
|
+
{
|
|
44
|
+
orgId,
|
|
45
|
+
email,
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (
|
|
50
|
+
isMissingOrganizationTableError(error) &&
|
|
51
|
+
isStandaloneDispatchRuntime()
|
|
52
|
+
) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
if (!membership.active) {
|
|
58
|
+
throw new ForbiddenError(
|
|
59
|
+
"Dispatch administration requires active organization membership.",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (membership.role === "owner" || membership.role === "admin") return;
|
|
63
|
+
await getDispatchAccess().assertAny(["admin"], {
|
|
64
|
+
userEmail: email,
|
|
65
|
+
orgId,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -340,6 +340,45 @@ describe("dispatch approval request status fencing", () => {
|
|
|
340
340
|
});
|
|
341
341
|
});
|
|
342
342
|
|
|
343
|
+
it("does not let the same email approve a request from another organization", async () => {
|
|
344
|
+
const [{ runWithRequestContext }, { getDbExec }, dispatchStore] =
|
|
345
|
+
await Promise.all([
|
|
346
|
+
import("@agent-native/core/server"),
|
|
347
|
+
import("@agent-native/core/db"),
|
|
348
|
+
import("./dispatch-store.js"),
|
|
349
|
+
]);
|
|
350
|
+
const exec = getDbExec();
|
|
351
|
+
|
|
352
|
+
const requestId = await runWithRequestContext(
|
|
353
|
+
{ userEmail: ownerEmail, orgId },
|
|
354
|
+
async () => {
|
|
355
|
+
const created = await dispatchStore.createApprovalRequest({
|
|
356
|
+
changeType: "approval-policy.update",
|
|
357
|
+
targetType: "dispatch-settings",
|
|
358
|
+
targetId: "dispatch-approval-policy",
|
|
359
|
+
summary: "Keep this request in its organization",
|
|
360
|
+
payload: { enabled: true, approverEmails: [] },
|
|
361
|
+
});
|
|
362
|
+
return (created as any).id as string;
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
await runWithRequestContext(
|
|
367
|
+
{ userEmail: ownerEmail, orgId: otherOrgId },
|
|
368
|
+
async () => {
|
|
369
|
+
await expect(dispatchStore.approveRequest(requestId)).rejects.toThrow(
|
|
370
|
+
"Approval request not found",
|
|
371
|
+
);
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const rows = await exec.execute({
|
|
376
|
+
sql: "SELECT status FROM dispatch_approval_requests WHERE id = ?",
|
|
377
|
+
args: [requestId],
|
|
378
|
+
});
|
|
379
|
+
expect(rows.rows[0]).toMatchObject({ status: "pending" });
|
|
380
|
+
});
|
|
381
|
+
|
|
343
382
|
it("does not let a caller from a different tenant reject another tenant's request", async () => {
|
|
344
383
|
const [{ runWithRequestContext }, { getDbExec }, dispatchStore] =
|
|
345
384
|
await Promise.all([
|
|
@@ -157,7 +157,10 @@ function ctxScope<T extends { ownerEmail: any; orgId: any }>(
|
|
|
157
157
|
if (!ctx.orgId) {
|
|
158
158
|
return and(eq(table.ownerEmail, ctx.ownerEmail), isNull(table.orgId));
|
|
159
159
|
}
|
|
160
|
-
return or(
|
|
160
|
+
return or(
|
|
161
|
+
and(eq(table.ownerEmail, ctx.ownerEmail), isNull(table.orgId)),
|
|
162
|
+
eq(table.orgId, ctx.orgId),
|
|
163
|
+
);
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
function id() {
|
|
@@ -4,6 +4,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
4
4
|
getDb: vi.fn(),
|
|
5
5
|
currentOwnerEmail: vi.fn(() => "owner@example.test"),
|
|
6
6
|
currentOrgId: vi.fn(() => null),
|
|
7
|
+
authorizeDispatchAdmin: vi.fn(),
|
|
7
8
|
getApprovalPolicy: vi.fn(),
|
|
8
9
|
createApprovalRequest: vi.fn(),
|
|
9
10
|
recordAudit: vi.fn(),
|
|
@@ -42,6 +43,10 @@ vi.mock("./dispatch-store.js", () => ({
|
|
|
42
43
|
recordAudit: mocks.recordAudit,
|
|
43
44
|
}));
|
|
44
45
|
|
|
46
|
+
vi.mock("./app-roles.js", () => ({
|
|
47
|
+
authorizeDispatchAdmin: mocks.authorizeDispatchAdmin,
|
|
48
|
+
}));
|
|
49
|
+
|
|
45
50
|
vi.mock("./thread-debug-store.js", () => ({
|
|
46
51
|
searchAgentThreads: mocks.searchAgentThreads,
|
|
47
52
|
getAgentThreadDebug: mocks.getAgentThreadDebug,
|
|
@@ -72,6 +77,7 @@ import { schema } from "../../db/index.js";
|
|
|
72
77
|
import {
|
|
73
78
|
applyApprovedDreamProposal,
|
|
74
79
|
applyDreamProposal,
|
|
80
|
+
authorizeDreamProposalMutation,
|
|
75
81
|
buildProposalInputs,
|
|
76
82
|
ensureDreamJob,
|
|
77
83
|
getDreamSettings,
|
|
@@ -301,6 +307,78 @@ afterEach(() => {
|
|
|
301
307
|
vi.clearAllMocks();
|
|
302
308
|
});
|
|
303
309
|
|
|
310
|
+
describe("authorizeDreamProposalMutation", () => {
|
|
311
|
+
it("allows an owner to change a personal-memory proposal", async () => {
|
|
312
|
+
mocks.getDb.mockReturnValue(createDbMock(pendingProposal()));
|
|
313
|
+
|
|
314
|
+
await expect(
|
|
315
|
+
authorizeDreamProposalMutation(
|
|
316
|
+
{ id: "proposal-1" },
|
|
317
|
+
{
|
|
318
|
+
caller: "http",
|
|
319
|
+
orgId: null,
|
|
320
|
+
userEmail: "owner@example.test",
|
|
321
|
+
},
|
|
322
|
+
),
|
|
323
|
+
).resolves.toBeUndefined();
|
|
324
|
+
expect(mocks.authorizeDispatchAdmin).not.toHaveBeenCalled();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("requires Dispatch admin authorization for shared proposals", async () => {
|
|
328
|
+
mocks.getDb.mockReturnValue(
|
|
329
|
+
createDbMock(
|
|
330
|
+
pendingProposal({
|
|
331
|
+
orgId: "org-1",
|
|
332
|
+
targetType: "shared-learnings",
|
|
333
|
+
targetPath: "LEARNINGS.md",
|
|
334
|
+
}),
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
mocks.authorizeDispatchAdmin.mockResolvedValue(undefined);
|
|
338
|
+
|
|
339
|
+
await expect(
|
|
340
|
+
authorizeDreamProposalMutation(
|
|
341
|
+
{ id: "proposal-1" },
|
|
342
|
+
{
|
|
343
|
+
caller: "http",
|
|
344
|
+
orgId: "org-1",
|
|
345
|
+
userEmail: "member@example.test",
|
|
346
|
+
},
|
|
347
|
+
),
|
|
348
|
+
).resolves.toBeUndefined();
|
|
349
|
+
expect(mocks.authorizeDispatchAdmin).toHaveBeenCalledWith(
|
|
350
|
+
{ id: "proposal-1" },
|
|
351
|
+
expect.objectContaining({
|
|
352
|
+
orgId: "org-1",
|
|
353
|
+
userEmail: "member@example.test",
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("does not let an organization member change another user's personal proposal", async () => {
|
|
359
|
+
mocks.getDb.mockReturnValue(
|
|
360
|
+
createDbMock(
|
|
361
|
+
pendingProposal({
|
|
362
|
+
ownerEmail: "other@example.test",
|
|
363
|
+
orgId: "org-1",
|
|
364
|
+
}),
|
|
365
|
+
),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
await expect(
|
|
369
|
+
authorizeDreamProposalMutation(
|
|
370
|
+
{ id: "proposal-1" },
|
|
371
|
+
{
|
|
372
|
+
caller: "http",
|
|
373
|
+
orgId: "org-1",
|
|
374
|
+
userEmail: "member@example.test",
|
|
375
|
+
},
|
|
376
|
+
),
|
|
377
|
+
).rejects.toThrow("can only be changed by their owner");
|
|
378
|
+
expect(mocks.authorizeDispatchAdmin).not.toHaveBeenCalled();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
304
382
|
describe("listDreamCandidates", () => {
|
|
305
383
|
it("scores grounded thread signals and keeps per-thread debug errors isolated", async () => {
|
|
306
384
|
mocks.searchAgentThreads.mockResolvedValue({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
|
|
3
|
+
import type { ActionRunContext } from "@agent-native/core/action";
|
|
3
4
|
import {
|
|
4
5
|
and,
|
|
5
6
|
desc,
|
|
@@ -20,8 +21,10 @@ import {
|
|
|
20
21
|
putOrgSetting,
|
|
21
22
|
putUserSetting,
|
|
22
23
|
} from "@agent-native/core/settings";
|
|
24
|
+
import { ForbiddenError } from "@agent-native/core/sharing";
|
|
23
25
|
|
|
24
26
|
import { getDb, schema } from "../../db/index.js";
|
|
27
|
+
import { authorizeDispatchAdmin } from "./app-roles.js";
|
|
25
28
|
import {
|
|
26
29
|
createApprovalRequest,
|
|
27
30
|
currentOrgId,
|
|
@@ -444,7 +447,10 @@ function scopeFor<T extends { ownerEmail: any; orgId: any }>(
|
|
|
444
447
|
if (!ctx.orgId) {
|
|
445
448
|
return and(eq(table.ownerEmail, ctx.ownerEmail), isNull(table.orgId));
|
|
446
449
|
}
|
|
447
|
-
return or(
|
|
450
|
+
return or(
|
|
451
|
+
and(eq(table.ownerEmail, ctx.ownerEmail), isNull(table.orgId)),
|
|
452
|
+
eq(table.orgId, ctx.orgId),
|
|
453
|
+
);
|
|
448
454
|
}
|
|
449
455
|
|
|
450
456
|
function safeJson(value: unknown): string {
|
|
@@ -2394,6 +2400,36 @@ async function getProposalRow(
|
|
|
2394
2400
|
return row ?? null;
|
|
2395
2401
|
}
|
|
2396
2402
|
|
|
2403
|
+
export async function authorizeDreamProposalMutation(
|
|
2404
|
+
args: { id: string },
|
|
2405
|
+
ctx?: ActionRunContext,
|
|
2406
|
+
): Promise<void> {
|
|
2407
|
+
const ownerEmail =
|
|
2408
|
+
ctx?.userEmail !== undefined ? ctx.userEmail : currentOwnerEmail();
|
|
2409
|
+
const orgId = ctx?.orgId !== undefined ? ctx.orgId : currentOrgId();
|
|
2410
|
+
const proposal = await getProposalRow(args.id, {
|
|
2411
|
+
ownerEmail,
|
|
2412
|
+
orgId: orgId?.trim() || null,
|
|
2413
|
+
});
|
|
2414
|
+
if (!proposal) {
|
|
2415
|
+
throw new ForbiddenError("Dream proposal not found");
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
if (proposal.targetType === "personal-memory") {
|
|
2419
|
+
if (
|
|
2420
|
+
proposal.ownerEmail.trim().toLowerCase() !==
|
|
2421
|
+
ownerEmail.trim().toLowerCase()
|
|
2422
|
+
) {
|
|
2423
|
+
throw new ForbiddenError(
|
|
2424
|
+
"Personal memory proposals can only be changed by their owner",
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
await authorizeDispatchAdmin(args, ctx);
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2397
2433
|
export async function createDreamReport(input: {
|
|
2398
2434
|
sourceId?: string;
|
|
2399
2435
|
sourceIds?: string[];
|