@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
@@ -3,15 +3,27 @@
3
3
  Use this procedure before investigating, claiming, or using a CI rollup in
4
4
  place of Slack source messages.
5
5
 
6
+ At the start of a sweep, capture one Slack timestamp as `windowEndTs`. Use it
7
+ as the inclusive `before` argument on the source scan and all three `hasmy`
8
+ ownership searches in every scoped channel. Use the same inclusive `after` on
9
+ the source scan and ownership searches. Record the bounds in `scanQuery` and
10
+ each owned search's `query`. Keep `after` fixed through the sweep. Keep the
11
+ exact upper bound for the whole sweep; page-cap recovery slices may narrow only
12
+ `before` using the existing one-microsecond boundary rule. Messages newer than
13
+ `windowEndTs` belong to the next scheduled sweep. Never advance either bound to
14
+ chase arrivals or expand a finite catch-up request into an all-history audit.
15
+
6
16
  - For the exhaustive channel source scan, use `slack_search_public` with only
7
- `filters=in:<#CHANNEL>`, `after=<channel cutoff>`, `keywords=[]`, an empty
17
+ `filters=in:<#CHANNEL>`, `after=<channel cutoff>`,
18
+ `before=<windowEndTs>`, `keywords=[]`, an empty
8
19
  `natural_language_query`, `include_bots=true`, `include_context=false`,
9
20
  `content_types=messages`, `limit=20`, `sort=timestamp`, `sort_dir=desc`, and
10
21
  `response_format=detailed`. Do not add lexical, semantic, date, author, or
11
22
  reaction filters: they silently hide reports. Record these exact arguments
12
23
  under that channel's `scanQuery`; the inventory checker rejects a missing or
13
- narrowed query. `after` is inclusive and must equal that channel's effective
14
- cutoff. Follow every `next_cursor` newest-first to Slack exhaustion, even
24
+ narrowed query. `after` and `before` are inclusive and must equal that
25
+ channel's effective lower cutoff and the sweep's fixed upper bound. Follow
26
+ every `next_cursor` newest-first to Slack exhaustion, even
15
27
  after reaching the cutoff, because later pages can contain replies to older
16
28
  roots. Page and
17
29
  source-row timestamps must descend across the full chain. This is separate
@@ -50,9 +62,18 @@ place of Slack source messages.
50
62
  `ts >= effectiveCutoffTs`, and record the exact `oldest` in `oldestQuery`.
51
63
  - Record `scope.channelIds` as the exact IDs requested and scanned. The default
52
64
  sweep lists all three default feedback channels. For an explicitly narrowed
53
- invocation, inspect the active user request itself; it must explicitly limit
54
- scope and name every selected channel by exact ID or `#channel-name`. A
55
- permalink or time anchor does not narrow channel scope. Ask the user if the
65
+ invocation, inspect the active user request itself. It can name the exact
66
+ channel set with an explicit limit such as “only” and exact IDs or
67
+ `#channel-name`s. It can also select exactly two channels by linking one
68
+ report permalink in the first channel and directly asking for “the same in
69
+ channel-name” (with or without `#`) in the second; that direct linked-report
70
+ form selects exactly those two channels and does not need an exclusivity
71
+ word. It requires exactly one linked root and one direct “same in” target. In
72
+ that clause only, accept a single adjacent transposition in the channel name
73
+ only when it uniquely resolves to one canonical channel in the requested
74
+ set. Do not infer scope from incidental or quoted channel mentions or a
75
+ channel-name prefix. A permalink or time anchor by itself does not narrow
76
+ channel scope. Ask the user if the
56
77
  requested channels are unclear. Store the exact instruction in
57
78
  `scope.userRequest` and `scope.narrowingJudgment`, then create a separate
58
79
  `.tmp/feedback-scope-confirmation.json` from that active request with
@@ -85,10 +106,12 @@ place of Slack source messages.
85
106
  `administrative` decision, and a human rationale tied to that text. This is
