@agent-native/core 0.157.4 → 0.157.6

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 (78) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/actions/create-meeting.ts +19 -1
  3. package/corpus/templates/clips/app/components/meetings/bullet-link.tsx +48 -25
  4. package/corpus/templates/clips/app/components/meetings/canvas-editor.tsx +7 -6
  5. package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +388 -178
  6. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +7 -13
  7. package/corpus/templates/clips/app/components/transcript/transcript-segment-row.tsx +98 -0
  8. package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
  9. package/corpus/templates/clips/app/lib/public-meeting.ts +4 -0
  10. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +49 -31
  11. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +15 -1
  12. package/corpus/templates/clips/netlify.toml +4 -4
  13. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +10 -0
  14. package/corpus/templates/clips/shared/transcript-segments.ts +20 -2
  15. package/dist/collab/routes.d.ts +1 -1
  16. package/dist/collab/struct-routes.d.ts +1 -1
  17. package/dist/deploy/build.js +28 -11
  18. package/dist/notifications/routes.d.ts +1 -1
  19. package/docs/content/actions-access-control.mdx +226 -0
  20. package/docs/content/actions-advanced.mdx +152 -0
  21. package/docs/content/actions-defining.mdx +275 -0
  22. package/docs/content/actions-other-surfaces.mdx +167 -0
  23. package/docs/content/actions-overview.mdx +214 -0
  24. package/docs/content/actions-run-context.mdx +180 -0
  25. package/docs/content/agent-surfaces.mdx +1 -1
  26. package/docs/content/agent-teams.mdx +1 -1
  27. package/docs/content/agent-web-surfaces.mdx +1 -1
  28. package/docs/content/audit-log.mdx +1 -1
  29. package/docs/content/automation-connectors.mdx +1 -1
  30. package/docs/content/automations.mdx +1 -1
  31. package/docs/content/blueprint-installer.mdx +1 -1
  32. package/docs/content/cli-adapters.mdx +1 -1
  33. package/docs/content/client-agent-chat.mdx +1 -1
  34. package/docs/content/client-data.mdx +2 -2
  35. package/docs/content/client-overview.mdx +2 -2
  36. package/docs/content/components.mdx +1 -1
  37. package/docs/content/data-programs.mdx +2 -2
  38. package/docs/content/database.mdx +1 -1
  39. package/docs/content/deployment.mdx +1 -1
  40. package/docs/content/docs-components.mdx +14 -3
  41. package/docs/content/drop-in-agent.mdx +2 -2
  42. package/docs/content/durable-resume.mdx +1 -1
  43. package/docs/content/evals.mdx +1 -1
  44. package/docs/content/extensions.mdx +1 -1
  45. package/docs/content/external-agents-catalog.mdx +2 -2
  46. package/docs/content/external-agents.mdx +1 -1
  47. package/docs/content/generative-ui.mdx +1 -1
  48. package/docs/content/getting-started.mdx +1 -1
  49. package/docs/content/http-api.mdx +4 -4
  50. package/docs/content/human-approval.mdx +1 -1
  51. package/docs/content/key-concepts.mdx +1 -1
  52. package/docs/content/mcp-apps.mdx +1 -1
  53. package/docs/content/mcp-clients.mdx +1 -1
  54. package/docs/content/mcp-protocol.mdx +1 -1
  55. package/docs/content/native-chat-ui.mdx +1 -1
  56. package/docs/content/observability.mdx +1 -1
  57. package/docs/content/organizations-teams-permissions.mdx +1 -1
  58. package/docs/content/processors.mdx +1 -1
  59. package/docs/content/pure-agent-apps.mdx +1 -1
  60. package/docs/content/recurring-jobs.mdx +1 -1
  61. package/docs/content/server-database.mdx +1 -1
  62. package/docs/content/server-overview.mdx +3 -3
  63. package/docs/content/server-routes.mdx +2 -2
  64. package/docs/content/template-analytics-developers.mdx +1 -1
  65. package/docs/content/template-assets-developers.mdx +1 -1
  66. package/docs/content/template-brain-developers.mdx +1 -1
  67. package/docs/content/template-calendar-developers.mdx +1 -1
  68. package/docs/content/template-chat-first-edits.mdx +1 -1
  69. package/docs/content/template-chat.mdx +1 -1
  70. package/docs/content/template-content-developers.mdx +1 -1
  71. package/docs/content/template-forms-developers.mdx +1 -1
  72. package/docs/content/template-forms.mdx +1 -1
  73. package/docs/content/template-mail-developers.mdx +2 -2
  74. package/docs/content/toolkit-org-team.mdx +1 -1
  75. package/docs/content/tracking.mdx +1 -1
  76. package/docs/content/what-is-agent-native.mdx +1 -1
  77. package/package.json +3 -3
  78. package/docs/content/actions.mdx +0 -750
