@agent-native/core 0.210.2 → 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 (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/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-session.js +2 -0
  15. package/dist/collab/awareness.d.ts +2 -2
  16. package/dist/collab/routes.d.ts +1 -1
  17. package/dist/collab/struct-routes.d.ts +1 -1
  18. package/dist/deploy/build.js +24 -3
  19. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  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/mcp/screen-memory-stdio.d.ts +7 -7
  23. package/dist/notifications/routes.d.ts +1 -1
  24. package/dist/observability/routes.d.ts +5 -5
  25. package/dist/org/actions/set-app-member-roles.js +4 -0
  26. package/dist/org/actions/set-app-permission-roles.js +4 -0
  27. package/dist/org/actions/set-workspace-app-access.js +4 -0
  28. package/dist/org/index.d.ts +2 -2
  29. package/dist/progress/routes.d.ts +1 -1
  30. package/dist/provider-api/actions/custom-provider-registration.d.ts +17 -17
  31. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  32. package/dist/resources/handlers.d.ts +1 -1
  33. package/dist/review/suggestions/actions.d.ts +1 -0
  34. package/dist/review/suggestions/actions.js +54 -42
  35. package/dist/secrets/actions/delete-api-key.js +4 -0
  36. package/dist/secrets/routes.d.ts +4 -4
  37. package/dist/server/action-approval.d.ts +15 -0
  38. package/dist/server/action-approval.js +39 -0
  39. package/dist/server/action-routes.js +33 -28
  40. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  41. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  42. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  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/transcribe-voice.d.ts +1 -1
  47. package/dist/server/workspace-provider-oauth.js +1 -0
  48. package/dist/shared/action-error-codes.d.ts +5 -0
  49. package/dist/shared/action-error-codes.js +5 -0
  50. package/dist/shared/index.d.ts +1 -0
  51. package/dist/shared/index.js +1 -0
  52. package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  53. package/dist/templates/default/.agents/skills/actions/references/action-fields.md +5 -3
  54. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  55. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  56. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  57. package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +5 -3
  58. package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +5 -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
@@ -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
+ }
@@ -3,7 +3,7 @@ import "../authorization/check-action.js";
3
3
  import { verifyA2ATokenWithClaims } from "../a2a-claims.js";
4
4
  import { isA2AIdentityVerificationUnavailableError } from "../a2a/server.js";
5
5
  import { actionChangeResource } from "../action-call-classification.js";
6
- import { actionCallIntent, ActionContractError, isActionContractError, isActionExposedToExternalAgents, isAgentActionStopError, failedCallEmitsChange, recordInvalidActionCall, preparedCallArguments, ranCallEmitsChange, runPreparedActionCall, } from "../action.js";
6
+ import { actionCallIntent, ActionContractError, isActionContractError, isActionExposedToExternalAgents, isAgentActionStopError, failedCallEmitsChange, recordInvalidActionCall, preparedCallArguments, preparedCallNeedsApproval, ranCallEmitsChange, runPreparedActionCall, } from "../action.js";
7
7
  import { LLM_CREDENTIAL_KEYS, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "../agent/engine/credential-errors.js";
8
8
  import { isTransientDatabaseError } from "../db/client.js";
9
9
  import { declaresFeatureFlagDelegation } from "../feature-flags/a2a-action-route.js";
@@ -13,12 +13,13 @@ import { isWorkspaceAppAccessUnavailable, workspaceAppAccessUnavailableBody, } f
13
13
  import { checkWorkspaceAppAccessForRequest } from "../org/workspace-app-access-request.js";
14
14
  import { ACTION_BROWSER_PERSIST_ALLOW, ACTION_BROWSER_PERSIST_HEADER, } from "../shared/action-browser-persist.js";
15
15
  import { ACTION_CHANGE_MARKER_FAILED, ACTION_CHANGE_MARKER_HEADER, } from "../shared/action-change-marker-header.js";
16
- import { LLM_PROVIDER_MISSING_ERROR_CODE, LLM_PROVIDER_MISSING_STATUS, } from "../shared/action-error-codes.js";
16
+ import { LLM_PROVIDER_MISSING_ERROR_CODE, LLM_PROVIDER_MISSING_STATUS, UI_CAPABILITY_REQUIRED_ERROR_CODE, } from "../shared/action-error-codes.js";
17
17
  import { agentNativeMcpInstructions, agentNativeToolTitle, } from "../shared/agent-mcp-metadata.js";
18
18
  import { EMBED_TARGET_HEADER, EMBED_TARGET_QUERY_PARAM, EMBED_TOKEN_QUERY_PARAM, MCP_DIRECTORY_WIDGET_SESSION_EXPIRED_HEADER, allowsMcpDirectoryWidgetReadAction, getMcpDirectoryWidgetReadCapabilityResourceIds, getMcpDirectoryWidgetWriteCapabilityGrant, isExpiredMcpDirectoryWidgetWriteCapability, isMcpDirectoryWidgetCapabilityScope, isMcpDirectoryWidgetReadCapabilityScope, isMcpDirectoryWidgetWriteCapabilityScope, normalizeMcpDirectoryWidgetReadActionArguments, normalizeMcpDirectoryWidgetWriteActionArguments, } from "../shared/embed-auth.js";
19
19
  import { isMcpEmbedCorsOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, shouldAllowMcpEmbedCredentials, } from "../shared/mcp-embed-headers.js";
20
20
  import { countActionFailure, countCredentialState, } from "../tracking/failure-counters.js";
21
21
  import { redact, redactErrorStack } from "../tracking/redaction.js";
22
+ import { actionApprovalRequiredError } from "./action-approval.js";
22
23
  import { ACTION_ROUTE_PREFIX, bindActionBatch } from "./action-batch.js";
23
24
  import { notifyActionChangeForResponse } from "./action-change.js";
24
25
  import { readBrowserSessionIdHeader, readBrowserTabIdHeader, readAnalyticsClientPlatformHeader, readSyntheticTrafficHeader, resolveAgentRunOrgId, seedAgentRunOwnerContext, } from "./agent-run-context.js";
@@ -150,6 +151,17 @@ function isFrontendActionRequest(event) {
150
151
  return false;
151
152
  }
152
153
  }