86
107
  an attributed human triage judgment; the checker verifies its presence and
87
108
  source-text linkage but cannot prove the interpretation is correct.
88
- For every classified root, including administrative posts, capture a
109
+ For every unheld classified root, including administrative posts, capture a
89
110
  complete thread read and make `sourceText` exactly match its `rootText`.
90
- Search output can confuse user-authored `Files:` or `Reactions:` lines with
91
- connector metadata, so do not classify from a truncated search excerpt.
111
+ A held root keeps the exact channel-scan `sourceText` and direct reaction
112
+ evidence but has no thread-derived fields. Search output can confuse
113
+ user-authored `Files:` or `Reactions:` lines with connector metadata, so do
114
+ not classify from a truncated search excerpt.
92
115
  For a canonical Slack message permalink, inspect its `thread_ts` query value:
93
116
  if it differs from the message's `ts`, the message is a reply and that value
94
117
  is its root. A self-valued `thread_ts` or a canonical root permalink without
@@ -194,9 +217,9 @@ place of Slack source messages.
194
217
  root message.
195
218
  Read and capture a complete, oldest-first `threadSnapshot` after the final
196
219
  channel page for every in-scope classified root, including administrative
197
- posts, unmarked roots, and roots with no replies. The checker rejects a
198
- classified root without this read or with `rootText` that differs from its
199
- `sourceText`.
220
+ posts, unmarked roots, and roots with no replies, except roots held by the
221
+ reaction ownership boundary below. The checker rejects a classified root
222
+ without this read or with `rootText` that differs from its `sourceText`.
200
223
  Supplemental roots must strictly predate the oldest `sourceTimestamps` entry
201
224
  across every scanned page, as well as `cutoffTs`; a root inside the scanned
202
225
  source range must have its own source-manifest row even when it predates the
@@ -205,8 +228,8 @@ place of Slack source messages.
205
228
  `ts === threadParentTs` and must not be duplicated in `threadRoots`. A
206
229
  supplemental root exists to resolve an in-window reply;
207
230
  its presence does not make the older report a new in-scope claim. Read its
208
- exact root reactions and complete thread, then apply the ownership gate to
209
- the root itself. Do not add `👀`, `✅`, or `🎫`, or post a status, just because
231
+ exact root reactions, then apply the ownership gate before opening the thread.
232
+ Do not add `👀`, `✅`, or `🎫`, or post a status, just because
210
233
  an unowned older root appears as context. If this actor already owns a mark
211
234
  on that older root, reconcile its current status and scopes as usual; record
212
235
  another actor's marks as ownership evidence and leave them untouched. Their
@@ -214,29 +237,53 @@ place of Slack source messages.
214
237
  shows an open disposition; a later closed status makes that eye historical.
215
238
  Keep
216
239
  the root disposition and any open scope in the audit row without claiming it.
