@agent-native/core 0.210.1 → 0.211.0-nightly-20261020183616
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/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-agent-engine-configured.d.ts +2 -2
- package/dist/client/use-agent-engine-configured.js +3 -9
- package/dist/client/use-session.js +2 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-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/execute-action-call.js +8 -17
- package/dist/observability/routes.d.ts +5 -5
- 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/index.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/resources/handlers.d.ts +1 -1
- 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 +4 -4
- 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/edge.d.ts +1 -0
- package/dist/server/edge.js +1 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.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/vite/client.js +2 -1
- package/docs/content/actions-access-control.mdx +3 -3
- package/package.json +4 -4
- 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
package/dist/deploy/build.js
CHANGED
|
@@ -1127,7 +1127,9 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
|
|
|
1127
1127
|
actionRegistrations.push(` const ${handlerName} = defineEventHandler(async (event) => {
|
|
1128
1128
|
setResponseHeader(event, "Cache-Control", "no-" + "store");
|
|
1129
1129
|
const actionIsUiOnly = ${a.uiOnly ? "true" : `${varName}.uiOnly === true`};
|
|
1130
|
-
const
|
|
1130
|
+
const actionDeclaresApproval =
|
|
1131
|
+
${varName}.needsApproval !== undefined && ${varName}.needsApproval !== false;
|
|
1132
|
+
const uiActionContext = actionIsUiOnly || actionDeclaresApproval
|
|
1131
1133
|
? await getGeneratedUiActionContext(event)
|
|
1132
1134
|
: undefined;
|
|
1133
1135
|
if (actionIsUiOnly && !uiActionContext) {
|
|
@@ -1175,10 +1177,29 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
|
|
|
1175
1177
|
actionName: ${JSON.stringify(a.name)},
|
|
1176
1178
|
...(actionContext ?? {}),
|
|
1177
1179
|
};
|
|
1178
|
-
const runAction = () =>
|
|
1180
|
+
const runAction = async () => {
|
|
1181
|
+
if (
|
|
1182
|
+
actionDeclaresApproval &&
|
|
1183
|
+
!(uiActionContext && caller === "frontend")
|
|
1184
|
+
) {
|
|
1185
|
+
const refusal = await generatedActionApprovalRefusal(
|
|
1186
|
+
${varName},
|
|
1187
|
+
params,
|
|
1188
|
+
actionRunContext,
|
|
1189
|
+
);
|
|
1190
|
+
if (refusal) {
|
|
1191
|
+
return new Response(
|
|
1192
|
+
JSON.stringify({ error: refusal.message, errorCode: refusal.errorCode }),
|
|
1193
|
+
{ status: refusal.statusCode, headers: { "Content-Type": "application/json" } },
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return ${varName}.run(params, actionRunContext);
|
|
1198
|
+
};
|
|
1179
1199
|
const result = actionContext
|
|
1180
1200
|
? await runWithGeneratedRequestContext(actionContext, runAction)
|
|
1181
1201
|
: await runAction();
|
|
1202
|
+
if (result instanceof Response) return result;
|
|
1182
1203
|
if (typeof result === "string") { try { return JSON.parse(result); } catch { return result; } }
|
|
1183
1204
|
return result;
|
|
1184
1205
|
} catch (err) {
|
|
@@ -1200,7 +1221,7 @@ ${["post", "put", "delete"]
|
|
|
1200
1221
|
getFrameworkRoutePrefix as getAgentNativeFrameworkRoutePrefix,
|
|
1201
1222
|
getSsrAuthRedirectScript as getAgentNativeSsrAuthRedirectScript,
|
|
1202
1223
|
resolveAppHomePath as resolveAgentNativeAppHomePath,
|
|
1203
|
-
${hasActions ? " getSession as getGeneratedSession,\n hasUiActionCapability as hasGeneratedUiActionCapability,\n isSameOriginRequest as isGeneratedSameOriginRequest,\n mountUiActionCapabilityRoute as mountGeneratedUiActionCapabilityRoute,\n resolveOrgIdForEmailViaEvent as resolveGeneratedOrgId,\n runWithRequestContext as runWithGeneratedRequestContext,\n" : ""}
|
|
1224
|
+
${hasActions ? " directHttpActionApprovalRefusal as generatedActionApprovalRefusal,\n getSession as getGeneratedSession,\n hasUiActionCapability as hasGeneratedUiActionCapability,\n isSameOriginRequest as isGeneratedSameOriginRequest,\n mountUiActionCapabilityRoute as mountGeneratedUiActionCapabilityRoute,\n resolveOrgIdForEmailViaEvent as resolveGeneratedOrgId,\n runWithRequestContext as runWithGeneratedRequestContext,\n" : ""}
|
|
1204
1225
|
} from "${EDGE_SERVER_ENTRYPOINT}";`);
|
|
1205
1226
|
for (let i = 0; i < edgePlugins.length; i++) {
|
|
1206
1227
|
const varName = `plugin_${i}`;
|
|
@@ -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,
|
|
@@ -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) {
|
|
@@ -27,39 +27,39 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
27
27
|
thumbsUpRate: number;
|
|
28
28
|
avgEvalScore: number;
|
|
29
29
|
} | {
|
|
30
|
+
ok?: undefined;
|
|
30
31
|
error: string;
|
|
31
32
|
message: string;
|
|
32
33
|
summary?: undefined;
|
|
33
34
|
spans?: undefined;
|
|
34
|
-
ok?: undefined;
|
|
35
35
|
} | {
|
|
36
36
|
error?: undefined;
|
|
37
|
+
ok?: undefined;
|
|
37
38
|
message?: undefined;
|
|
38
39
|
summary: import("./types.js").TraceSummary;
|
|
39
40
|
spans: import("./types.js").TraceSpan[];
|
|
40
|
-
ok?: undefined;
|
|
41
41
|
} | {
|
|
42
42
|
error?: undefined;
|
|
43
|
+
ok?: undefined;
|
|
43
44
|
message?: undefined;
|
|
44
45
|
summary?: undefined;
|
|
45
46
|
spans?: undefined;
|
|
46
47
|
traceMissing: true;
|
|
47
48
|
id: string;
|
|
48
|
-
ok?: undefined;
|
|
49
49
|
} | {
|
|
50
50
|
error?: undefined;
|
|
51
|
+
ok?: undefined;
|
|
51
52
|
message?: undefined;
|
|
52
53
|
summary?: undefined;
|
|
53
54
|
spans?: undefined;
|
|
54
55
|
traceMissing?: undefined;
|
|
55
56
|
id: string;
|
|
56
|
-
ok?: undefined;
|
|
57
57
|
} | {
|
|
58
|
+
ok?: undefined;
|
|
58
59
|
message?: undefined;
|
|
59
60
|
summary?: undefined;
|
|
60
61
|
spans?: undefined;
|
|
61
62
|
error: any;
|
|
62
|
-
ok?: undefined;
|
|
63
63
|
} | {
|
|
64
64
|
error?: undefined;
|
|
65
65
|
message?: undefined;
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -162,9 +162,9 @@ 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;
|
|
167
166
|
id?: undefined;
|
|
167
|
+
name?: undefined;
|
|
168
168
|
role?: undefined;
|
|
169
169
|
} | {
|
|
170
170
|
success?: undefined;
|
|
@@ -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;
|
|
@@ -39,8 +39,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
39
39
|
error: string;
|
|
40
40
|
}>;
|
|
41
41
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
42
|
-
ok?: undefined;
|
|
43
42
|
error: string;
|
|
43
|
+
ok?: undefined;
|
|
44
44
|
} | {
|
|
45
45
|
error?: undefined;
|
|
46
46
|
ok: boolean;
|
|
@@ -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
|
@@ -57,31 +57,31 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
|
|
|
57
57
|
} | {
|
|
58
58
|
error: string;
|
|
59
59
|
errorCode?: undefined;
|
|
60
|
-
status?: undefined;
|
|
61
60
|
ok?: undefined;
|
|
61
|
+
status?: undefined;
|
|
62
62
|
} | {
|
|
63
63
|
error: string;
|
|
64
64
|
errorCode: string;
|
|
65
|
-
status?: undefined;
|
|
66
65
|
ok?: undefined;
|
|
66
|
+
status?: undefined;
|
|
67
67
|
} | {
|
|
68
68
|
error?: undefined;
|
|
69
69
|
errorCode?: undefined;
|
|
70
70
|
ok: boolean;
|
|
71
71
|
status: string;
|
|
72
72
|
} | {
|
|
73
|
+
ok?: undefined;
|
|
73
74
|
error: string;
|
|
74
75
|
removed?: undefined;
|
|
75
|
-
ok?: undefined;
|
|
76
76
|
} | {
|
|
77
77
|
error?: undefined;
|
|
78
78
|
ok: boolean;
|
|
79
79
|
removed: boolean;
|
|
80
80
|
}>>;
|
|
81
81
|
export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
82
|
+
ok?: undefined;
|
|
82
83
|
error: string;
|
|
83
84
|
note?: undefined;
|
|
84
|
-
ok?: undefined;
|
|
85
85
|
} | {
|
|
86
86
|
error?: undefined;
|
|
87
87
|
ok: boolean;
|
|
@@ -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
|
+
}
|