154
+ /**
155
+ * The frontend header is only the client's claim. A request proves it came
156
+ * from this app's signed-in UI with a same-origin request that carries the
157
+ * short-lived UI capability the browser minted for the same user.
158
+ */
159
+ function isProvenUiRequest(event, caller, userEmail) {
160
+ return (caller === "frontend" &&
161
+ Boolean(userEmail) &&
162
+ isSameOriginRequest(event) &&
163
+ hasUiActionCapability(event, userEmail));
164
+ }
153
165
  function getAllowedCorsOrigin(origin) {
154
166
  const allowedOrigin = resolveAllowedCorsOrigin(origin, {
155
167
  allowedOrigins: readCorsAllowedOrigins(),
@@ -793,16 +805,12 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
793
805
  : isFrontendActionRequest(event)
794
806
  ? "frontend"
795
807
  : "http");
796
- const frontendCaller = requestCaller === "frontend";
797
- if (entry.uiOnly === true &&
798
- (!frontendCaller ||
799
- !userEmail ||
800
- !isSameOriginRequest(event) ||
801
- !hasUiActionCapability(event, userEmail))) {
808
+ const provenUiRequest = isProvenUiRequest(event, requestCaller, userEmail);
809
+ if (entry.uiOnly === true && !provenUiRequest) {
802
810
  setResponseStatus(event, 403);
803
811
  return {
804
812
  error: "This action can only be called from the signed-in app UI.",
805
- errorCode: "ui_capability_required",
813
+ errorCode: UI_CAPABILITY_REQUIRED_ERROR_CODE,
806
814
  };
807
815
  }
808
816
  const timezone = readTimezoneHeader(event);
@@ -1090,25 +1098,22 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
1090
1098
  // Each reader below gets its own copy of the prepared
1091
1099
  // arguments; this one is for a failure's change marker.
1092
1100
  params = preparedCallArguments(prepared);
1093
- if (caller === "webmcp" && entry.needsApproval !== undefined) {
1094
- let mustApprove = false;
1095
- try {
1096
- mustApprove =
1097
- typeof entry.needsApproval === "function"
1098
- ? Boolean(await entry.needsApproval(preparedCallArguments(prepared), {
1099
- userEmail,
1100
- orgId: orgId ?? null,
1101
- appId: options?.appId,
1102
- caller,
1103
- }))
1104
- : entry.needsApproval === true;
1105
- }
1106
- catch {
1107
- mustApprove = true;
1108
- }
1109
- if (mustApprove) {
1110
- throw new ActionContractError(`"${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.`, { errorCode: "approval_required", statusCode: 409 });
1111
- }
1101
+ // Approval is given in the app: in its UI, or in an agent chat
1102
+ // or MCP flow that runs the action in-process. No other HTTP
1103
+ // caller can grant it. The app's UI is the proven browser app or
1104
+ // its MCP directory widget, whose capability is scoped to the
1105
+ // resources and actions the user is working on.
1106
+ if (!provenUiRequest &&
1107
+ !directoryWidgetReadAllowed &&
1108
+ !directoryWidgetWriteAllowed &&
1109
+ (await preparedCallNeedsApproval(entry, prepared, {
1110
+ userEmail,
1111
+ orgId: orgId ?? null,
1112
+ appId: options?.appId,
1113
+ caller,
1114
+ actionName: name,
1115
+ }))) {
1116
+ throw actionApprovalRequiredError(name, caller, Boolean(verifiedServiceIdentity));
1112
1117
  }
1113
1118
  const result = await runPreparedActionCall(entry, prepared, runContext, execution);
1114
1119
  // The change follows what ran in the call, with the arguments
@@ -66,7 +66,6 @@ export declare function createAgentEngineApiKeyHandler(): import("h3").EventHand
66
66
  error: any;
67
67
  code: any;
68
68
  } | {
69
- error?: undefined;
70
69
  errorCode?: undefined;
71
70
  code?: undefined;
72
71
  ok: boolean;
@@ -74,5 +73,6 @@ export declare function createAgentEngineApiKeyHandler(): import("h3").EventHand
74
73
  baseUrlKey?: string;
75
74
  scope: AgentEngineApiKeyScope;
76
75
  defaultModel?: import("./agent-engine-default-model-route.js").SavedKeyDefaultModelOutcome;
76
+ error?: undefined;
77
77
  }>>;
78
78
  export {};
@@ -8,8 +8,8 @@ export declare const AGENT_ENGINE_API_KEY_ACTION_NAME = "agent-engine-api-key";
8
8
  * resolveEngine's credential detection.
9
9
  */
10
10
  export declare function createAgentEngineDisconnectHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
11
- error: string;
12
11
  ok?: undefined;
12
+ error: string;
13
13
  scope?: undefined;
14
14
  } | {
15
15
  ok: boolean;
@@ -1,9 +1,9 @@
1
1
  export declare function createAgentEngineOllamaModelsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
2
  error: string;
3
- models?: undefined;
4
3
  ok?: undefined;
4
+ models?: undefined;
5
5
  } | {
6
+ error?: undefined;
6
7
  ok: boolean;
7
8
  models: string[];
8
- error?: undefined;
9
9
  }>>;
@@ -15,6 +15,7 @@ export { createOnboardingPlugin, defaultOnboardingPlugin, } from "../onboarding/
15
15
  export { createOrgPlugin, defaultOrgPlugin } from "../org/plugin.js";
16
16
  export { createResourcesPlugin, defaultResourcesPlugin, } from "./resources-plugin.js";
17
17
  export { getH3App, awaitBootstrap, markDefaultPluginProvided, type H3AppShim, } from "./framework-request-handler.js";
18
+ export { directHttpActionApprovalRefusal } from "./action-approval.js";
18
19
  export { runWithRequestContext } from "./request-context.js";
19
20
  export { isSameOriginRequest } from "./request-origin.js";
20
21
  export { hasUiActionCapability, mountUiActionCapabilityRoute, } from "./ui-action-capability.js";
@@ -14,6 +14,7 @@ export { createOnboardingPlugin, defaultOnboardingPlugin, } from "../onboarding/
14
14
  export { createOrgPlugin, defaultOrgPlugin } from "../org/plugin.js";
15
15
  export { createResourcesPlugin, defaultResourcesPlugin, } from "./resources-plugin.js";
16
16
  export { getH3App, awaitBootstrap, markDefaultPluginProvided, } from "./framework-request-handler.js";
17
+ export { directHttpActionApprovalRefusal } from "./action-approval.js";
17
18
  export { runWithRequestContext } from "./request-context.js";
18
19
  export { isSameOriginRequest } from "./request-origin.js";
19
20
  export { hasUiActionCapability, mountUiActionCapabilityRoute, } from "./ui-action-capability.js";
@@ -54,8 +54,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
54
54
  expiresAt?: undefined;
55
55
  ttlSeconds?: undefined;
56
56
  } | {
57
- error?: undefined;
58
57
  token: string;
59
58
  expiresAt: string;
60
59
  ttlSeconds: number;
60
+ error?: undefined;
61
61
  }>>;
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
@@ -405,6 +405,7 @@ export async function handleWorkspaceProviderOAuthCallback(event, providerId) {
405
405
  ? { externalAccountId: identity.accountId }
406
406
  : {}),
407
407
  ...(identity.config ?? {}),
408
+ workspaceConnectionScope: flow.scope,
408
409
  ...(providerId === "salesforce"
409
410
  ? {
410
411
  salesforceLoginUrl: flow.salesforceLoginUrl ?? SALESFORCE_PRODUCTION_LOGIN_URL,
@@ -5,3 +5,8 @@
5
5
  export declare const LLM_PROVIDER_MISSING_ERROR_CODE = "llm_provider_missing";
6
6
  /** 424 Failed Dependency: the request needs an LLM provider that is not connected. */
7
7
  export declare const LLM_PROVIDER_MISSING_STATUS = 424;
8
+ /**
9
+ * 403: the call needs proof it came from the app's signed-in UI. The browser
10
+ * client mints its UI capability and retries once on this code.
11
+ */
12
+ export declare const UI_CAPABILITY_REQUIRED_ERROR_CODE = "ui_capability_required";
@@ -5,3 +5,8 @@
5
5
  export const LLM_PROVIDER_MISSING_ERROR_CODE = "llm_provider_missing";
6
6
  /** 424 Failed Dependency: the request needs an LLM provider that is not connected. */
7
7
  export const LLM_PROVIDER_MISSING_STATUS = 424;
8
+ /**
9
+ * 403: the call needs proof it came from the app's signed-in UI. The browser
10
+ * client mints its UI capability and retries once on this code.
11
+ */
12
+ export const UI_CAPABILITY_REQUIRED_ERROR_CODE = "ui_capability_required";
@@ -1,6 +1,7 @@
1
1
  export { agentChat, type AgentChatMessage, type AgentChatCallOptions, type AgentChatResponse, } from "./agent-chat.js";
2
2
  export { appendAgentChatContextToMessage, splitAgentChatContextFromMessage, stripAgentChatContextFromMessage, type AgentChatMessageParts, } from "./agent-chat-context.js";
3
3
  export { agentEnv, type EnvVar } from "./agent-env.js";
4
+ export { ACTION_BROWSER_PERSIST_ALLOW, ACTION_BROWSER_PERSIST_HEADER, } from "./action-browser-persist.js";
4
5
  export { APP_STATUS, DEFAULT_APP_STATUS, getAppStatus, type AppStatus, } from "./app-status.js";
5
6
  export { extractOAuthStateAppId, extractOAuthStateProvider, } from "./oauth-state.js";
6
7
  export { isGoogleProfileImageUrl } from "./google-profile-image.js";
@@ -1,6 +1,7 @@
1
1
  export { agentChat, } from "./agent-chat.js";
2
2
  export { appendAgentChatContextToMessage, splitAgentChatContextFromMessage, stripAgentChatContextFromMessage, } from "./agent-chat-context.js";
3
3
  export { agentEnv } from "./agent-env.js";
4
+ export { ACTION_BROWSER_PERSIST_ALLOW, ACTION_BROWSER_PERSIST_HEADER, } from "./action-browser-persist.js";
4
5
  export { APP_STATUS, DEFAULT_APP_STATUS, getAppStatus, } from "./app-status.js";
5
6
  export { extractOAuthStateAppId, extractOAuthStateProvider, } from "./oauth-state.js";
6
7
  export { isGoogleProfileImageUrl } from "./google-profile-image.js";
@@ -95,9 +95,9 @@ MCP clients see no `outputSchema` by default. Set `mcpOutputSchema: true` (requi
95
95
 
96
96
  `authorize` decides whether the caller may run the action at all. It wraps
97
97
  `run`, so it holds at **all six dispatch sites** — agent tool, HTTP, frontend
98
- hook, MCP, A2A, CLI — unlike `needsApproval`, which is honoured only in the
99
- agent loop. Use `authorize` for "not everyone may do this"; use `needsApproval`
100
- to ask a human to bless one call a permitted caller is already allowed to make.
98
+ hook, MCP, A2A, CLI. Use `authorize` for "not everyone may do this"; use
99
+ `needsApproval` to ask a human to bless one call a permitted caller is already
100
+ allowed to make.
101
101
 
102
102
  ```ts
103
103
  import { coachAccess } from "../lib/access.js"; // defineAppRoles(...)
@@ -140,4 +140,6 @@ export default defineAction({
140
140
 
141
141
  When the gate is truthy and the call isn't yet approved, the loop emits an `approval_required` event and **stops the turn — `run()` never executes**. A predicate gates conditionally (e.g. only external recipients) and **fails closed**: a throw is treated as "approval required". It receives its own copy of the prepared snapshot of the validated arguments, defaults applied, and the approval binds to the snapshot's hash, so an approved call runs only with the arguments that were approved (a Date binds by its time). The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's `approvalKey`, and only then does the action run.
142
142
 
143
+ Approval is given in the app, so HTTP never grants it. The HTTP action route and the generated Workers entry run a gated call only for the proven app UI (a frontend call from the signed-in user, same origin, holding the short-lived UI capability; `isProvenUiRequest`, the proof `uiOnly` uses) or the app's MCP directory widget. Every other HTTP caller (bearer or API key, MCP OAuth token, org service token, A2A, extension iframe, mobile app) gets `409 approval_required`, and a service identity can never approve. The shared client presents the capability with every write from a signed-in session (minted once per user, reused for nine minutes, never for reads or public pages), so a gated button works on its first request. A call without proof gets `403 ui_capability_required`; the client then re-mints and retries once, which is only the fallback for an expired or rejected capability. Every surface evaluates the gate through `preparedCallNeedsApproval`, so a throwing predicate asks there too. A caller that must run unattended needs a different action, never a bypass of the gate.
144
+
143
145
  **Keep approvals rare** — the default is off and almost every action should leave it off. The canonical example is Mail's `send-email` (`needsApproval: true`). See the `security` skill and the Human Approval doc.
@@ -95,9 +95,9 @@ MCP clients see no `outputSchema` by default. Set `mcpOutputSchema: true` (requi
95
95
 
96
96
  `authorize` decides whether the caller may run the action at all. It wraps
97
97
  `run`, so it holds at **all six dispatch sites** — agent tool, HTTP, frontend
98
- hook, MCP, A2A, CLI — unlike `needsApproval`, which is honoured only in the
99
- agent loop. Use `authorize` for "not everyone may do this"; use `needsApproval`
100
- to ask a human to bless one call a permitted caller is already allowed to make.
98
+ hook, MCP, A2A, CLI. Use `authorize` for "not everyone may do this"; use
99
+ `needsApproval` to ask a human to bless one call a permitted caller is already
100
+ allowed to make.
101
101
 
102
102
  ```ts
103
103
  import { coachAccess } from "../lib/access.js"; // defineAppRoles(...)
@@ -140,4 +140,6 @@ export default defineAction({
140
140
 
141
141
  When the gate is truthy and the call isn't yet approved, the loop emits an `approval_required` event and **stops the turn — `run()` never executes**. A predicate gates conditionally (e.g. only external recipients) and **fails closed**: a throw is treated as "approval required". It receives its own copy of the prepared snapshot of the validated arguments, defaults applied, and the approval binds to the snapshot's hash, so an approved call runs only with the arguments that were approved (a Date binds by its time). The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's `approvalKey`, and only then does the action run.
142
142
 
143
+ Approval is given in the app, so HTTP never grants it. The HTTP action route and the generated Workers entry run a gated call only for the proven app UI (a frontend call from the signed-in user, same origin, holding the short-lived UI capability; `isProvenUiRequest`, the proof `uiOnly` uses) or the app's MCP directory widget. Every other HTTP caller (bearer or API key, MCP OAuth token, org service token, A2A, extension iframe, mobile app) gets `409 approval_required`, and a service identity can never approve. The shared client presents the capability with every write from a signed-in session (minted once per user, reused for nine minutes, never for reads or public pages), so a gated button works on its first request. A call without proof gets `403 ui_capability_required`; the client then re-mints and retries once, which is only the fallback for an expired or rejected capability. Every surface evaluates the gate through `preparedCallNeedsApproval`, so a throwing predicate asks there too. A caller that must run unattended needs a different action, never a bypass of the gate.
144
+
143
145
  **Keep approvals rare** — the default is off and almost every action should leave it off. The canonical example is Mail's `send-email` (`needsApproval: true`). See the `security` skill and the Human Approval doc.
@@ -21,7 +21,25 @@ Four phases, in order. Phase 0 comes before any investigation, not after.
21
21
  2. **Fix** what the evidence actually proves, at the owning boundary.
22
22
  3. **Reply** with useful questions, then recap.
23
23
 
24
- Output is fixes. For every report root this task owns, keep one concise, current same-actor status reply after the latest human message. It names every verified `✅` scope and its evidence, every open scope, and each `🎫` action and owner (or why a mark does not apply). Add and read back root `✅` as soon as direct post-change proof verifies a scope, and root `🎫` as soon as another person or team must act. An `👀` alone never completes the status. Preserve all reaction history, and follow [`slack-reaction-replies.md`](references/slack-reaction-replies.md). Do not reply on person-assigned or `:mag:` items.
24
+ Output is fixes. For every unheld in-scope report root this task owns, keep one concise, current same-actor status reply after the latest human message. It names every verified `✅` scope and its evidence, every open scope, and each `🎫` action and owner (or why a mark does not apply). Add and read back root `✅` as soon as direct post-change proof verifies a scope, and root `🎫` as soon as another person or team must act. An `👀` alone never completes the status. Preserve all reaction history, and follow [`slack-reaction-replies.md`](references/slack-reaction-replies.md). Do not reply on held, person-assigned, or `:mag:` items.
25
+
26
+ ### Read the root marks as history; use the reply for current status
27
+
28
+ - `👀` records this workflow's claim. It does not mean the report is still open.
29
+ - `✅` records a scope that passed direct post-change proof. Add it as soon as
30
+ any scope passes, even when another scope remains open; name the verified
31
+ scope and evidence in the status reply.
32
+ - `🎫` records a distinct action another person must take, including a reporter
33
+ request. Keep the reaction as history after that action is completed,
34
+ answered, or abandoned; the current reply must say whether the action is
35
+ still open or historical.
36
+ - Every owned root needs one current same-actor reply after the latest human
37
+ reply. Reuse it while the disposition and evidence remain current; do not
38
+ send a duplicate progress reply on every sweep. The reply names verified
39
+ scopes, open scopes, human actions and owners, and why either `✅` or `🎫` is
40
+ absent. An eye-only root is correct when no scope is verified and no separate
41
+ human action is pending; say whether the next step is this workflow's
42
+ investigation or no further action.
25
43
 
26
44
  ## Slack channels and ownership
27
45
 
@@ -34,44 +52,93 @@ channel. For a permalink, its channel ID identifies only where the exact linked
34
52
  root must appear; it does not limit the cutoff or channel scope. For an
35
53
  explicitly narrowed sweep, verify that the active request names every selected
36
54
  channel, then record its exact text and IDs in the separate scope-confirmation
37
- file. `#dev-agent-native-feedback` is not excluded.
55
+ file. A request can select exactly two channels by linking one report in the
56
+ first and directly asking for “the same in” the second, without saying “only.”
57
+ The checker permits one adjacent-transposition typo in that target only when it
58
+ uniquely matches a selected canonical channel; it rejects incidental or quoted
59
+ mentions and prefixes. See [`channel-inventory.md`](references/channel-inventory.md)
60
+ for the scope gate and its confirmation requirements. `#dev-agent-native-feedback`
61
+ is not excluded.
38
62
 
39
63
  In every scoped channel, run the complete bot-inclusive `slack_search_public`
40
64
  scan with only the channel filter and that channel's inclusive lower bound.
41
- Exhaust every cursor; `slack_read_channel` cannot replace this inventory. Resolve
42
- each row to its true parent. Read each report's full thread, attachments, and
43
- root reactions before classifying, claiming, or skipping it. Follow
65
+ Freeze one `windowEndTs` at sweep start and use it for every channel and
66
+ owned-mark search. Messages newer than it belong to the next scheduled sweep;
67
+ never move the boundary to chase arrivals. Exhaust every cursor;
68
+ `slack_read_channel` cannot replace this inventory. Resolve
69
+ each row to its true parent and read the root reactions before opening the thread.
70
+ If another actor owns any workflow mark (`👀`, `✅`, or `🎫`), or this actor owns
71
+ `:mag:`, apply the held-thread boundary below: do not read the thread, react, or
72
+ reply. Otherwise read the report's full thread and attachments before
73
+ classifying or claiming it. Follow
44
74
  [`channel-inventory.md`](references/channel-inventory.md) for exact query
45
- arguments, raw-page receipts, cap recovery, root classification, all-history
46
- searches for this actor's `👀`, `✅`, and `🎫`, and checker requirements.
75
+ arguments, raw-page receipts, cap recovery, root classification, fixed-window
76
+ source scans, bounded ownership searches for this actor's `👀`, `✅`, and `🎫`,
77
+ and checker requirements. Owned-mark searches use Slack's exact syntax
78
+ `hasmy::eyes:`, `hasmy::white_check_mark:`, or `hasmy::ticket:` with the channel
79
+ filter and the same inclusive `after` and `before` as that channel's source
80
+ scan. Do not widen a finite catch-up request into an all-history audit. A root
81
+ older than `after` is in scope only when an in-window reply links to it. A
82
+ missing trailing colon is not a valid ownership query.
47
83
 
48
84
  Only the invoking Slack actor's root marks are owned here. Another task's eye
49
85
  plus its current open status remains theirs; an explicit handoff naming this
50
86
  task and scope is required to transfer investigation. An eye is a permanent
51
87
  claim, not proof of open or completed work. A mark on a reply never claims or
52
- checks off its parent. Before exit, reconcile every owned root in the full
53
- inventory, including older marks: latest human reply, current status reply,
54
- verified scopes, open scopes, human/team actions, and exact root reactions.
55
- Never let a high eye count hide an unchecked verified scope or a missing
56
- current reply.
88
+ checks off its parent. Before exit, reconcile every in-window root carrying
89
+ this actor's `👀`, `✅`, or `🎫` found by the bounded searches. Also reconcile an
90
+ older root when an in-window reply links to it. For every owned root, compare
91
+ the latest human reply, current status, verified scopes, open scopes, human
92
+ actions, and exact root reactions.
93
+ Never let a high eye count hide a verified scope without `✅` or a missing
94
+ current status reply.
95
+
96
+ **Held-thread boundary:** A direct root reaction read showing another person's
97
+ `👀`, `✅`, or `🎫` means the thread belongs to that workflow; this actor's
98
+ `:mag:` means this actor opted out. Record the exact root, reaction owners, and
99
+ one reason (`foreign_workflow_mark` or `mag_opt_out`) in `ownershipSkip`, with
100
+ `Skipped` disposition. When this actor owns both `:mag:` and a workflow mark,
101
+ use `mag_opt_out` and preserve the exact root reaction-owner record as history.
102
+ Do not open/read that thread, inspect reply reactions, add or remove reactions,
103
+ or post a reply. Apply the same skip to channel reply
104
+ rows linked to that root. A linked pre-window root without source text remains
105
+ `unclassified`; do not infer its original report from the reply. This explicit
106
+ exception keeps ownership boundaries intact while allowing the rest of the
107
+ window to be reconciled. A foreign `👀` without current status is still held;
108
+ do not read it to decide whether its owner is active.
57
109
 
58
110
  Only Steve's `:mag:` and `:yes-party:` count (`U0GCV21HC`). When Steve invokes,
59
- exhaust `hasmy::mag:` and `hasmy::yes-party:` searches; otherwise inspect root
60
- reactions. `:mag:` opts out and wins over `:yes-party:`. The latter authorizes
61
- only an otherwise review-only item; it never overrides an assignment or another
111
+ exhaust `hasmy::mag:` and `hasmy::yes-party:` searches within each channel's
112
+ same inclusive cutoff; otherwise inspect root reactions in the requested
113
+ window. `:mag:` opts out and wins over `:yes-party:`. The latter authorizes only
114
+ an otherwise review-only item; it never overrides an assignment or another
62
115
  task's claim.
63
116
 
64
117
  ## Phase 0: claim and reconcile
65
118
 
66
119
  Resolve known duplicates before claiming. Check ownership, then add and read
67
120
  back this task's root `👀` before investigating an eligible unowned report.
68
- Reconcile every root this actor already owns, including eyes, checks, and
69
- handoffs from prior runs, before discovering new questions. Keep one concise
70
- canonical status by the same actor after the latest human message; read back the
71
- visible reply and root reactions after each write. Use
121
+ Reconcile only owned roots inside this invocation's fixed window and older
122
+ roots explicitly linked by an in-window reply. Never expand a finite sweep to
123
+ all historical roots just because this actor owns an old mark; widen the range
124
+ only when the invocation explicitly requests it or hands off exact roots. Finish
125
+ this bounded reconciliation before discovering new questions. Keep one
126
+ concise canonical status by the same actor after the latest human message; read
127
+ back the visible reply and root reactions after each write. Use
72
128
  [`slack-reaction-replies.md`](references/slack-reaction-replies.md) for exact
73
129
  status and reaction rules.
74
130
 
131
+ **Bounded exit gate:** enumerate the complete candidate set from the fixed
132
+ source window, the three matching owned-mark searches, and older roots linked
133
+ by in-window replies. For every unheld owned root, compare the latest human
134
+ message, current status reply, verified scopes, open scopes, and root marks.
135
+ Add and read back `✅` for each newly verified scope and `🎫` for each distinct
136
+ pending human action; the status reply must follow the latest human message and
137
+ name current evidence, remaining work, and owners. An `👀` with a verified
138
+ scope missing `✅`, a pending human action missing `🎫`, or no current status is
139
+ unfinished. Finish the complete bounded set once, then stop; do not chase
140
+ unrelated historical marks.
141
+
75
142
  Split reports into distinct scopes. Add root `✅` as soon as any named scope
76
143
  passes direct post-change proof, even while its PR is open; say **Verified
77
144
  locally** and that merge is pending. Use **Fixed** only after merge and direct
@@ -97,11 +164,14 @@ action and owner. Use **Skipped** only for non-defects.
97
164
 
98
165
  Phase 0 handles new human replies on owned roots first; never hide a newer
99
166
  reporter reply behind a mark. Then find prior unanswered questions using the
100
- exact bot disclosure and the saved channel reply cursor. On a first run, scan
101
- available history. Exhaust pages, use a one-day overlap for date-only cursors,
102
- resolve every hit to its parent, and read the full thread after checking
103
- reactions. Follow [`slack-reaction-replies.md`](references/slack-reaction-replies.md)
104
- for exact searches, legacy identity handling, and answer criteria.
167
+ exact bot disclosure and the saved channel reply cursor, bounded by this
168
+ invocation's fixed window. On a first run, use the same default five-day window;
169
+ never scan all available history to initialize the cursor. Exhaust pages, use a
170
+ one-day overlap for date-only cursors, resolve every hit to its parent, and read
171
+ the full thread after checking reactions. An older root is eligible only when
172
+ an in-window reply links to it. Follow
173
+ [`slack-reaction-replies.md`](references/slack-reaction-replies.md) for exact
174
+ searches, legacy identity handling, and answer criteria.
105
175
 
106
176
  A valid answer is a human post after the question, without this workflow's
107
177
  disclosure, that addresses the question. Rebuild the evidence and attempt the
@@ -316,15 +386,17 @@ send one concise disposition covering done, remaining, and next action/owner.
316
386
  Do not reply again to announce merge, publication, or live beta; answer direct
317
387
  reporter follow-ups. End Slack replies with `this was sent from a bot.`.
318
388
 
319
- Replies are one or two sentences: the change or question, its limit, and the next step. Never explain reactions, ledgers, or tickets. Never send **Open**/**pending** or a symptom recap. Ask for reporter input only when it changes the next step; never request supplied evidence or internal access. Keep hashes, branches, CI, publisher, and run details in the recap.
320
-
321
- That brevity rule is for a reporter-facing reply. Separately, every root this
322
- actor marks with `👀`, `✅`, or `🎫` still needs the current same-actor status
323
- reply required by the Slack reaction reference. One concise reply may serve both
324
- purposes when it answers the reporter and records the current disposition,
325
- verified/open scopes, owner/action, and any missing-proof reason. A Slack `🎫`
326
- reaction tracks a distinct unfinished human/team action; it does not mean an
327
- external tracker ticket was created or assigned. Never imply that it was.
389
+ Reporter-facing replies are one or two sentences: the change or question, its limit, and the next step. Do not explain workflow mechanics, ledgers, or external tracker tickets. Never send **Open**/**pending** or a symptom recap. Ask for reporter input only when it changes the next step; never request supplied evidence or internal access. Keep hashes, branches, CI, publisher, and run details in the recap.
390
+
391
+ Every root this actor marks with `👀`, `✅`, or `🎫` also needs the current
392
+ same-actor status reply required by the Slack reaction reference. One concise
393
+ reply may serve both purposes when it answers the reporter and records the
394
+ current disposition, verified/open scopes, owner/action, and any reason a mark
395
+ is absent. This status requirement takes precedence over the reporter-facing
396
+ brevity rule only for those facts; do not explain ledger mechanics or external
397
+ tracker tickets. A Slack `🎫` reaction tracks a distinct human action, not an
398
+ external ticket, and remains as history after the action closes. Never imply
399
+ that an external ticket was created or assigned.
328
400
 
329
401
  Keep publisher and beta checks in the internal recap after the disposition;
330
402
  they do not trigger a second reporter reply.