217
- The inventory checker still requires a complete, oldest-first
218
- `threadSnapshot` for each supplemental root, including unowned roots, so a
219
- reply cannot predate or otherwise contradict its reported root.
220
- - After the regular bounded channel scans, discover this actor's older owned
221
- roots with three separate unbounded `slack_search_public` queries per scoped
222
- channel: `in:<#CHANNEL> hasmy::eyes`,
223
- `in:<#CHANNEL> hasmy::white_check_mark`, and
224
- `in:<#CHANNEL> hasmy::ticket`. Use the same bot-inclusive detailed search
225
- arguments as the channel scan, but omit `after`, `before`, keywords, semantic
226
- search, and all other filters. Follow every cursor to explicit exhaustion. If
227
- Slack caps a cursor before exhaustion, preserve that capped page and record
228
- the exact `page_limit_exceeded` response in that search's
240
+ For an unheld supplemental root, the inventory checker still requires a
241
+ complete, oldest-first `threadSnapshot`, so a reply cannot predate or
242
+ otherwise contradict its reported root.
243
+ - **Held thread:** If the exact root reaction read shows another actor owns any
244
+ workflow mark (`👀`, `✅`, or `🎫`), or this actor owns `:mag:`, do not open or
245
+ read the thread, inspect reply reactions, react, or reply. Record an
246
+ `ownershipSkip` with the root timestamp and `foreign_workflow_mark` or
247
+ `mag_opt_out`; use `Skipped` and leave thread-derived fields empty. If this
248
+ actor also owns a workflow mark, use `mag_opt_out` and preserve the exact
249
+ direct root reaction-owner record as history. The checker treats it as held
250
+ and requires no thread snapshot or status. Mark every
251
+ channel reply linked to that root with the same skip reference. The checker
252
+ requires direct root reaction evidence and the matching skip reason, but no
253
+ full-thread snapshot. For a linked pre-window root without source text, use
254
+ `unclassified`; do not guess from its in-window reply. This boundary also
255
+ applies when the other actor's eye has no visible status reply: ownership is
256
+ not permission to inspect. The direct root-reaction read must still be fresh
257
+ and after the final source page; it is the only read allowed for a held root.
258
+ Preserve the source's exact file ID, name, and MIME type when its raw search
259
+ result includes `Files:`. If that result omits file metadata, leave the held
260
+ root's `files` field absent to represent unknown; do not record `[]`. For an
261
+ unheld root, use the full thread read to record files and require the source
262
+ row and snapshot to agree.
263
+ - After each bounded channel scan, run three separate bounded ownership
264
+ searches for this actor's `👀`, `✅`, and `🎫` with `slack_search_public`. Each
265
+ query uses only `in:<#CHANNEL> hasmy::EMOJI:` plus the same inclusive `after`
266
+ and `before` as the source scan. These searches cover only this invocation's
267
+ finite window; do not search unrelated channel history. An older root is in
268
+ scope only when an in-window reply links to it. Resolve that reply's true
269
+ parent, read the older root's exact reactions, and apply the held-thread gate
270
+ before opening it. Reconcile every candidate in the window and each linked
271
+ older root; unheld roots need exact reaction and full-thread evidence in the
272
+ source or supplemental-root audit. A candidate missing either audit is
273
+ unresolved, not outside scope.
274
+ If Slack caps a cursor before exhaustion, preserve that capped page and
275
+ record the exact `page_limit_exceeded` response in that search's
229
276
  `pageLimitRecoveries[]`, including its failed cursor and request args, raw
230
277
  response, read time, and the capped page's oldest result as `boundaryTs`.
231
- Restart the same exact `hasmy` query at `cursor: null` with an inclusive
232
- `before` exactly one microsecond below that boundary. Keep that `before` on
233
- every page in the restarted cursor slice and in each raw request wrapper; the
234
- base `filters` query remains unchanged. Follow the restarted slice to
235
- exhaustion, and repeat this recovery for every later cap. The checker proves
236
- each failed cursor against exactly one restarted slice, checks the boundary
237
- and read order, and requires explicit exhaustion. If Slack does not let you
238
- recover a capped tail, report the historical ownership search as incomplete;
239
- do not claim every owned mark was reconciled.
278
+ Restart the same `hasmy` query at `cursor: null` with the same fixed `after`
279
+ and a `before` exactly one microsecond below that boundary. Keep both bounds
280
+ on every page in the restarted cursor slice and in each raw request wrapper.
281
+ Follow the restarted slice to exhaustion and repeat this recovery for every
282
+ later cap. The checker proves each failed cursor against exactly one
283
+ restarted slice, checks the boundary and read order, and requires explicit
284
+ exhaustion. If Slack does not let you recover a capped tail, report the
285
+ bounded ownership search as incomplete; do not claim every in-window owned
286
+ mark was reconciled.
240
287
  Record each exact query and page in `ownedMarkSearches`; save raw wrappers
