@agent-native/core 0.210.2 → 0.211.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/action.d.ts +8 -0
- package/dist/action.js +17 -0
- package/dist/agent/production-agent.js +11 -20
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/chat/agentkit-protocol.js +48 -5
- package/dist/client/chat/runtime.d.ts +2 -0
- package/dist/client/chat/runtime.js +6 -1
- package/dist/client/hooks/index.d.ts +1 -1
- package/dist/client/hooks/index.js +1 -1
- package/dist/client/resources/mcp-integration-logos.js +4 -0
- package/dist/client/session-identity.d.ts +8 -0
- package/dist/client/session-identity.js +13 -0
- package/dist/client/use-action.d.ts +14 -0
- package/dist/client/use-action.js +117 -28
- package/dist/client/use-session.js +2 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.js +24 -3
- package/dist/mcp/actions/revoke-org-service-token.js +5 -1
- package/dist/mcp/build-server.js +18 -2
- package/dist/mcp/execute-action-call.js +8 -17
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +4 -4
- package/dist/org/actions/set-app-member-roles.js +4 -0
- package/dist/org/actions/set-app-permission-roles.js +4 -0
- package/dist/org/actions/set-workspace-app-access.js +4 -0
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/index.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +17 -17
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/review/suggestions/actions.d.ts +1 -0
- package/dist/review/suggestions/actions.js +54 -42
- package/dist/secrets/actions/delete-api-key.js +4 -0
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/action-approval.d.ts +15 -0
- package/dist/server/action-approval.js +39 -0
- package/dist/server/action-routes.js +33 -28
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/server/better-auth-instance.d.ts +6 -0
- package/dist/server/better-auth-instance.js +22 -12
- package/dist/server/edge.d.ts +1 -0
- package/dist/server/edge.js +1 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/workspace-provider-oauth.js +1 -0
- package/dist/shared/action-error-codes.d.ts +5 -0
- package/dist/shared/action-error-codes.js +5 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
- package/dist/templates/default/.agents/skills/actions/references/action-fields.md +5 -3
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
- package/dist/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
- package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
- package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +5 -3
- package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +5 -3
- package/dist/triggers/actions/manage-automation.d.ts +3 -3
- package/dist/vite/client.js +2 -1
- package/docs/content/actions-access-control.mdx +3 -3
- package/package.json +3 -3
- package/src/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
- package/src/templates/default/.agents/skills/actions/references/action-fields.md +5 -3
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
- package/src/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
- package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
- package/src/templates/headless/.agents/skills/actions/references/action-fields.md +5 -3
- package/src/templates/workspace-core/.agents/skills/actions/references/action-fields.md +5 -3
|
@@ -10,11 +10,15 @@ import { defineAction } from "../../action.js";
|
|
|
10
10
|
import { revokeOrgServiceToken } from "../connect-store.js";
|
|
11
11
|
import { requireServiceTokenCaller } from "./service-token-access.js";
|
|
12
12
|
export default defineAction({
|
|
13
|
-
description: "Revoke one of your organization's service tokens by id (get ids from list-org-service-tokens). The token stops working immediately. Org owner/admin only. Idempotent.",
|
|
13
|
+
description: "Revoke one of your organization's service tokens by id (get ids from list-org-service-tokens). The token stops working immediately. Every call needs the user's approval. Org owner/admin only. Idempotent.",
|
|
14
14
|
schema: z.object({
|
|
15
15
|
id: z.string().min(1).describe("Service token id to revoke"),
|
|
16
16
|
}),
|
|
17
17
|
toolCallable: false,
|
|
18
|
+
// It creates, changes, or revokes a credential or who may use one: every
|
|
19
|
+
// call needs the user's approval, and "always allow" is not offered.
|
|
20
|
+
needsApproval: true,
|
|
21
|
+
allowPersistentApproval: false,
|
|
18
22
|
run: async (args, ctx) => {
|
|
19
23
|
const caller = await requireServiceTokenCaller({
|
|
20
24
|
userEmail: ctx?.userEmail,
|
package/dist/mcp/build-server.js
CHANGED
|
@@ -2094,10 +2094,23 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
2094
2094
|
}
|
|
2095
2095
|
: {}),
|
|
2096
2096
|
});
|
|
2097
|
-
|
|
2097
|
+
// A verified user's MCP token names them by email. Browser sessions also
|
|
2098
|
+
// carry the Better Auth user id, and per-user gates such as Clips' hosted
|
|
2099
|
+
// storage key on it, so a tool call resolves it the same way.
|
|
2100
|
+
let callerAuthUserId;
|
|
2101
|
+
function resolveCallerAuthUserId() {
|
|
2102
|
+
if (!hasVerifiedMcpUserIdentity(effectiveIdentity)) {
|
|
2103
|
+
return Promise.resolve(undefined);
|
|
2104
|
+
}
|
|
2105
|
+
const email = effectiveIdentity.userEmail;
|
|
2106
|
+
callerAuthUserId ??= import("../server/better-auth-instance.js").then(({ findBetterAuthUserIdForEmail }) => findBetterAuthUserIdForEmail(email));
|
|
2107
|
+
return callerAuthUserId;
|
|
2108
|
+
}
|
|
2109
|
+
async function withCallerContext(fn, mcpRequestId, authUserId) {
|
|
2098
2110
|
const orgId = await orgIdPromise;
|
|
2099
2111
|
return runWithRequestContext({
|
|
2100
2112
|
userEmail: effectiveIdentity?.userEmail,
|
|
2113
|
+
...(authUserId ? { authUserId } : {}),
|
|
2101
2114
|
orgId,
|
|
2102
2115
|
...(verifiedServiceIdentity ? { verifiedServiceIdentity } : {}),
|
|
2103
2116
|
...(effectiveIdentity?.orgId === null
|
|
@@ -2108,6 +2121,9 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
2108
2121
|
mcpCredentialIssuedAtMs: effectiveIdentity?.mcpCredentialIssuedAtMs ?? null,
|
|
2109
2122
|
}, fn);
|
|
2110
2123
|
}
|
|
2124
|
+
async function withToolCallContext(fn, mcpRequestId) {
|
|
2125
|
+
return withCallerContext(fn, mcpRequestId, await resolveCallerAuthUserId());
|
|
2126
|
+
}
|
|
2111
2127
|
const actionCallContext = {
|
|
2112
2128
|
appId: config.appId,
|
|
2113
2129
|
identity: effectiveIdentity,
|
|
@@ -2350,7 +2366,7 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
2350
2366
|
: requestMeta?.mcpRetryToken
|
|
2351
2367
|
? `stateless:${requestMeta.mcpRetryToken}`
|
|
2352
2368
|
: undefined;
|
|
2353
|
-
const result = await
|
|
2369
|
+
const result = await withToolCallContext(async () => {
|
|
2354
2370
|
const { name, arguments: args } = request.params;
|
|
2355
2371
|
try {
|
|
2356
2372
|
const { allowedActions } = await resolveServiceGrant();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isMcpReadOnlyTool } from "../action-call-classification.js";
|
|
2
2
|
import { vouchedOutcome } from "../action-frame.js";
|
|
3
|
-
import { actionCallIntent, isActionContractError, recordInvalidActionCall,
|
|
3
|
+
import { actionCallIntent, isActionContractError, recordInvalidActionCall, preparedCallNeedsApproval, runPreparedActionCall, } from "../action.js";
|
|
4
4
|
import { AGENT_IMAGES_FIELD } from "../agent/tool-result-images.js";
|
|
5
5
|
import { MCP_ACTION_RESULT_MARKER, isMcpActionResult, mcpActionResultText, } from "../mcp-client/app-result.js";
|
|
6
6
|
import { assertServicePrincipalMayCall, assertServicePrincipalMayRun, recordServicePrincipalDenial, ServicePrincipalRefusedError, } from "../org/service-principal-guard.js";
|
|
@@ -564,22 +564,13 @@ async function authorizeApproval(context, call, entry, args, prepared) {
|
|
|
564
564
|
if (entry.needsApproval === undefined) {
|
|
565
565
|
return { status: "authorized", clientAccepted: false };
|
|
566
566
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
appId: context.appId,
|
|
575
|
-
caller: "mcp",
|
|
576
|
-
actionName: name,
|
|
577
|
-
}))
|
|
578
|
-
: entry.needsApproval === true;
|
|
579
|
-
}
|
|
580
|
-
catch {
|
|
581
|
-
mustApprove = true;
|
|
582
|
-
}
|
|
567
|
+
const mustApprove = await preparedCallNeedsApproval(entry, prepared, {
|
|
568
|
+
userEmail: getRequestUserEmail(),
|
|
569
|
+
orgId: getRequestOrgId() ?? null,
|
|
570
|
+
appId: context.appId,
|
|
571
|
+
caller: "mcp",
|
|
572
|
+
actionName: name,
|
|
573
|
+
});
|
|
583
574
|
if (!mustApprove)
|
|
584
575
|
return { status: "authorized", clientAccepted: false };
|
|
585
576
|
if (!hasVerifiedUserIdentity) {
|
|
@@ -33,26 +33,26 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
33
33
|
spans?: undefined;
|
|
34
34
|
ok?: undefined;
|
|
35
35
|
} | {
|
|
36
|
-
error?: undefined;
|
|
37
36
|
message?: undefined;
|
|
38
37
|
summary: import("./types.js").TraceSummary;
|
|
39
38
|
spans: import("./types.js").TraceSpan[];
|
|
39
|
+
error?: undefined;
|
|
40
40
|
ok?: undefined;
|
|
41
41
|
} | {
|
|
42
|
-
error?: undefined;
|
|
43
42
|
message?: undefined;
|
|
44
43
|
summary?: undefined;
|
|
45
44
|
spans?: undefined;
|
|
46
45
|
traceMissing: true;
|
|
47
46
|
id: string;
|
|
47
|
+
error?: undefined;
|
|
48
48
|
ok?: undefined;
|
|
49
49
|
} | {
|
|
50
|
-
error?: undefined;
|
|
51
50
|
message?: undefined;
|
|
52
51
|
summary?: undefined;
|
|
53
52
|
spans?: undefined;
|
|
54
53
|
traceMissing?: undefined;
|
|
55
54
|
id: string;
|
|
55
|
+
error?: undefined;
|
|
56
56
|
ok?: undefined;
|
|
57
57
|
} | {
|
|
58
58
|
message?: undefined;
|
|
@@ -61,9 +61,9 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
61
61
|
error: any;
|
|
62
62
|
ok?: undefined;
|
|
63
63
|
} | {
|
|
64
|
-
error?: undefined;
|
|
65
64
|
message?: undefined;
|
|
66
65
|
summary?: undefined;
|
|
67
66
|
spans?: undefined;
|
|
68
67
|
ok: boolean;
|
|
68
|
+
error?: undefined;
|
|
69
69
|
}>>;
|
|
@@ -19,6 +19,10 @@ export default defineAction({
|
|
|
19
19
|
return `Updated ${args.appId} roles for ${args.email}: [${(change.previousRoles ?? []).join(", ")}] -> [${(change.roles ?? []).join(", ")}]`;
|
|
20
20
|
},
|
|
21
21
|
}),
|
|
22
|
+
// It changes who may use an app or what they may do in it: every call
|
|
23
|
+
// needs the user's approval, and "always allow" is not offered.
|
|
24
|
+
needsApproval: true,
|
|
25
|
+
allowPersistentApproval: false,
|
|
22
26
|
run: async ({ appId, email, roles }, ctx) => {
|
|
23
27
|
const caller = await requireOrgMember(ctx, true);
|
|
24
28
|
const descriptor = getRegisteredAppRoles(appId);
|
|
@@ -19,6 +19,10 @@ export default defineAction({
|
|
|
19
19
|
return `${args.reset ? "Reset" : "Updated"} ${args.appId} permission ${args.permission}: [${(change.previousRoles ?? []).join(", ")}] -> [${(change.roles ?? []).join(", ")}]`;
|
|
20
20
|
},
|
|
21
21
|
}),
|
|
22
|
+
// It changes who may use an app or what they may do in it: every call
|
|
23
|
+
// needs the user's approval, and "always allow" is not offered.
|
|
24
|
+
needsApproval: true,
|
|
25
|
+
allowPersistentApproval: false,
|
|
22
26
|
run: async ({ appId, permission, roles, reset }, ctx) => {
|
|
23
27
|
const caller = await requireOrgMember(ctx, true);
|
|
24
28
|
const descriptor = getRegisteredAppRoles(appId);
|
|
@@ -16,6 +16,10 @@ export default defineAction({
|
|
|
16
16
|
targetId: (args) => args.appId,
|
|
17
17
|
summary: (args) => `Set ${args.appId} workspace access to ${args.mode}`,
|
|
18
18
|
}),
|
|
19
|
+
// It changes who may use an app or what they may do in it: every call
|
|
20
|
+
// needs the user's approval, and "always allow" is not offered.
|
|
21
|
+
needsApproval: true,
|
|
22
|
+
allowPersistentApproval: false,
|
|
19
23
|
run: async ({ appId, mode }, ctx) => {
|
|
20
24
|
const caller = await requireOrgMember(ctx, true);
|
|
21
25
|
const db = getDbExec();
|
package/dist/org/handlers.d.ts
CHANGED
|
@@ -73,9 +73,9 @@ export declare const setWorkspaceAppDefaultVisibilityHandler: import("h3").Event
|
|
|
73
73
|
}>>;
|
|
74
74
|
export declare const createOrgHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
75
75
|
success: boolean;
|
|
76
|
-
id?: undefined;
|
|
77
76
|
name?: undefined;
|
|
78
77
|
role?: undefined;
|
|
78
|
+
id?: undefined;
|
|
79
79
|
} | {
|
|
80
80
|
success?: undefined;
|
|
81
81
|
id: string;
|
package/dist/org/index.d.ts
CHANGED
|
@@ -162,8 +162,8 @@ export declare const getMyOrgHandler: import("h3").EventHandlerWithFetch<import(
|
|
|
162
162
|
soloDeploymentAdmin: boolean;
|
|
163
163
|
}>>;
|
|
164
164
|
export declare const createOrgHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
165
|
-
name?: undefined;
|
|
166
165
|
success: boolean;
|
|
166
|
+
name?: undefined;
|
|
167
167
|
role?: undefined;
|
|
168
168
|
id?: undefined;
|
|
169
169
|
} | {
|
|
@@ -75,10 +75,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
75
75
|
user: "user";
|
|
76
76
|
}>>;
|
|
77
77
|
}, z.core.$strip>>, {
|
|
78
|
-
|
|
78
|
+
message?: undefined;
|
|
79
79
|
id?: undefined;
|
|
80
|
-
deleted?: undefined;
|
|
81
|
-
found?: undefined;
|
|
82
80
|
providers: {
|
|
83
81
|
id: string;
|
|
84
82
|
label: string;
|
|
@@ -89,45 +87,47 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
89
87
|
updatedAt: number;
|
|
90
88
|
}[];
|
|
91
89
|
count: number;
|
|
90
|
+
provider?: undefined;
|
|
91
|
+
found?: undefined;
|
|
92
|
+
deleted?: undefined;
|
|
92
93
|
registered?: undefined;
|
|
93
94
|
label?: undefined;
|
|
94
|
-
message?: undefined;
|
|
95
95
|
} | {
|
|
96
|
+
message?: undefined;
|
|
96
97
|
id?: undefined;
|
|
97
|
-
count?: undefined;
|
|
98
|
-
deleted?: undefined;
|
|
99
98
|
providers?: undefined;
|
|
99
|
+
count?: undefined;
|
|
100
100
|
found: boolean;
|
|
101
101
|
provider: import("../custom-registry.js").CustomProviderConfig;
|
|
102
|
+
deleted?: undefined;
|
|
102
103
|
registered?: undefined;
|
|
103
104
|
label?: undefined;
|
|
104
|
-
message?: undefined;
|
|
105
105
|
} | {
|
|
106
|
-
|
|
107
|
-
count?: undefined;
|
|
108
|
-
deleted?: undefined;
|
|
106
|
+
message?: undefined;
|
|
109
107
|
providers?: undefined;
|
|
108
|
+
count?: undefined;
|
|
109
|
+
provider?: undefined;
|
|
110
110
|
found: boolean;
|
|
111
111
|
id: string;
|
|
112
|
+
deleted?: undefined;
|
|
112
113
|
registered?: undefined;
|
|
113
114
|
label?: undefined;
|
|
114
|
-
message?: undefined;
|
|
115
115
|
} | {
|
|
116
|
-
|
|
116
|
+
message?: undefined;
|
|
117
|
+
providers?: undefined;
|
|
117
118
|
count?: undefined;
|
|
119
|
+
provider?: undefined;
|
|
118
120
|
found?: undefined;
|
|
119
|
-
providers?: undefined;
|
|
120
121
|
deleted: boolean;
|
|
121
122
|
id: string;
|
|
122
123
|
registered?: undefined;
|
|
123
124
|
label?: undefined;
|
|
124
|
-
message?: undefined;
|
|
125
125
|
} | {
|
|
126
|
-
|
|
126
|
+
providers?: undefined;
|
|
127
127
|
count?: undefined;
|
|
128
|
-
|
|
128
|
+
provider?: undefined;
|
|
129
129
|
found?: undefined;
|
|
130
|
-
|
|
130
|
+
deleted?: undefined;
|
|
131
131
|
registered: boolean;
|
|
132
132
|
id: string;
|
|
133
133
|
label: string;
|
|
@@ -316,11 +316,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
316
316
|
notes?: string | undefined;
|
|
317
317
|
scope?: "org" | "user" | undefined;
|
|
318
318
|
}, {
|
|
319
|
-
id?: undefined;
|
|
320
319
|
provider?: undefined;
|
|
321
|
-
|
|
320
|
+
id?: undefined;
|
|
322
321
|
deleted?: undefined;
|
|
323
|
-
found?: undefined;
|
|
324
322
|
providers: {
|
|
325
323
|
id: string;
|
|
326
324
|
label: string;
|
|
@@ -331,44 +329,46 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
331
329
|
updatedAt: number;
|
|
332
330
|
}[];
|
|
333
331
|
count: number;
|
|
332
|
+
found?: undefined;
|
|
334
333
|
registered?: undefined;
|
|
335
334
|
label?: undefined;
|
|
335
|
+
message?: undefined;
|
|
336
336
|
} | {
|
|
337
337
|
id?: undefined;
|
|
338
|
-
count?: undefined;
|
|
339
|
-
message?: undefined;
|
|
340
338
|
deleted?: undefined;
|
|
341
339
|
providers?: undefined;
|
|
340
|
+
count?: undefined;
|
|
342
341
|
found: boolean;
|
|
343
342
|
provider: import("../custom-registry.js").CustomProviderConfig;
|
|
344
343
|
registered?: undefined;
|
|
345
344
|
label?: undefined;
|
|
345
|
+
message?: undefined;
|
|
346
346
|
} | {
|
|
347
347
|
provider?: undefined;
|
|
348
|
-
count?: undefined;
|
|
349
|
-
message?: undefined;
|
|
350
348
|
deleted?: undefined;
|
|
351
349
|
providers?: undefined;
|
|
350
|
+
count?: undefined;
|
|
352
351
|
found: boolean;
|
|
353
352
|
id: string;
|
|
354
353
|
registered?: undefined;
|
|
355
354
|
label?: undefined;
|
|
355
|
+
message?: undefined;
|
|
356
356
|
} | {
|
|
357
357
|
provider?: undefined;
|
|
358
|
+
providers?: undefined;
|
|
358
359
|
count?: undefined;
|
|
359
|
-
message?: undefined;
|
|
360
360
|
found?: undefined;
|
|
361
|
-
providers?: undefined;
|
|
362
361
|
deleted: boolean;
|
|
363
362
|
id: string;
|
|
364
363
|
registered?: undefined;
|
|
365
364
|
label?: undefined;
|
|
365
|
+
message?: undefined;
|
|
366
366
|
} | {
|
|
367
367
|
provider?: undefined;
|
|
368
|
-
count?: undefined;
|
|
369
368
|
deleted?: undefined;
|
|
370
|
-
found?: undefined;
|
|
371
369
|
providers?: undefined;
|
|
370
|
+
count?: undefined;
|
|
371
|
+
found?: undefined;
|
|
372
372
|
registered: boolean;
|
|
373
373
|
id: string;
|
|
374
374
|
label: string;
|
|
@@ -83,6 +83,7 @@ export declare const createResourceSuggestionProposal: import("../../action.js")
|
|
|
83
83
|
}[];
|
|
84
84
|
metadata?: Record<string, unknown>;
|
|
85
85
|
}[];
|
|
86
|
+
supersedes?: string[];
|
|
86
87
|
}, {
|
|
87
88
|
proposal: import("./types.js").ResourceSuggestionProposal;
|
|
88
89
|
suggestions: ResourceSuggestion[];
|
|
@@ -58,6 +58,50 @@ function assertCreationReplay(receipt, requestHash, authorEmail, actorKind) {
|
|
|
58
58
|
}
|
|
59
59
|
return receipt.suggestion;
|
|
60
60
|
}
|
|
61
|
+
// One revised proposal can replace every edit of the one before it.
|
|
62
|
+
const MAX_SUPERSEDED = 100;
|
|
63
|
+
async function supersedeSuggestions(tx, args, authorEmail, replacementId) {
|
|
64
|
+
for (const supersededId of new Set(args.supersedes ?? [])) {
|
|
65
|
+
const prior = await getSuggestion(supersededId, tx);
|
|
66
|
+
if (!prior ||
|
|
67
|
+
prior.resourceType !== args.resourceType ||
|
|
68
|
+
prior.resourceId !== args.resourceId) {
|
|
69
|
+
fail("A superseded suggestion was not found on this resource", {
|
|
70
|
+
statusCode: 404,
|
|
71
|
+
errorCode: "not_found",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (!authorEmail || prior.authorEmail !== authorEmail) {
|
|
75
|
+
fail("Only the author can supersede this suggestion", {
|
|
76
|
+
statusCode: 403,
|
|
77
|
+
errorCode: "forbidden",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (prior.status !== "pending")
|
|
81
|
+
continue;
|
|
82
|
+
const claimed = await updateSuggestionStatus(tx, prior.id, "superseded", prior.revision);
|
|
83
|
+
if (!claimed) {
|
|
84
|
+
const latest = await getSuggestion(prior.id, tx);
|
|
85
|
+
// A reviewer decided it first; only pending proposals are replaced.
|
|
86
|
+
if (latest && latest.status !== "pending")
|
|
87
|
+
continue;
|
|
88
|
+
fail("The superseded suggestion changed; refresh and try again", {
|
|
89
|
+
statusCode: 409,
|
|
90
|
+
errorCode: "suggestion_conflict",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
await recordDecision(tx, {
|
|
94
|
+
suggestionId: prior.id,
|
|
95
|
+
idempotencyKey: `${args.idempotencyKey}:supersedes:${prior.id}`,
|
|
96
|
+
reviewer: authorEmail,
|
|
97
|
+
decision: "superseded",
|
|
98
|
+
observedBase: prior.baseRevision,
|
|
99
|
+
outcome: "superseded",
|
|
100
|
+
detail: replacementId,
|
|
101
|
+
});
|
|
102
|
+
await resolveReviewThreadWithClient(tx, prior.threadId, authorEmail, { resourceType: prior.resourceType, resourceId: prior.resourceId }, "superseded");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
61
105
|
export const createResourceSuggestion = defineAction({
|
|
62
106
|
description: "Create a typed pending suggestion without changing the canonical resource. Pass supersedes to replace your own earlier pending suggestions on the same resource in the same step.",
|
|
63
107
|
schema: z.object({
|
|
@@ -70,7 +114,7 @@ export const createResourceSuggestion = defineAction({
|
|
|
70
114
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
71
115
|
supersedes: z
|
|
72
116
|
.array(z.string().min(1))
|
|
73
|
-
.max(
|
|
117
|
+
.max(MAX_SUPERSEDED)
|
|
74
118
|
.optional()
|
|
75
119
|
.describe("IDs of your own earlier suggestions on this resource that this one replaces. Pending ones become superseded atomically with this creation; already decided ones are left as they are."),
|
|
76
120
|
}),
|
|
@@ -133,46 +177,7 @@ export const createResourceSuggestion = defineAction({
|
|
|
133
177
|
threadComment: null,
|
|
134
178
|
};
|
|
135
179
|
}
|
|
136
|
-
|
|
137
|
-
const prior = await getSuggestion(supersededId, tx);
|
|
138
|
-
if (!prior ||
|
|
139
|
-
prior.resourceType !== args.resourceType ||
|
|
140
|
-
prior.resourceId !== args.resourceId) {
|
|
141
|
-
fail("A superseded suggestion was not found on this resource", {
|
|
142
|
-
statusCode: 404,
|
|
143
|
-
errorCode: "not_found",
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
if (!authorEmail || prior.authorEmail !== authorEmail) {
|
|
147
|
-
fail("Only the author can supersede this suggestion", {
|
|
148
|
-
statusCode: 403,
|
|
149
|
-
errorCode: "forbidden",
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
if (prior.status !== "pending")
|
|
153
|
-
continue;
|
|
154
|
-
const claimed = await updateSuggestionStatus(tx, prior.id, "superseded", prior.revision);
|
|
155
|
-
if (!claimed) {
|
|
156
|
-
const latest = await getSuggestion(prior.id, tx);
|
|
157
|
-
// A reviewer decided it first; only pending proposals are replaced.
|
|
158
|
-
if (latest && latest.status !== "pending")
|
|
159
|
-
continue;
|
|
160
|
-
fail("The superseded suggestion changed; refresh and try again", {
|
|
161
|
-
statusCode: 409,
|
|
162
|
-
errorCode: "suggestion_conflict",
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
await recordDecision(tx, {
|
|
166
|
-
suggestionId: prior.id,
|
|
167
|
-
idempotencyKey: `${args.idempotencyKey}:supersedes:${prior.id}`,
|
|
168
|
-
reviewer: authorEmail,
|
|
169
|
-
decision: "superseded",
|
|
170
|
-
observedBase: prior.baseRevision,
|
|
171
|
-
outcome: "superseded",
|
|
172
|
-
detail: created.id,
|
|
173
|
-
});
|
|
174
|
-
await resolveReviewThreadWithClient(tx, prior.threadId, authorEmail, { resourceType: prior.resourceType, resourceId: prior.resourceId }, "superseded");
|
|
175
|
-
}
|
|
180
|
+
await supersedeSuggestions(tx, args, authorEmail, created.id);
|
|
176
181
|
const threadComment = await insertReviewCommentWithClient({
|
|
177
182
|
resourceType: created.resourceType,
|
|
178
183
|
resourceId: created.resourceId,
|
|
@@ -533,6 +538,11 @@ const proposalCreationSchema = z.object({
|
|
|
533
538
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
534
539
|
}))
|
|
535
540
|
.min(1),
|
|
541
|
+
supersedes: z
|
|
542
|
+
.array(z.string().min(1))
|
|
543
|
+
.max(MAX_SUPERSEDED)
|
|
544
|
+
.optional()
|
|
545
|
+
.describe("IDs of your own earlier suggestions on this resource that this proposal replaces. Pending ones become superseded atomically with this creation; already decided ones are left as they are."),
|
|
536
546
|
});
|
|
537
547
|
export const getResourceSuggestionProposalByCreationKey = defineAction({
|
|
538
548
|
description: "Find an earlier proposal creation by its idempotency key before recomputing an edit.",
|
|
@@ -565,7 +575,7 @@ export const getResourceSuggestionProposalByCreationKey = defineAction({
|
|
|
565
575
|
},
|
|
566
576
|
});
|
|
567
577
|
export const createResourceSuggestionProposal = defineAction({
|
|
568
|
-
description: "Create or append independently reviewable suggestions under one durable proposal.",
|
|
578
|
+
description: "Create or append independently reviewable suggestions under one durable proposal. Pass supersedes to replace your own earlier pending suggestions on the same resource in the same step.",
|
|
569
579
|
schema: proposalCreationSchema,
|
|
570
580
|
run: async (args, ctx) => {
|
|
571
581
|
await assertReviewableResourceAccess(args.resourceType, args.resourceId, ctx, "commenter");
|
|
@@ -582,6 +592,7 @@ export const createResourceSuggestionProposal = defineAction({
|
|
|
582
592
|
summary: args.summary,
|
|
583
593
|
operations: args.suggestions,
|
|
584
594
|
metadata: { proposalId: args.proposalId ?? null },
|
|
595
|
+
supersedes: args.supersedes,
|
|
585
596
|
});
|
|
586
597
|
const db = getDbExec();
|
|
587
598
|
await ensureSuggestionTables();
|
|
@@ -701,6 +712,7 @@ export const createResourceSuggestionProposal = defineAction({
|
|
|
701
712
|
const recorded = await recordProposalCreation(tx, args.idempotencyKey, proposal.id, authorEmail, actorKind, requestHash, suggestions.map((suggestion) => suggestion.id));
|
|
702
713
|
if (!recorded)
|
|
703
714
|
throw concurrentCreation;
|
|
715
|
+
await supersedeSuggestions(tx, args, authorEmail, suggestions[0].id);
|
|
704
716
|
return { proposal, suggestions, comments };
|
|
705
717
|
})
|
|
706
718
|
.catch(async (error) => {
|
|
@@ -29,6 +29,10 @@ export default defineAction({
|
|
|
29
29
|
? `Deleted organization key ${args.name}`
|
|
30
30
|
: `Deleted personal key ${args.name}`,
|
|
31
31
|
},
|
|
32
|
+
// It creates, changes, or revokes a credential or who may use one: every
|
|
33
|
+
// call needs the user's approval, and "always allow" is not offered.
|
|
34
|
+
needsApproval: true,
|
|
35
|
+
allowPersistentApproval: false,
|
|
32
36
|
run: async (args, ctx) => {
|
|
33
37
|
const email = ctx?.userEmail?.trim().toLowerCase();
|
|
34
38
|
if (!email)
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -65,31 +65,31 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
|
|
|
65
65
|
status?: undefined;
|
|
66
66
|
ok?: undefined;
|
|
67
67
|
} | {
|
|
68
|
-
error?: undefined;
|
|
69
68
|
errorCode?: undefined;
|
|
70
69
|
ok: boolean;
|
|
71
70
|
status: string;
|
|
71
|
+
error?: undefined;
|
|
72
72
|
} | {
|
|
73
73
|
error: string;
|
|
74
74
|
removed?: undefined;
|
|
75
75
|
ok?: undefined;
|
|
76
76
|
} | {
|
|
77
|
-
error?: undefined;
|
|
78
77
|
ok: boolean;
|
|
79
78
|
removed: boolean;
|
|
79
|
+
error?: undefined;
|
|
80
80
|
}>>;
|
|
81
81
|
export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
82
82
|
error: string;
|
|
83
83
|
note?: undefined;
|
|
84
84
|
ok?: undefined;
|
|
85
85
|
} | {
|
|
86
|
-
error?: undefined;
|
|
87
86
|
ok: boolean;
|
|
88
87
|
note?: undefined;
|
|
89
|
-
} | {
|
|
90
88
|
error?: undefined;
|
|
89
|
+
} | {
|
|
91
90
|
ok: boolean;
|
|
92
91
|
note: string;
|
|
92
|
+
error?: undefined;
|
|
93
93
|
} | {
|
|
94
94
|
note?: undefined;
|
|
95
95
|
ok: boolean;
|
|
@@ -117,11 +117,11 @@ export declare function createAdHocSecretHandler(): import("h3").EventHandlerWit
|
|
|
117
117
|
} | {
|
|
118
118
|
error: string;
|
|
119
119
|
} | {
|
|
120
|
-
error?: undefined;
|
|
121
120
|
ok: boolean;
|
|
122
121
|
key: string;
|
|
123
|
-
} | {
|
|
124
122
|
error?: undefined;
|
|
123
|
+
} | {
|
|
125
124
|
ok: boolean;
|
|
126
125
|
removed: boolean;
|
|
126
|
+
error?: undefined;
|
|
127
127
|
}>>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { actionCallIntent, ActionContractError, preparedCallNeedsApproval, type ActionCaller, type ActionRunContext } from "../action.js";
|
|
2
|
+
/**
|
|
3
|
+
* The refusal for an HTTP call that needs approval. No HTTP caller can grant
|
|
4
|
+
* approval: it is given in the app's UI, or in an agent chat or MCP flow that
|
|
5
|
+
* runs the action in-process.
|
|
6
|
+
*/
|
|
7
|
+
export declare function actionApprovalRequiredError(name: string, caller: ActionCaller, serviceIdentity: boolean): ActionContractError;
|
|
8
|
+
/**
|
|
9
|
+
* For a dispatcher that runs `entry.run` directly instead of a prepared call
|
|
10
|
+
* (the generated worker entry): the refusal when this call needs approval.
|
|
11
|
+
* Arguments that fail validation get no refusal here; the run refuses them.
|
|
12
|
+
*/
|
|
13
|
+
export declare function directHttpActionApprovalRefusal(entry: Parameters<typeof actionCallIntent>[0] & Parameters<typeof preparedCallNeedsApproval>[0], params: unknown, ctx: ActionRunContext & {
|
|
14
|
+
actionName: string;
|
|
15
|
+
}): Promise<ActionContractError | null>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { actionCallIntent, ActionContractError, preparedCallNeedsApproval, } from "../action.js";
|
|
2
|
+
import { UI_CAPABILITY_REQUIRED_ERROR_CODE } from "../shared/action-error-codes.js";
|
|
3
|
+
/**
|
|
4
|
+
* The refusal for an HTTP call that needs approval. No HTTP caller can grant
|
|
5
|
+
* approval: it is given in the app's UI, or in an agent chat or MCP flow that
|
|
6
|
+
* runs the action in-process.
|
|
7
|
+
*/
|
|
8
|
+
export function actionApprovalRequiredError(name, caller, serviceIdentity) {
|
|
9
|
+
// The browser client answers this code by minting its UI capability and
|
|
10
|
+
// retrying once, so the app's own UI is never refused for lacking proof.
|
|
11
|
+
if (caller === "frontend") {
|
|
12
|
+
return new ActionContractError(`"${name}" requires approval for these arguments, which only the signed-in app can give.`, { errorCode: UI_CAPABILITY_REQUIRED_ERROR_CODE, statusCode: 403 });
|
|
13
|
+
}
|
|
14
|
+
const message = serviceIdentity
|
|
15
|
+
? `"${name}" requires human approval for these arguments, and a service identity can never give it. Ask a person to run this action from the app.`
|
|
16
|
+
: caller === "webmcp"
|
|
17
|
+
? `"${name}" requires human approval for these arguments. WebMCP tool calls cannot grant that approval themselves — ask the user to confirm this action in chat, then call it there.`
|
|
18
|
+
: `"${name}" requires human approval for these arguments, which an HTTP action call cannot grant. Ask the user to approve it in the app's agent chat, or call it as an MCP tool, where the approval flow exists.`;
|
|
19
|
+
return new ActionContractError(message, {
|
|
20
|
+
errorCode: "approval_required",
|
|
21
|
+
statusCode: 409,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* For a dispatcher that runs `entry.run` directly instead of a prepared call
|
|
26
|
+
* (the generated worker entry): the refusal when this call needs approval.
|
|
27
|
+
* Arguments that fail validation get no refusal here; the run refuses them.
|
|
28
|
+
*/
|
|
29
|
+
export async function directHttpActionApprovalRefusal(entry, params, ctx) {
|
|
30
|
+
if (entry.needsApproval === undefined || entry.needsApproval === false) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const prepared = await actionCallIntent(entry, params);
|
|
34
|
+
if (!prepared.valid)
|
|
35
|
+
return null;
|
|
36
|
+
return (await preparedCallNeedsApproval(entry, prepared, ctx))
|
|
37
|
+
? actionApprovalRequiredError(ctx.actionName, ctx.caller, false)
|
|
38
|
+
: null;
|
|
39
|
+
}
|