@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.
Files changed (62) hide show
  1. package/dist/action.d.ts +8 -0
  2. package/dist/action.js +17 -0
  3. package/dist/agent/production-agent.js +11 -20
  4. package/dist/client/chat/agentkit-protocol.js +48 -5
  5. package/dist/client/chat/runtime.d.ts +2 -0
  6. package/dist/client/chat/runtime.js +6 -1
  7. package/dist/client/hooks/index.d.ts +1 -1
  8. package/dist/client/hooks/index.js +1 -1
  9. package/dist/client/resources/mcp-integration-logos.js +4 -0
  10. package/dist/client/session-identity.d.ts +8 -0
  11. package/dist/client/session-identity.js +13 -0
  12. package/dist/client/use-action.d.ts +14 -0
  13. package/dist/client/use-action.js +117 -28
  14. package/dist/client/use-agent-engine-configured.d.ts +2 -2
  15. package/dist/client/use-agent-engine-configured.js +3 -9
  16. package/dist/client/use-session.js +2 -0
  17. package/dist/collab/awareness.d.ts +2 -2
  18. package/dist/collab/struct-routes.d.ts +1 -1
  19. package/dist/deploy/build.js +24 -3
  20. package/dist/mcp/actions/revoke-org-service-token.js +5 -1
  21. package/dist/mcp/execute-action-call.js +8 -17
  22. package/dist/observability/routes.d.ts +5 -5
  23. package/dist/org/actions/set-app-member-roles.js +4 -0
  24. package/dist/org/actions/set-app-permission-roles.js +4 -0
  25. package/dist/org/actions/set-workspace-app-access.js +4 -0
  26. package/dist/org/index.d.ts +1 -1
  27. package/dist/provider-api/actions/custom-provider-registration.d.ts +17 -17
  28. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  29. package/dist/resources/handlers.d.ts +1 -1
  30. package/dist/review/suggestions/actions.d.ts +1 -0
  31. package/dist/review/suggestions/actions.js +54 -42
  32. package/dist/secrets/actions/delete-api-key.js +4 -0
  33. package/dist/secrets/routes.d.ts +4 -4
  34. package/dist/server/action-approval.d.ts +15 -0
  35. package/dist/server/action-approval.js +39 -0
  36. package/dist/server/action-routes.js +33 -28
  37. package/dist/server/edge.d.ts +1 -0
  38. package/dist/server/edge.js +1 -0
  39. package/dist/server/realtime-token.d.ts +1 -1
  40. package/dist/server/transcribe-voice.d.ts +1 -1
  41. package/dist/server/workspace-provider-oauth.js +1 -0
  42. package/dist/shared/action-error-codes.d.ts +5 -0
  43. package/dist/shared/action-error-codes.js +5 -0
  44. package/dist/shared/index.d.ts +1 -0
  45. package/dist/shared/index.js +1 -0
  46. package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  47. package/dist/templates/default/.agents/skills/actions/references/action-fields.md +5 -3
  48. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  49. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  50. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  51. package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +5 -3
  52. package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +5 -3
  53. package/dist/vite/client.js +2 -1
  54. package/docs/content/actions-access-control.mdx +3 -3
  55. package/package.json +4 -4
  56. package/src/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  57. package/src/templates/default/.agents/skills/actions/references/action-fields.md +5 -3
  58. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  59. package/src/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  60. package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  61. package/src/templates/headless/.agents/skills/actions/references/action-fields.md +5 -3
  62. package/src/templates/workspace-core/.agents/skills/actions/references/action-fields.md +5 -3
@@ -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 uiActionContext = actionIsUiOnly
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 = () => ${varName}.run(params, actionRunContext);
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, preparedCallArguments, runPreparedActionCall, } from "../action.js";
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
- let mustApprove = false;
568
- try {
569
- mustApprove =
570
- typeof entry.needsApproval === "function"
571
- ? Boolean(await entry.needsApproval(preparedCallArguments(prepared), {
572
- userEmail: getRequestUserEmail(),
573
- orgId: getRequestOrgId() ?? null,
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();
@@ -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
- provider?: undefined;
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
- provider?: undefined;
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
- provider?: undefined;
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
- provider?: undefined;
126
+ providers?: undefined;
127
127
  count?: undefined;
128
- deleted?: undefined;
128
+ provider?: undefined;
129
129
  found?: undefined;
130
- providers?: undefined;
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
- message?: undefined;
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(20)
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
- for (const supersededId of new Set(args.supersedes ?? [])) {
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)
@@ -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
+ }