241
288
  named `owned-marks-{channelName}-{eyes|white_check_mark|ticket}-00.json` and
242
289
  continue numbering for each cursor page. Each wrapper preserves exact
@@ -252,16 +299,19 @@ place of Slack source messages.
252
299
  evidence; if the exact read fails or is incomplete, leave ownership
253
300
  unresolved rather than treating the query as confirmation. A result with
254
301
  `ts !== root` is reply-level evidence: resolve and preserve its parent, but
255
- never count it as a root checkoff. Verify reply reactions from the exact
256
- reply read-back in its complete thread snapshot; if that historical reply
257
- cannot be read back, report reply-level reconciliation as incomplete.
258
- Deduplicate confirmed roots across the three searches. For any
259
- exact-readback-confirmed root missing from the bounded source pages and
260
- `threadRootAudits`, add a
261
- full root audit row to `ownedRootAudits`; an in-scope root must instead
262
- appear in the source pages. Reconcile each discovered root's latest human
263
- message, status reply, scopes, and exact current reaction state. Read its
264
- full thread after the final ownership-search page and preserve every raw
302
+ never count it as a root checkoff. For an unheld root, verify reply reactions
303
+ from the exact reply read-back in its complete thread snapshot; if that
304
+ historical reply cannot be read back, report reply-level reconciliation as
305
+ incomplete. For a held root, preserve the reply parent and matching skip
306
+ reason without opening the thread.
307
+ Deduplicate confirmed roots across the three searches. Every in-window root
308
+ candidate must appear in the source pages. An older root linked by an
309
+ in-window reply belongs in `threadRoots` and `threadRootAudits`. If a bounded
310
+ search hit has no matching source row or linked supplemental root, rerun the
311
+ source scan and include the missing evidence; do not invent an audit row.
312
+ Reconcile each unheld discovered root's latest human message, status reply,
313
+ scopes, and exact current reaction state. Read its full thread after the final
314
+ ownership-search page and preserve every raw
265
315
  `slack_read_thread` page under `thread-{channelName}-{rootTs}-00.json` and
266
316
  contiguous page numbers. Each wrapper records exact request `args`,
267
317
  requested `cursor`, raw response, and `readAt`; the raw response must
@@ -387,9 +437,13 @@ place of Slack source messages.
387
437
  and every source `{ ts, root }` row with the reviewed inventory and raw Slack
388
438
  responses. If it finds a mismatch,
389
439
  rebuild the inventory from the captured Slack pages and rerun the check; never
390
- edit the receipt to fit the inventory. Each receipt page must also be fresh
391
- within the checker's 15-minute window, so repeat the complete channel scan and
392
- required final thread/reaction reads before recap when that window expires.
440
+ edit the receipt to fit the inventory. Source and ownership-search pages are
441
+ immutable evidence for the fixed `after`/`before` interval; their age does not
442
+ restart the sweep. The 15-minute freshness window applies to current root and
443
+ reply reaction reads and full-thread snapshots. If one expires, reread only
444
+ that exact state. After one targeted retry, if a required current-state read
445
+ is still stale or unavailable, mark the inventory incomplete and stop; do
446
+ not restart the channel-wide search or loop.
393
447
  It rejects any root mark this actor owns without a current same-actor
394
448
  status reply, a
395
449
  assigned-human handoff without that person's explicit acceptance, an
@@ -434,19 +488,18 @@ verified disposition may retain the eye as history. Never remove or clean up a
434
488
  reaction. Slack thread replies appear under the root's reply count, not as
435
489
  separate channel posts; do not broadcast routine status replies.
436
490
 