@@ -0,0 +1,226 @@
1
+ ---
2
+ title: "Access & Authorization"
3
+ description: "Who can call an action: exposure flags (agentTool, toolCallable), ctx-based scoping, accessFilter/assertAccess, and the authorize guard."
4
+ ---
5
+
6
+ # Access & Authorization
7
+
8
+ Five settings decide who can call an action: the agent, the frontend, an extension, an outside API caller, or nobody without a human's approval first. Most actions never need to touch any of these, since the defaults already fit normal UI-and-agent use.
9
+
10
+ ### Exposure flags {#exposure-flags}
11
+
12
+ All five flags default to the permissive value, so you only set one when you need to tighten a specific surface. The table below is a quick summary. The sections after it add the one detail each flag needs.
13
+
14
+ | Flag | Default | Restrictive value → who can still call | Typical use |
15
+ | --------------- | ------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------- |
16
+ | `agentTool` | `true` | `false` → UI, HTTP, and CLI only (hidden from the model, MCP, and A2A) | UI-only or programmatic actions that shouldn't take up a tool slot |
17
+ | `toolCallable` | `true` | `false` → everything **except** the sandboxed extension iframe bridge (403) | Sensitive account or org changes (delete account, change membership) |
18
+ | `publicAgent` | off (private) | `{ expose: true }` → adds the action to **public** MCP/A2A/OpenAPI surfaces | Safe read or ingest tools that don't require authentication |
19
+ | `needsApproval` | `false` | `true` → the agent **pauses**, and a human must approve the specific call | Consequential side effects (send email, charge a card, delete) |
20
+ | `authorize` | none | a guard function → only callers it accepts, on **every** surface | Operations restricted to a role (per-app RBAC, admin-only ops) |
21
+
22
+ These flags are independent, so setting one doesn't change the others. `agentTool` controls what the model sees, see [Hide from the model](#agent-tool) below. `toolCallable` controls only the extension iframe, see [Block extension iframes](#tool-callable) below. `publicAgent` adds an opt-in public surface, and a public web route never implies public tool exposure, see [Expose to public agents](#public-agent) below. `needsApproval` gates execution after the call is already made, see [Require human approval](#needs-approval) below. `authorize` decides whether this caller may run the action at all, on every dispatch path, before `run()` is entered, see [Restrict callers by role](#authorize) below.
23
+
24
+ ## Hide from the model {#agent-tool}
25
+
26
+ By default, every action is a callable agent tool. Set `agentTool: false` to remove it from every agent tool list, while keeping it behind the framework's auth and action surface. It stays callable from the UI (`useActionMutation()` or `callAction()`), the CLI, and `/_agent-native/actions/<name>`:
27
+
28
+ ```ts
29
+ export default defineAction({
30
+ description: "Persist the user's sidebar width.",
31
+ agentTool: false, // UI-only, not a tool in the model's context window
32
+ schema: z.object({ widthPx: z.number() }),
33
+ http: { method: "PUT" },
34
+ run: async ({ widthPx }) => {
35
+ /* ... */
36
+ },
37
+ });
38
+ ```
39
+
40
+ Use this when you add a UI-only or purely programmatic action, or when the UI stops using an action that would otherwise stay exposed to the model.
41
+
42
+ ## Block extension iframes {#tool-callable}
43
+
44
+ Extensions ([Alpine.js mini-apps in sandboxed iframes](/docs/extensions)) call actions through `appAction(name, params)`, running with the viewer's own permissions, secrets, and SQL scope. For sensitive operations, that's too much trust by default. Set `toolCallable: false` to make the extension bridge return a 403, while keeping the action callable from the UI, agent, CLI, MCP, and A2A:
45
+
46
+ ```ts
47
+ export default defineAction({
48
+ description: "Delete the current user's account.",
49
+ toolCallable: false, // never callable from an extension iframe
50
+ schema: z.object({ confirm: z.literal("yes") }),
51
+ run: async () => {
52
+ /* ... */
53
+ },
54
+ });
55
+ ```
56
+
57
+ Use it for actions that delete or transfer an account or org, change auth state, modify org membership, or grant share access. The framework's built-in `share-resource`, `unshare-resource`, and `set-resource-visibility` actions already opt out. The framework enforces this with a header the host sets on iframe calls, one an iframe can't fake. Regular UI, agent, CLI, MCP, and A2A calls are unaffected. See [Security](/docs/security) for details.
58
+
59
+ ## Expose to public agents {#public-agent}
60
+
61
+ Every action stays off any public protocol surface by default, even one whose web route is public. Making a page crawlable never exposes a tool. Set `publicAgent: { expose: true, readOnly: true, ... }` to opt one safe action into the public MCP, A2A, and OpenAPI surfaces:
62
+
63
+ ```ts
64
+ export default defineAction({
65
+ description: "Search published docs.",
66
+ schema: z.object({ query: z.string() }),
67
+ http: { method: "GET" },
68
+ readOnly: true,
69
+ publicAgent: {
70
+ expose: true,
71
+ readOnly: true,
72
+ requiresAuth: false,
73
+ },
74
+ run: async ({ query }) => {
75
+ /* ... */
76
+ },
77
+ });
78
+ ```
79
+
80
+ Use it only for safe, read-only, or ingest-only actions, the kind you'd let a stranger call without signing in. `requiresAuth: false` tells the public surface this action is safe to list before authentication. Leave it `true` for an action that still needs a connected caller's identity, like a per-account ingest tool. See [External Agents](/docs/external-agents) and [MCP Protocol](/docs/mcp-protocol) for the full catalog and auth model this opts into.
81
+
82
+ ## Require human approval {#needs-approval}
83
+
84
+ A handful of actions are too consequential to let the agent run on its own, like sending an email, charging a card, or deleting an account. Set `needsApproval: true` (or a predicate `(args, ctx) => boolean | Promise<boolean>`) on `defineAction()`, and the loop pauses before `run()`, requiring a human to approve that specific call. The default is **off**, and almost every action should stay that way.
85
+
86
+ See [Human-in-the-Loop Approvals](/docs/human-approval) for the predicate API, the `approval_required` event, and the full end-to-end flow.
87
+
88
+ ## Restrict callers by role {#authorize}
89
+
90
+ `authorize` answers one specific question: **may this caller run this operation at all?** Give it a guard function, and it runs before `run()` on every caller: agent tool, HTTP, frontend, MCP, A2A, and CLI.
91
+
92
+ Declare the role vocabulary once with `defineAppRoles()`:
93
+
94
+ ```ts
95
+ // server/lib/access.ts
96
+ import { defineAppRoles } from "@agent-native/core/org-team";
97
+
98
+ export const coachAccess = defineAppRoles({
99
+ appId: "coach",
100
+ roles: ["member", "coach-admin"] as const,
101
+ defaultRole: "member", // display only, never satisfies a guard
102
+ });
103
+ ```
104
+
105
+ Then guard the action with an explicit list of accepted roles:
106
+
107
+ ```ts
108
+ // actions/archive-plan.ts
109
+ import { coachAccess } from "../server/lib/access.js";
110
+
111
+ export default defineAction({
112
+ description: "Archive a training plan.",
113
+ schema: z.object({ id: z.string() }),
114
+ authorize: coachAccess.requireAny("coach-admin"),
115
+ run: async ({ id }) => {
116
+ /* ... */
117
+ },
118
+ });
119
+ ```
120
+
121
+ The guard wraps `run()` itself, instead of attaching to some other part of the action. That's because `run()` is the one thing all six dispatch sites go through, so there is no way for a caller to reach the body without passing the check. A guard that throws denies the call with its own error message. A guard that returns `false` denies the call with a generic 403. Anything else, including `undefined`, allows the call. Because the check needs a caller identity, an unattended CLI or cron invocation with no user email is denied rather than let through.
122
+
123
+ The wrappers compose in this order:
124
+
125
+ ```text
126
+ validate input -> authorize -> run -> validate output -> audit
127
+ ```
128
+
129
+ Authorization sits _inside_ input validation, so a guard that inspects `args` gets the same parsed, type-checked value that `run()` receives, not whatever raw shape the caller sent. Auditing is outermost, so a denial is still recorded.
130
+
131
+ `requireAny()` fails at definition time, not at request time. Calling it with no roles, or with a role outside the declared list, throws as soon as the module loads. That way a mistake, like `authorize: coachAccess.requireAny()` with a dropped argument, breaks right where it happened, instead of showing up later as an unexplained 403.
132
+
133
+ ### Access control in actions {#access-control}
134
+
135
+ `authorize` only answers whether the caller may run the operation at all. It says nothing about which rows they may touch, that's a separate, row-level check. A table built with `ownableColumns()` and registered with `registerShareableResource()` (see [Sharing](/docs/sharing#building)) gets two helpers for this: `accessFilter()` scopes a list or read query to the rows the caller may see, and `assertAccess()` throws before a write if the caller doesn't hold the required role on that row.
136
+
137
+ Use `accessFilter()` inside a list or read action's query:
138
+
139
+ ```ts
140
+ // actions/list-decks.ts
141
+ import { defineAction } from "@agent-native/core/action";
142
+ import { accessFilter } from "@agent-native/core/sharing";
143
+ import { z } from "zod";
144
+ import { getDb, schema } from "../server/db/index.js";
145
+
146
+ export default defineAction({
147
+ description: "List the decks the current user can see.",
148
+ schema: z.object({}),
149
+ http: { method: "GET" },
150
+ run: async () => {
151
+ const db = getDb();
152
+ return await db
153
+ .select()
154
+ .from(schema.decks)
155
+ .where(accessFilter(schema.decks, schema.deckShares));
156
+ },
157
+ });
158
+ ```
159
+
160
+ Use `assertAccess()` before an update or delete, since a `WHERE` clause alone can't stop a write to a row it excludes:
161
+
162
+ ```ts
163
+ // actions/rename-deck.ts
164
+ import { defineAction } from "@agent-native/core/action";
165
+ import { assertAccess } from "@agent-native/core/sharing";
166
+ import { eq } from "drizzle-orm";
167
+ import { z } from "zod";
168
+ import { getDb, schema } from "../server/db/index.js";
169
+
170
+ export default defineAction({
171
+ description: "Rename a deck.",
172
+ schema: z.object({ id: z.string(), title: z.string() }),
173
+ run: async ({ id, title }) => {
174
+ await assertAccess("deck", id, "editor");
175
+ const db = getDb();
176
+ await db.update(schema.decks).set({ title }).where(eq(schema.decks.id, id));
177
+ return { ok: true };
178
+ },
179
+ });
180
+ ```
181
+
182
+ A create action calls neither helper. It only needs to stamp `ownerEmail` (and `orgId`) from the request context when inserting the new row, since there's no existing row yet to check access against. See [Sharing](/docs/sharing#building) for the full registration and create pattern, and [Run Context](/docs/actions-run-context) for the full shape of the `ctx` second argument used above.
183
+
184
+ The three mechanisms are not interchangeable:
185
+
186
+ | Mechanism | Question it answers | Applies to |
187
+ | --------------------------------- | ---------------------------------------------------- | -------------------------------- |
188
+ | `authorize` | May this caller perform the operation at all? | All six dispatch sites |
189
+ | `needsApproval` | Should a human approve this specific permitted call? | The agent loop only |
190
+ | `accessFilter()`/`assertAccess()` | Which rows may this permitted caller see or write? | Inside `run()`, per query or row |
191
+
192
+ A restricted write action typically needs both `authorize` and `assertAccess()`. The first keeps non-coaches out of the operation. The second keeps a coach out of another org's plan. `needsApproval` is a separate axis. It never restricts who may call, only whether the agent may proceed unattended.
193
+
194
+ `coachAccess` above comes from `defineAppRoles()`, the per-app role overlay on org membership. See [Organizations, Teams & Permissions → App roles](/docs/organizations-teams-permissions#app-roles-per-app-rbac) for the role model and its guarantees.
195
+
196
+ ## Audit logging {#audit}
197
+
198
+ Every mutating action is audited automatically. The framework records who ran it, when, from which surface, and, when it was the agent, which thread and turn, with credential-redacted inputs. Read-only (`GET`) actions are skipped. You don't write any code for this. It happens automatically inside `defineAction()`.
199
+
200
+ Add an `audit` block only to fine-tune capture. The most common reason is to declare the resource the action changed, so the change shows up in that resource owner's trail:
201
+
202
+ ```ts
203
+ export default defineAction({
204
+ description: "Delete a recording.",
205
+ schema: z.object({ id: z.string() }),
206
+ audit: {
207
+ target: (args, result) => ({ type: "recording", id: args.id }),
208
+ summary: (args) => `Deleted recording ${args.id}`,
209
+ },
210
+ run: async (args, ctx) => {
211
+ /* ...delete... */
212
+ },
213
+ });
214
+ ```
215
+
216
+ A few other options. `audit: { onRead: true }` audits a sensitive read, like a secret access or bulk export. `audit: { enabled: false }` opts a noisy write out. `audit: { recordInputs: false }` skips capturing arguments. Read the trail back with the built-in `list-audit-events` and `get-audit-event` actions. See [Audit Log](/docs/audit-log) for full details.
217
+
218
+ ## What's next
219
+
220
+ - [**Overview**](/docs/actions-overview) — what an action is and the hello-world walkthrough
221
+ - [**Defining Actions**](/docs/actions-defining) — `defineAction()` anatomy, schema, and HTTP config
222
+ - [**Run Context**](/docs/actions-run-context) — the `ctx` argument these checks read
223
+ - [**Human-in-the-Loop Approvals**](/docs/human-approval) — the `needsApproval` gate in depth
224
+ - [**Audit Log**](/docs/audit-log) — the automatic who-changed-what trail
225
+ - [**Security**](/docs/security) — `accessFilter`/`assertAccess` and the extension iframe trust boundary
226
+ - [**Sharing**](/docs/sharing) — the framework sharing system these helpers back
@@ -0,0 +1,152 @@
1
+ ---
2
+ title: "Advanced & Legacy"
3
+ description: "Feature-flag gating for actions still rolling out, and the older CLI-only action style (export default async function(args) plus parseArgs) kept for one-off dev scripts."
4
+ ---
5
+
6
+ # Advanced & Legacy
7
+
8
+ ## Guarding staged features {#feature-flags}
9
+
10
+ Feature flags exist for one specific situation: an action is safe to deploy, but you want to turn it on for a few accounts before everyone gets it. Getting there takes three required steps, plus an optional fourth for the UI.
11
+
12
+ <Steps>
13
+
14
+ ### Declare the flag
15
+
16
+ Declare the flag in a module both the server and the client can import, since the same flag key has to reach both sides:
17
+
18
+ ```ts
19
+ // shared/feature-flags.ts
20
+ import { defineFeatureFlag } from "@agent-native/core/feature-flags";
21
+
22
+ export const FULL_APP_BUILDING = defineFeatureFlag({
23
+ key: "full-app-building",
24
+ displayName: "Full app building",
25
+ description: "Create and edit Fusion-backed applications.",
26
+ });
27
+ ```
28
+
29
+ `key` is the only field every other piece of code will reference, so it needs to be stable. `displayName` and `description` only show up later, in the operator UI where someone actually turns the flag on. Declaring the flag here doesn't enable anything. Every flag starts out off for every account.
30
+
31
+ ### Register it at startup
32
+
33
+ Register the flag with a Nitro plugin so the running server knows it exists:
34
+
35
+ ```ts
36
+ // server/plugins/feature-flags.ts
37
+ import { createFeatureFlagsPlugin } from "@agent-native/core/server";
38
+
39
+ import { FULL_APP_BUILDING } from "../../shared/feature-flags.js";
40
+
41
+ export default createFeatureFlagsPlugin({ flags: [FULL_APP_BUILDING] });
42
+ ```
43
+
44
+ An unregistered flag always evaluates to `false`, no matter what an operator later sets. This step is what makes the flag real to the server. It still doesn't turn the flag on for anyone. That happens through the operator actions described at the end of this section.
45
+
46
+ ### Guard the action
47
+
48
+ This is the check that actually matters, since every caller, whether that's the agent, the frontend, or a script, has to go through it:
49
+
50
+ ```ts
51
+ // actions/create-fusion-app.ts
52
+ import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
53
+
54
+ run: async (args, ctx) => {
55
+ if (!(await isFeatureFlagEnabled(FULL_APP_BUILDING, ctx))) {
56
+ throw new Error("Full app building is not enabled for this account.");
57
+ }
58
+ // Perform the guarded operation.
59
+ };
60
+ ```
61
+
62
+ `isFeatureFlagEnabled()` reads the caller's identity off `ctx` (see [the ctx argument](/docs/actions-run-context#ctx)) and evaluates the flag for that specific user and org. If the flag isn't on for them, the action throws before doing anything else. This check has to live inside `run()`, because `run()` is the one place every dispatch path is guaranteed to pass through.
63
+
64
+ ### Hide it in the UI (optional)
65
+
66
+ Use the same flag on the client to hide the feature from people who can't call it anyway:
67
+
68
+ ```tsx
69
+ // app/routes/fusion.tsx
70
+ import { Button } from "@/components/ui/button";
71
+ import { useFeatureFlag } from "@agent-native/core/client";
72
+
73
+ import { FULL_APP_BUILDING } from "../../shared/feature-flags.js";
74
+
75
+ export function CreateFusionAppButton() {
76
+ const enabled = useFeatureFlag(FULL_APP_BUILDING.key);
77
+ if (!enabled) return null;
78
+ return <Button onClick={createFusionApp}>Create Fusion app</Button>;
79
+ }
80
+ ```
81
+
82
+ `useFeatureFlag()` returns a plain boolean, evaluated for the signed-in user the same way the server-side check is. Hiding the button when it's `false` keeps someone from clicking into a feature they can't use yet, but it's presentation only. Nothing about it stops a direct call to the action. That's exactly why the check in `run()` from the previous step still has to be there, regardless of what the UI shows.
83
+
84
+ </Steps>
85
+
86
+ Once a flag is declared and registered, turning it on for real accounts goes through three mounted actions. `get-feature-flags` is the one `useFeatureFlag()` itself calls: it's open to any signed-in caller, since every user needs to check their own flags. `list-feature-flags` and `set-feature-flag` are the two management actions. Both are restricted to an organization's owners and admins (or the no-org administrator), and both are audited.
87
+
88
+ `set-feature-flag` is how an operator actually changes who the flag is on for:
89
+
90
+ | Operation | What it does | Example args |
91
+ | ------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
92
+ | `enable-for-current-user` | Turns the flag on for your own account only. | `{ key: "full-app-building", operation: "enable-for-current-user" }` |
93
+ | `off` | Turns the flag off for everyone, resetting any rules. | `{ key: "full-app-building", operation: "off" }` |
94
+ | `replace-rules`, mode `"on"` | Turns the flag on for everyone. | `{ key: "full-app-building", operation: "replace-rules", rules: { mode: "on" } }` |
95
+ | `replace-rules`, mode `"rules"` with `emails` | Turns it on for a specific list of accounts. | `{ key: "full-app-building", operation: "replace-rules", rules: { mode: "rules", emails: ["a@example.com"] } }` |
96
+ | `replace-rules`, mode `"rules"` with `orgIds` | Turns it on for specific organizations. | `{ key: "full-app-building", operation: "replace-rules", rules: { mode: "rules", orgIds: ["org_123"] } }` |
97
+ | `replace-rules`, mode `"rules"` with `percentage` | Turns it on for a percentage rollout. | `{ key: "full-app-building", operation: "replace-rules", rules: { mode: "rules", percentage: 10 } }` |
98
+
99
+ Fleet operator UIs call these same actions through narrowly scoped A2A delegation instead of a shared login, and Analytics is the sole operator UI for them. Apps should not build their own flag settings panel, and flag writes should never route through generic settings endpoints or raw SQL.
100
+
101
+ <Callout id="doc-block-actions-advanced-1" tone="warning">
102
+
103
+ Never put authentication, authorization, audit, secrets, or SSR cache behavior behind a runtime flag. Those need to be correct unconditionally, not toggled.
104
+
105
+ </Callout>
106
+
107
+ ## Legacy CLI-style actions {#legacy-cli-actions}
108
+
109
+ The framework still supports older `export default async function(args)` actions that aren't wrapped in `defineAction()`. These are useful for one-off dev scripts that don't need agent or HTTP exposure. They're CLI-only: they don't appear as agent tools, don't mount HTTP endpoints, and don't get typesafe frontend hooks.
110
+
111
+ ```ts
112
+ // actions/debug-dump.ts (CLI-only)
113
+ import { parseArgs } from "@agent-native/core";
114
+
115
+ export default async function main(args: string[]) {
116
+ const { table } = parseArgs(args);
117
+ // one-off script you wouldn't want the agent to call
118
+ }
119
+ ```
120
+
121
+ New code should prefer `defineAction()`. Reach for this pattern only when you deliberately don't want the action exposed to agents or the UI.
122
+
123
+ ### `parseArgs(args)` {#parseargs}
124
+
125
+ Helper for legacy-style actions. Parses CLI arguments in `--key value` or `--key=value` format:
126
+
127
+ ```ts
128
+ import { parseArgs } from "@agent-native/core";
129
+
130
+ const args = parseArgs(["--name", "Steve", "--verbose", "--count=3"]);
131
+ // { name: "Steve", verbose: "true", count: "3" }
132
+ ```
133
+
134
+ ## Utility functions {#utility-functions}
135
+
136
+ These are small helpers for writing scripts, from the same `@agent-native/core` import `parseArgs()` above comes from. They aren't tied to the legacy CLI-style action style specifically. Reach for them in any one-off dev script: loading environment variables, validating a path argument, or aborting with a clear error.
137
+
138
+ | Function | Returns | Description |
139
+ | ----------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
140
+ | `loadEnv(path?)` | `void` | Load `.env` (and `.env.local`) from the project root or a custom path. Falls back to the workspace root's `.env` in an enterprise workspace. |
141
+ | `camelCaseArgs(args)` | `Record<string, string>` | Convert kebab-case keys to camelCase. |
142
+ | `isValidPath(p)` | `boolean` | Validate a relative path (no traversal, no absolute). |
143
+ | `isValidProjectPath(p)` | `boolean` | Validate a project slug (e.g. `my-project` or `group/my-project`). |
144
+ | `ensureDir(dir)` | `void` | `mkdir -p` helper. |
145
+ | `fail(message)` | `never` | Throw an error to abort a script. The CLI runner catches it and exits with code 1. In-server callers (agent tools, A2A) get it back as a tool-call error instead. |
146
+
147
+ ## What's next
148
+
149
+ - [**Defining Actions**](/docs/actions-defining) — the `defineAction()` pattern this legacy style predates
150
+ - [**Overview**](/docs/actions-overview) — what an action is and the hello-world walkthrough
151
+ - [**Other Surfaces**](/docs/actions-other-surfaces) — the CLI runner these utilities and legacy actions support
152
+ - [**Feature Flags**](/docs/feature-flags) — the full flag lifecycle beyond gating a single action
@@ -0,0 +1,275 @@
1
+ ---
2
+ title: "Defining Actions"
3
+ description: "The defineAction() anatomy: schema, run, and HTTP config, plus how to keep the action surface small."
4
+ ---
5
+
6
+ # Defining Actions
7
+
8
+ Every action starts with a call to `defineAction()`. It bundles a description for the agent, a schema that validates input, and a `run` function that does the work, so the UI and the agent call the exact same code.
9
+
10
+ <AnnotatedCode
11
+ id="doc-block-actions-defining-1"
12
+ title="Anatomy of an action"
13
+ filename="actions/reply-to-email.ts"
14
+ language="ts"
15
+ code={
16
+ 'import { defineAction } from "@agent-native/core/action";\nimport { z } from "zod";\n\nimport { getDb, schema } from "../server/db/index.js";\n\nexport default defineAction({\n description: "Reply to an email thread in the user\'s voice.",\n schema: z.object({\n emailId: z.string().describe("The id of the email to reply to."),\n body: z.string().describe("The reply body, in markdown."),\n }),\n run: async ({ emailId, body }) => {\n const db = getDb();\n await db.insert(schema.replies).values({ emailId, body });\n return { ok: true, emailId };\n },\n});'
17
+ }
18
+ annotations={[
19
+ {
20
+ lines: "7",
21
+ label: "Tool surface",
22
+ note: "`description` is what the agent reads to decide when to call this. The per-field `.describe()` calls flow into the JSON Schema too.",
23
+ },
24
+ {
25
+ lines: "8-11",
26
+ label: "Typed contract",
27
+ note: "One schema validates input from **every** surface and is converted to JSON Schema for the model. Invalid inputs never reach `run`.",
28
+ },
29
+ {
30
+ lines: "12-16",
31
+ label: "One implementation",
32
+ note: "The `run` body is the single source of truth. The UI button and the agent tool both execute exactly this. `getDb()` and `schema` come from your app's `server/db/index.ts`. See [Database](/docs/database).",
33
+ },
34
+ ]}
35
+ />
36
+
37
+ The framework auto-discovers every file in `actions/` and mounts it on startup. There's nothing else to register by hand.
38
+
39
+ <Signature
40
+ id="doc-block-actions-defining-2"
41
+ name="defineAction"
42
+ params={[
43
+ {
44
+ name: "config",
45
+ type: "DefineActionConfig<TSchema, TReturn, TOutputSchema>",
46
+ description:
47
+ "Everything below is one config object. Only description, schema, and run are required.",
48
+ fields: [
49
+ {
50
+ name: "description",
51
+ type: "string",
52
+ description:
53
+ "What the agent reads to decide when to call this action.",
54
+ },
55
+ {
56
+ name: "schema",
57
+ type: "StandardSchemaV1",
58
+ description:
59
+ "Validates input on every surface. See Schema options below.",
60
+ },
61
+ {
62
+ name: "run",
63
+ type: "(args, ctx?) => Promise<TReturn> | TReturn",
64
+ description:
65
+ "The single implementation every surface calls. See Run Context for the ctx argument.",
66
+ },
67
+ {
68
+ name: "http",
69
+ type: "ActionHttpConfig | false",
70
+ optional: true,
71
+ description:
72
+ "Overrides the mounted method/path, or disables the HTTP endpoint entirely. See HTTP config below.",
73
+ },
74
+ {
75
+ name: "readOnly",
76
+ type: "boolean",
77
+ optional: true,
78
+ default: "false",
79
+ description:
80
+ "Auto-true when http.method is GET. Skips the UI poll-refresh and write-auditing. Not access control.",
81
+ },
82
+ {
83
+ name: "outputSchema",
84
+ type: "StandardSchemaV1",
85
+ optional: true,
86
+ description:
87
+ "Validates what run() returns. See Validating the return value below.",
88
+ },
89
+ {
90
+ name: "outputErrorStrategy",
91
+ type: '"warn" | "strict" | "fallback"',
92
+ optional: true,
93
+ default: '"warn"',
94
+ description: "What happens on an output mismatch.",
95
+ },
96
+ {
97
+ name: "outputFallback",
98
+ type: "TReturn",
99
+ optional: true,
100
+ description:
101
+ 'Returned in place of an invalid result when outputErrorStrategy is "fallback".',
102
+ },
103
+ {
104
+ name: "parallelSafe",
105
+ type: "boolean",
106
+ optional: true,
107
+ description:
108
+ "Allows a mutating action to run concurrently with other same-turn tool calls.",
109
+ },
110
+ {
111
+ name: "agentTool",
112
+ type: "boolean",
113
+ optional: true,
114
+ default: "true",
115
+ description:
116
+ "Set false to hide from every agent tool list. See Access & Authorization.",
117
+ },
118
+ {
119
+ name: "toolCallable",
120
+ type: "boolean",
121
+ optional: true,
122
+ default: "true",
123
+ description:
124
+ "Set false to block the sandboxed extension iframe bridge. See Access & Authorization.",
125
+ },
126
+ {
127
+ name: "publicAgent",
128
+ type: "PublicAgentActionConfig",
129
+ optional: true,
130
+ description:
131
+ "{ expose: true } adds the action to public MCP/A2A/OpenAPI surfaces. See Access & Authorization.",
132
+ },
133
+ {
134
+ name: "needsApproval",
135
+ type: "boolean | ((args, ctx?) => boolean | Promise<boolean>)",
136
+ optional: true,
137
+ default: "false",
138
+ description:
139
+ "Pauses the agent loop for human approval before run(). See Human-in-the-Loop Approvals.",
140
+ },
141
+ {
142
+ name: "authorize",
143
+ type: "ActionAuthorize<TInput>",
144
+ optional: true,
145
+ description:
146
+ "Guard function deciding whether this caller may run the action at all. See Access & Authorization.",
147
+ },
148
+ {
149
+ name: "audit",
150
+ type: "ActionAuditConfig",
151
+ optional: true,
152
+ description:
153
+ "Tunes the automatic audit capture every mutating action already gets. See Audit Log.",
154
+ },
155
+ {
156
+ name: "chatUI",
157
+ type: "ActionChatUIConfig",
158
+ optional: true,
159
+ description:
160
+ "Declares a native chat UI renderer for the action's result. See Other Surfaces.",
161
+ },
162
+ ],
163
+ },
164
+ ]}
165
+ returns={{
166
+ type: "ActionDefinition<TInput, TReturn>",
167
+ description:
168
+ "The typed action definition. Export it as the file's default export.",
169
+ }}
170
+ />
171
+
172
+ ## Schema options {#schemas}
173
+
174
+ `schema` accepts any [Standard Schema](https://standardschema.dev)-compatible library:
175
+
176
+ - **Zod** (v4): the most common choice, with the best type inference and automatic conversion to JSON Schema.
177
+ - **Valibot**: a good pick when bundle size matters.
178
+ - **ArkType**: a good pick if you prefer its syntax.
179
+
180
+ The schema does two jobs. It converts to JSON Schema for the Claude API tool definition, and it validates input at runtime before `run()` fires. Invalid input never reaches your handler.
181
+
182
+ ## Validating the return value {#output-schema}
183
+
184
+ `schema` validates _inputs_. To also validate what an action **returns**, pass an `outputSchema` (any Standard Schema-compatible schema: Zod, Valibot, ArkType, the same surface as `schema`). The framework validates input before `run()` and validates the result after `run()` resolves.
185
+
186
+ ```ts
187
+ export default defineAction({
188
+ description: "Summarize a thread.",
189
+ schema: z.object({ threadId: z.string() }),
190
+ outputSchema: z.object({
191
+ summary: z.string(),
192
+ messageCount: z.number(),
193
+ }),
194
+ outputErrorStrategy: "warn", // default
195
+ run: async ({ threadId }) => {
196
+ /* ...returns { summary, messageCount } ... */
197
+ },
198
+ });
199
+ ```
200
+
201
+ `outputErrorStrategy` controls what happens on a mismatch:
202
+
203
+ | Strategy | Behavior on mismatch |
204
+ | ------------ | -------------------------------------------------------------------------------------------------- |
205
+ | `"warn"` | **Default.** `console.warn` the issues and return the **original** result unchanged. Non-breaking. |
206
+ | `"strict"` | Throw a clear error, so a bug in the action is easy to notice. |
207
+ | `"fallback"` | Return the provided `outputFallback` value in place of the invalid result. |
208
+
209
+ When `run()` succeeds, the framework returns the validated value, so any defaults or coercions on `outputSchema` take effect, the same way they do for input. When you don't pass an `outputSchema`, the return value is unchanged.
210
+
211
+ ## HTTP config {#http}
212
+
213
+ By default every action is exposed as `POST /_agent-native/actions/<name>`. Override with the `http` option:
214
+
215
+ ```ts
216
+ export default defineAction({
217
+ description: "Get details for a lead.",
218
+ schema: z.object({ leadId: z.string() }),
219
+ http: { method: "GET" },
220
+ run: async ({ leadId }) => {
221
+ return await db.select().from(leads).where(eq(leads.id, leadId));
222
+ },
223
+ });
224
+ ```
225
+
226
+ For a `GET` action, `leadId` is passed as a query param: `/_agent-native/actions/get-lead?leadId=abc`.
227
+
228
+ <Endpoint id="doc-block-actions-defining-3" title="The auto-mounted action endpoint" method="GET" path="/_agent-native/actions/get-lead" summary={"Every action is mounted here automatically. The filename is the action name."} auth={"Session cookie (frontend calls carry `X-Agent-Native-Frontend: 1`), or `Authorization: Bearer <token>` for external callers"} params={[
229
+ {
230
+ "name": "leadId",
231
+ "in": "query",
232
+ "type": "string",
233
+ "required": true,
234
+ "description": "GET args arrive as query params. POST args arrive in the JSON body."
235
+ }
236
+ ]} responses={[
237
+ {
238
+ "status": "200",
239
+ "description": "The action's return value as JSON."
240
+ },
241
+ {
242
+ "status": "400",
243
+ "description": "Input failed schema validation before run() fired."
244
+ }
245
+ ]}>
246
+
247
+ POST is the default, and `http: { method: "GET" }` makes it a GET. The React hooks and `callAction()` always call this path by name, regardless of any `http.path` override. External systems can call the same route with a long-lived bearer token. See [HTTP API](/docs/http-api).
248
+
249
+ </Endpoint>
250
+
251
+ - **`http: { method: "GET" | "POST" | "PUT" | "DELETE" }`**: defaults to `POST`. `GET` actions are auto-marked `readOnly`, so a successful call doesn't trigger a UI poll-refresh.
252
+ - **`http: { path: "..." }`**: overrides the mounted URL under `/_agent-native/actions/`. It defaults to the filename. This override only changes the URL for direct HTTP callers. `useActionQuery()`, `useActionMutation()`, and `callAction()` always call `/_agent-native/actions/<name>`, regardless of this override, so overriding the path makes those hooks 404. Use a path override only for external HTTP callers. Also note that `:param` route segments in the override path are not parsed into `run()` args. Only query-string params and JSON body fields are.
253
+ - **`http: false`**: disables the HTTP endpoint entirely. Only the agent and CLI can call the action.
254
+ - **`readOnly: true`**: explicitly skips the poll-refresh, even for POST actions that don't mutate anything. This is not access control. It doesn't restrict who may call the action, and it doesn't affect `useActionQuery()`, `useActionMutation()`, or any client hook. Its only effects are suppressing the post-call refresh event and opting the action out of write-auditing. To hide an action from the model, use `agentTool: false`. To gate who can call it, use the auth and access options on [Access & Authorization](/docs/actions-access-control).
255
+ - **`parallelSafe: true`**: allows a mutating action to run at the same time as other tool calls in the same turn. Only set this when the action is safe to run concurrently and doesn't depend on order. Mutating actions serialize by default.
256
+
257
+ ## Keep the action surface small {#small-surface}
258
+
259
+ Every action the agent can see takes up a slot in the model's tool list, and a long, overlapping list makes it harder for the model to pick the right tool. Design the action surface like an API you maintain, not one action per button in the UI:
260
+
261
+ - Prefer **one CRUD-style `update` action** that takes a patch of optional fields, over many per-field actions like `update-name`, `update-order`, and `update-color`. The caller only sends the fields that changed.
262
+ - Before adding a new read action for every query or filter, consider a more general option first: the [provider API trio](/docs/template-dispatch-vault-integrations#provider-api) (`provider-api-catalog`, `provider-api-docs`, and `provider-api-request`) for provider data, or the dev `db-query` tool for app data.
263
+ - Mark UI-only or programmatic actions [`agentTool: false`](/docs/actions-access-control#agent-tool). They stay callable from the frontend or over HTTP, without taking a slot in the model's tool list.
264
+ - Delete or hide actions the UI no longer uses, instead of leaving them exposed to the model.
265
+
266
+ The repo includes an advisory helper, `node scripts/audit-template-actions.mjs [template ...]` (alias `pnpm actions:audit`). It scans a template's `actions/` folder and flags actions the UI may no longer use, along with groups of per-field actions that could be combined into one. It always exits with code 0 and never fails CI, and its heuristics are conservative, so treat its output as a suggestion, not an error.
267
+
268
+ ## What's next
269
+
270
+ - [**Overview**](/docs/actions-overview) — what an action is and the hello-world walkthrough
271
+ - [**Access & Authorization**](/docs/actions-access-control) — `agentTool`, `toolCallable`, `authorize`, and `ctx` scoping
272
+ - [**Run Context**](/docs/actions-run-context) — the full `ctx` field table
273
+ - [**Other Surfaces**](/docs/actions-other-surfaces) — native chat UI, CLI, A2A, MCP, and standard actions
274
+ - [**Database**](/docs/database) — `getDb()`/`schema` used inside `run()`
275
+ - [**Data & Sync**](/docs/client-data) — calling this action from the UI