@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.
Files changed (68) 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/automation/index.d.ts +1 -1
  5. package/dist/client/chat/agentkit-protocol.js +48 -5
  6. package/dist/client/chat/runtime.d.ts +2 -0
  7. package/dist/client/chat/runtime.js +6 -1
  8. package/dist/client/hooks/index.d.ts +1 -1
  9. package/dist/client/hooks/index.js +1 -1
  10. package/dist/client/resources/mcp-integration-logos.js +4 -0
  11. package/dist/client/session-identity.d.ts +8 -0
  12. package/dist/client/session-identity.js +13 -0
  13. package/dist/client/use-action.d.ts +14 -0
  14. package/dist/client/use-action.js +117 -28
  15. package/dist/client/use-session.js +2 -0
  16. package/dist/collab/routes.d.ts +1 -1
  17. package/dist/deploy/build.js +24 -3
  18. package/dist/mcp/actions/revoke-org-service-token.js +5 -1
  19. package/dist/mcp/build-server.js +18 -2
  20. package/dist/mcp/execute-action-call.js +8 -17
  21. package/dist/notifications/routes.d.ts +1 -1
  22. package/dist/observability/routes.d.ts +4 -4
  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/handlers.d.ts +1 -1
  27. package/dist/org/index.d.ts +1 -1
  28. package/dist/progress/routes.d.ts +1 -1
  29. package/dist/provider-api/actions/custom-provider-registration.d.ts +17 -17
  30. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  31. package/dist/review/suggestions/actions.d.ts +1 -0
  32. package/dist/review/suggestions/actions.js +54 -42
  33. package/dist/secrets/actions/delete-api-key.js +4 -0
  34. package/dist/secrets/routes.d.ts +6 -6
  35. package/dist/server/action-approval.d.ts +15 -0
  36. package/dist/server/action-approval.js +39 -0
  37. package/dist/server/action-routes.js +33 -28
  38. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  39. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  40. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  41. package/dist/server/better-auth-instance.d.ts +6 -0
  42. package/dist/server/better-auth-instance.js +22 -12
  43. package/dist/server/edge.d.ts +1 -0
  44. package/dist/server/edge.js +1 -0
  45. package/dist/server/realtime-token.d.ts +1 -1
  46. package/dist/server/workspace-provider-oauth.js +1 -0
  47. package/dist/shared/action-error-codes.d.ts +5 -0
  48. package/dist/shared/action-error-codes.js +5 -0
  49. package/dist/shared/index.d.ts +1 -0
  50. package/dist/shared/index.js +1 -0
  51. package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  52. package/dist/templates/default/.agents/skills/actions/references/action-fields.md +5 -3
  53. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  54. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  55. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  56. package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +5 -3
  57. package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +5 -3
  58. package/dist/triggers/actions/manage-automation.d.ts +3 -3
  59. package/dist/vite/client.js +2 -1
  60. package/docs/content/actions-access-control.mdx +3 -3
  61. package/package.json +3 -3
  62. package/src/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  63. package/src/templates/default/.agents/skills/actions/references/action-fields.md +5 -3
  64. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  65. package/src/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  66. package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  67. package/src/templates/headless/.agents/skills/actions/references/action-fields.md +5 -3
  68. 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,
@@ -2094,10 +2094,23 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
2094
2094
  }
2095
2095
  : {}),
2096
2096
  });
2097
- async function withCallerContext(fn, mcpRequestId) {
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 withCallerContext(async () => {
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, 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) {
@@ -16,6 +16,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
16
16
  } | {
17
17
  count?: undefined;
18
18
  updated?: undefined;
19
- ok: boolean;
20
19
  error?: undefined;
20
+ ok: boolean;
21
21
  }>>;
@@ -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();
@@ -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;
@@ -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
  } | {
@@ -2,6 +2,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
2
2
  error: string;
3
3
  ok?: undefined;
4
4
  } | {
5
- error?: undefined;
6
5
  ok: boolean;
6
+ error?: undefined;
7
7
  }>>;
@@ -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;
@@ -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)
@@ -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
+ }