437
- After any long investigation, CI/deploy wait, or handoff, a newest-first scan
438
- from the saved high-water mark can identify new activity. Immediately before
439
- recap, however, rerun the exact original channel-wide `slack_search_public`
440
- query for every scoped channel, including its recorded `scanQuery.after`, and
441
- follow every cursor to Slack exhaustion. Replace the saved page responses,
442
- timestamps, source manifests, and source rows. Do not
443
- append a delta to an older page chain or refresh its `readAt` without rereading
444
- the page. Resolve every newly found reply to its actual root, refresh root
445
- reactions, and reconcile human answers before replacement. Then reopen every
446
- marked root thread after the last channel page, read all replies, and refresh
447
- reply-level workflow reaction owners. If this final scan or any thread read is
448
- older than 15 minutes, repeat the full bounded scan and marked-thread reads
449
- before checking the inventory. Only a complete fresh pass can certify coverage.
491
+ After a long investigation, CI/deploy wait, or handoff, do one final exhaustive
492
+ pass of the exact original channel-wide query for every scoped channel, using
493
+ the same recorded `scanQuery.after` and `scanQuery.before`. Replace the saved
494
+ page responses, timestamps, source manifests, and source rows; never append a
495
+ delta or refresh `readAt` without rereading the page. This is one bounded
496
+ reconciliation pass, not a moving high-water mark. Resolve newly found replies
497
+ to their actual roots and reconcile them inside the fixed interval. Then reread
498
+ current root reactions and full threads for marked roots after the final source
499
+ and ownership-search pages. If an exact current-state read expires, reread that
500
+ target once. If it still cannot pass freshness, mark coverage incomplete and
501
+ stop; do not restart the global scan. Activity newer than `windowEndTs` is for
502
+ the next scheduled sweep.
450
503
 
451
504
  Channel history does not include the contents of existing thread replies. In
452
505
  that final pass, also enumerate every distinct root thread carrying this
@@ -492,21 +545,22 @@ read time as `reactionReadAt`; when this sweep writes a reaction, record
492
545
  also requires each owned root reaction read and the full-thread reaction reads
493
546
  to follow the final channel page.
494
547
 
495
- The normal inventory check also requires every source, owned-mark search, and
496
- thread page or snapshot to have been read within the last 15 minutes, allows at
497
- most one minute of local clock skew, and requires owned-root reaction and thread
498
- read-backs to follow all three exhaustive mark searches. An old, complete
499
- inventory is stale: rerun the channel scan and historical mark searches, then
500
- read every marked root thread again before recap. This freshness gate prevents
501
- a complete earlier scan from silently omitting reports or replies that arrived
502
- during investigation.
548
+ The inventory checker requires source pages to use one recorded inclusive
549
+ `after`/`before` window and ownership-census pages to use the fixed `before`
550
+ upper bound with no `after`. Both page chains must remain in cursor order and
551
+ have valid local ISO read times. Their age does not invalidate those fixed
552
+ bounds. Current root and reply reaction reads and full-thread snapshots must be
553
+ within 15 minutes, allow at most one minute of local clock skew, and follow the
554
+ final source and ownership-search pages. Refresh only a stale current-state
555
+ target; if one targeted reread is still stale or unavailable, mark coverage
556
+ incomplete and stop instead of repeating the full sweep.
503
557
 
504
558
  The inventory checker requires a complete `threadSnapshot` for every in-scope
505
- classified root, every supplemental root included to resolve an in-scope reply,
506
- and every older root discovered by the exhaustive owned-mark searches. It checks
507
- raw thread page cursor exhaustion, Slack's reply count, exact source text,
508
- attachments, and snapshot order, and rejects a newer human or sweep reply than
509
- the recorded status. A failed check means the sweep is not reconciled.
559
+ classified root and every supplemental root included to resolve an in-scope
560
+ reply, except roots recorded under the held-thread boundary. For unheld roots it
561
+ checks raw thread page cursor exhaustion, Slack's reply count, exact source
562
+ text, attachments, and snapshot order, and rejects a newer human or sweep reply
563
+ than the recorded status. A failed check means the sweep is not reconciled.
510
564
 
511
565
  A grouped CI report or attachment is a discovery index, never a replacement for
512
566
  the channel-message inventory.
@@ -2,12 +2,19 @@
2
2
 
3
3
  Use the connected Slack integration for every Slack read and write. Never use a browser, screenshots, or UI automation to operate Slack.
4
4
 
5
- At sweep exit, reconcile every report root in the scoped inventory that has
6
- this task's `👀`, `✅`, or `🎫`, including marks left by earlier runs. Capture
5
+ At sweep exit, reconcile every unheld report root in the scoped inventory that
6
+ has this task's `👀`, `✅`, or `🎫`, including marks left by earlier runs. A
7
+ root is held when a direct reaction read shows another actor owns any workflow
8
+ mark or this actor owns `:mag:`. For held roots, record the root and safe-skip
9
+ reason, then stop: do not open the thread, inspect reply reactions, write any
10
+ reaction, or post a status. When this actor owns both `:mag:` and a workflow
11
+ mark, use `mag_opt_out` and preserve the exact direct root reaction-owner
12
+ record as history; the root remains held without a thread read or status reply.
13
+ Capture
7
14
  the exact Slack actor ID used by the connected tool at sweep start. A mark is
8
15
  owned by this task only when its reaction owner ID matches that actor ID. Each
9
- owned root needs a current disposition and a matching status reply by that same
10
- actor under that same parent, even when the root has only `✅` or `🎫`. A
16
+ unheld owned root needs a current disposition and a matching status reply by
17
+ that same actor under that same parent, even when the root has only `✅` or `🎫`. A
11
18
  same-run fix gets its final reply after verification; continuing work gets a
12
19
  concrete progress reply. An earlier generic “taking a look” does not satisfy
13
20
  this update. If an existing reply already names the exact canonical disposition
@@ -30,6 +37,23 @@ includes human handoffs, the status reply must name each handoff's recorded
30
37
  owner and action; include its scope when the report context does not make it
31
38
  clear.
32
39
 
40
+ Interpret the three root reactions independently from the current disposition:
41
+
42
+ - `👀` is a permanent claim/history mark, not an open/closed indicator.
43
+ - `✅` is permanent history for named scopes that passed direct post-change
44
+ proof. Add it as soon as any scope passes; do not wait for other scopes or
45
+ merge. Keep it if later evidence changes the scope's status, and explain the
46
+ historical mark in the current reply and inventory.
47
+ - `🎫` is permanent history for a distinct human action, including an
48
+ unassigned team action or reporter request. Keep it after completion,
49
+ answer, or four-day abandonment; label it historical in the current reply
50
+ and inventory when it no longer blocks progress.
51
+
52
+ The current status reply is the source for whether work remains open. When a
53
+ root has only `👀`, say that no scope has passed the proof bar for `✅`, whether
54
+ the next step belongs to this workflow, and why no separate human action needs
55
+ `🎫`. Reuse a current reply rather than posting again on every sweep.
56
+
33
57
  An evidence-only addendum or a correction is not a current status. When your
34
58
  newest own reply is one of those, edit that reply into the full canonical
35
59
  disposition when possible; otherwise send one final status after the latest
@@ -101,10 +125,13 @@ slack_search: "this was sent from a bot." in:<#CHANNEL>
101
125
  sort=timestamp sort_dir=asc include_context=true max_context_length=300
102
126
  ```
103
127
 
104
- Also scan channel messages from the saved reply cursor; on the first run, scan
105
- available history. Since `after:` is date-only, start one day before the cursor,
106
- exhaust pages, resolve hits to their root, and advance the cursor only through
107
- processed messages. Read inherited threads carrying any of this actor's root marks (`👀`, `✅`,
128
+ Also scan channel messages from the saved reply cursor within the current
129
+ invocation's fixed window; on the first run, use the default five-day window,
130
+ not all available history. Since `after:` is date-only, start one day before
131
+ the later of the saved cursor and window start, exhaust pages, resolve hits to
132
+ their root, and advance the cursor only through processed messages. An older
133
+ root is eligible only when an in-window reply links to it. Read inherited
134
+ threads carrying any of this actor's root marks (`👀`, `✅`,
108
135
  or `🎫`): a checkmark closes only its verified scope, not a newer reporter
109
136
  answer or another open scope. Do not reply or react on another actor's root when work is handed off for investigation; that handoff permits local investigation only. A separate explicit authorization is required to post a status or reporter reply there.
110
137
 
@@ -130,9 +157,10 @@ human/team actions, including actions with no named owner yet. The eye alone
130
157
  does not mean work remains open. One root `✅` covers every verified scope
131
158
  listed in `fixedScopes[]`; one root `🎫` covers every action listed in
132
159
  `handoffs[]`, including unassigned, accepted, and completed actions. Completion
133
- evidence changes its status, not the historical `🎫` reaction. For every root
134
- carrying any of this actor's marks, post or reuse a current status in that root's thread;
135
- unclaimed items and another actor's marks are not ours to reply to. Reconcile
160
+ evidence changes its status, not the historical `🎫` reaction. For every
161
+ unheld root carrying any of this actor's marks, post or reuse a current status
162
+ in that root's thread; unclaimed items, held roots, and another actor's marks
163
+ are not ours to reply to. Reconcile
136
164
  each report as one unit: send or verify its current same-thread status reply,
137
165
  then immediately update and read back each applicable root reaction before
138
166
  advancing to the next report. Add `✅` as soon as any reported scope passes the
@@ -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.
@@ -14,7 +14,6 @@ declare const _default: import("../../action.js").ActionDefinition<{
14
14
  executionCwd?: string;
15
15
  }, {
16
16
  deleted?: undefined;
17
- enabled?: undefined;
18
17
  created: boolean;
19
18
  name: string;
20
19
  scope: "organization" | "personal";
@@ -25,24 +24,25 @@ declare const _default: import("../../action.js").ActionDefinition<{
25
24
  reasoningEffort: "auto" | "high" | "low" | "max" | "medium" | "minimal" | "none" | "xhigh";
26
25
  webhookPath: string;
27
26
  nextRun: string;
27
+ enabled?: undefined;
28
28
  executionHostId?: undefined;
29
29
  executionEngine?: undefined;
30
30
  executionCwd?: undefined;
31
31
  } | {
32
- enabled?: undefined;
33
- nextRun?: undefined;
34
32
  created?: undefined;
35
33
  scope?: undefined;
36
34
  triggerType?: undefined;
37
35
  event?: undefined;
38
36
  deleted: boolean;
39
37
  name: string;
38
+ enabled?: undefined;
40
39
  schedule?: undefined;
41
40
  timezone?: undefined;
42
41
  reasoningEffort?: undefined;
43
42
  executionHostId?: undefined;
44
43
  executionEngine?: undefined;
45
44
  executionCwd?: undefined;
45
+ nextRun?: undefined;
46
46
  webhookPath?: undefined;
47
47
  } | {
48
48
  deleted?: undefined;
@@ -2789,9 +2789,10 @@ function silenceConnectionResets() {
2789
2789
  const message = String(e?.message ?? "");
2790
2790
  return (code === "ECONNRESET" ||
2791
2791
  code === "ECONNABORTED" ||
2792
+ code === "ECANCELED" ||
2792
2793
  code === "EPIPE" ||
2793
2794
  isClosedWebStreamController(err) ||
2794
- /^(read ECONNRESET|write ECONNRESET|socket hang up|aborted|write EPIPE)$/i.test(message));
2795
+ /^(read ECONNRESET|write ECONNRESET|write ECANCELED|socket hang up|aborted|write EPIPE)$/i.test(message));
2795
2796
  };
2796
2797
  const isBenignErrorPayload = (payload) => {
2797
2798
  const p = payload;
@@ -166,13 +166,13 @@ When `needsApproval` is omitted, behavior is byte-for-byte unchanged. There is n
166
166
 
167
167
  This works the same way for legacy `parameters`-style actions and for schema-based actions. It also works the same way for the in-app agent, sub-agents, A2A, and MCP callers, because every agent surface routes through the same loop.
168
168
 
169
- Calls made over the [HTTP API](/docs/http-api) with a bearer token or API key skip the approval card: the route runs the action directly, as it does for the app's own UI. To have an agent's calls approved, connect it over [MCP](/docs/external-agents) instead of calling the HTTP routes.
169
+ Calls over the [HTTP API](/docs/http-api) cannot give approval. When a call needs it, the route refuses with a `409` and `errorCode: "approval_required"`, whether the caller holds a bearer token, an API key, an MCP OAuth token, an org service token, or an A2A identity. A service identity can never approve. Only the app's own signed-in UI runs such a call directly, because the person using it is the one approving. A browser call proves it comes from that UI with a short-lived UI capability. A call that claims to be the app without one gets a `403` with `errorCode: "ui_capability_required"`, and the app's client mints the capability and retries once. To have an agent's calls approved, connect it over [MCP](/docs/external-agents) or have it ask in the app's agent chat.
170
170
 
171
171
  ### How the loop pauses {#loop}
172
172
 
173
173
  When the agent calls a gated action and this specific call has **not** already been approved, the loop does **not** execute `run()`. Instead it:
174
174
 
175
- 1. Resolves the gate. For a predicate, the loop calls `needsApproval(input, ctx)`. If that call throws an error, the loop treats it the same as a `true` result, so the action requires approval.
175
+ 1. Resolves the gate. The loop first prepares the call: it validates the arguments and applies the schema's defaults and coercions. An invalid call stops here with its validation error, and nobody is asked to approve it. For a predicate, the loop then calls `needsApproval(args, ctx)` with the prepared arguments, the ones `run()` would receive. If that call throws an error, the loop treats it the same as a `true` result, so the action requires approval.
176
176
  2. Emits a `tool_start` event (so the UI shows the call) followed immediately by an **`approval_required`** event, then stops the turn. The action's side effect never happens.
177
177
 
178
178
  The `approval_required` event carries everything the client needs to render an affordance:
@@ -407,7 +407,7 @@ The four mechanisms are not interchangeable:
407
407
  | --------------------------------- | ----------------------------------------------------- | -------------------------------- |
408
408
  | `access` | Does the caller hold the declared role or membership? | All six dispatch sites |
409
409
  | `authorize` | May this caller perform the operation at all? | All six dispatch sites |
410
- | `needsApproval` | Should a human approve this specific permitted call? | The agent loop only |
410
+ | `needsApproval` | Should a human approve this specific permitted call? | Every surface |
411
411
  | `accessFilter()`/`assertAccess()` | Which rows may this permitted caller see or write? | Inside `run()`, per query or row |
412
412
 
413
413
  A restricted write action typically needs both `authorize` and a row check. The first keeps non-coaches out of the operation. The second keeps a coach out of another org's plan. The row check is `assertAccess()`, or `access` when the row's id is an input field. `needsApproval` is a separate axis. It never restricts who may call, only whether the agent may proceed unattended.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.210.2",
3
+ "version": "0.211.0",
4
4
  "description": "The agentic application framework for building autonomous agents with intuitive UIs",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -711,8 +711,8 @@
711
711
  "yaml": "^2.9.0",
712
712
  "yjs": "^13.6.33",
713
713
  "zod": "^4.3.6",
714
- "@agent-native/agentkit": "^0.210.2",
715
- "@agent-native/recap-cli": "^0.5.77"
714
+ "@agent-native/agentkit": "^0.211.0",
715
+ "@agent-native/recap-cli": "^0.5.78"
716
716
  },
717
717
  "devDependencies": {
718
718
  "@ai-sdk/anthropic": "^3.0.71",
@@ -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.