@agent-native/core 0.209.1 → 0.209.2-nightly-20261015213030
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.
- package/dist/agent/actions/list-model-providers.js +15 -2
- package/dist/agent/app-model-defaults.d.ts +9 -13
- package/dist/agent/app-model-defaults.js +23 -5
- package/dist/agent/default-agent-engine.d.ts +5 -3
- package/dist/agent/default-agent-engine.js +38 -8
- package/dist/agent/durable-background.d.ts +2 -0
- package/dist/agent/durable-background.js +2 -0
- package/dist/agent/engine/registry.js +6 -0
- package/dist/agent/production-agent.js +58 -5
- package/dist/automations/service.d.ts +2 -0
- package/dist/automations/service.js +4 -1
- package/dist/client/analytics.js +48 -1
- package/dist/client/application-state.js +7 -2
- package/dist/client/chat/agentkit-protocol.js +12 -3
- package/dist/client/client-bootstrap.js +2 -0
- package/dist/client/problem-report.d.ts +11 -0
- package/dist/client/problem-report.js +19 -0
- package/dist/client/recorder.d.ts +88 -0
- package/dist/client/recorder.js +585 -0
- package/dist/client/route-chunk-recovery.js +4 -1
- package/dist/client/sign-out.js +2 -0
- package/dist/client/use-action.js +3 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/integrations/google-docs-poller.js +12 -5
- package/dist/integrations/plugin.js +86 -19
- package/dist/integrations/request-scope.d.ts +16 -0
- package/dist/integrations/request-scope.js +27 -0
- package/dist/integrations/webhook-handler.d.ts +2 -0
- package/dist/integrations/webhook-handler.js +38 -18
- package/dist/jobs/background-automation-runner.d.ts +2 -0
- package/dist/jobs/background-automation-runner.js +25 -2
- package/dist/jobs/scheduler.js +36 -22
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/org/context.d.ts +12 -0
- package/dist/org/context.js +26 -11
- package/dist/org/index.d.ts +1 -1
- package/dist/problem-report/actions/read-problem-report.d.ts +27 -0
- package/dist/problem-report/actions/read-problem-report.js +103 -0
- package/dist/problem-report/actions/save-problem-report.d.ts +35 -0
- package/dist/problem-report/actions/save-problem-report.js +36 -0
- package/dist/problem-report/format.d.ts +7 -0
- package/dist/problem-report/format.js +110 -0
- package/dist/problem-report/schema.d.ts +41 -0
- package/dist/problem-report/schema.js +34 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
- package/dist/provider-api/actions/provider-api.d.ts +14 -14
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.js +9 -4
- package/dist/scripts/agent-engines/set-agent-engine.js +7 -1
- package/dist/server/action-discovery.js +11 -0
- package/dist/server/action-routes.js +2 -0
- package/dist/server/agent-chat-ai-setup.d.ts +2 -3
- package/dist/server/agent-chat-ai-setup.js +18 -4
- package/dist/server/agent-chat-plugin.js +97 -22
- package/dist/server/agent-chat-stream.d.ts +17 -5
- package/dist/server/agent-chat-stream.js +15 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +2 -2
- package/dist/server/agent-engine-default-model-route.js +39 -6
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/server/agent-engine-status-cache.d.ts +4 -0
- package/dist/server/agent-engine-status-cache.js +2 -0
- package/dist/server/agent-run-context.d.ts +9 -0
- package/dist/server/agent-run-context.js +82 -17
- package/dist/server/builder-browser.js +18 -3
- package/dist/server/chatgpt-subscription-oauth.js +12 -17
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.js +46 -9
- package/dist/server/loopback.d.ts +1 -0
- package/dist/server/loopback.js +30 -0
- package/dist/server/origin-allowlist.d.ts +2 -0
- package/dist/server/origin-allowlist.js +13 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/request-context.d.ts +4 -0
- package/dist/templates/chat/app/components/layout/Sidebar.tsx +11 -1
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +2 -2
- package/dist/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +31 -0
- package/dist/triggers/dispatcher.js +117 -31
- package/docs/content/recorder.mdx +259 -0
- package/docs/content/template-clips-developers.mdx +9 -8
- package/docs/content/template-clips-features.mdx +8 -7
- package/package.json +5 -3
- package/src/templates/chat/app/components/layout/Sidebar.tsx +11 -1
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +2 -2
- package/src/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +31 -0
package/dist/server/loopback.js
CHANGED
|
@@ -16,3 +16,33 @@ export function isLoopbackRequest(event) {
|
|
|
16
16
|
}
|
|
17
17
|
return isLoopbackAddress(ip);
|
|
18
18
|
}
|
|
19
|
+
export function isDirectLoopbackRequest(event) {
|
|
20
|
+
const request = event.node?.req;
|
|
21
|
+
const headers = request?.headers;
|
|
22
|
+
const host = headers?.host;
|
|
23
|
+
const socket = request?.socket;
|
|
24
|
+
const hasForwardedHeader = Object.keys(headers ?? {}).some((name) => {
|
|
25
|
+
const normalizedName = name.toLowerCase();
|
|
26
|
+
return (normalizedName === "forwarded" ||
|
|
27
|
+
normalizedName.startsWith("x-forwarded-"));
|
|
28
|
+
});
|
|
29
|
+
if (!isLoopbackRequest(event) ||
|
|
30
|
+
typeof host !== "string" ||
|
|
31
|
+
!/^127\.0\.0\.1(?::\d{1,5})?$/.test(host) ||
|
|
32
|
+
hasForwardedHeader ||
|
|
33
|
+
!socket ||
|
|
34
|
+
("encrypted" in socket && socket.encrypted) ||
|
|
35
|
+
(socket.localAddress !== "127.0.0.1" &&
|
|
36
|
+
socket.localAddress !== "::ffff:127.0.0.1")) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const originValue = `http://${host}`;
|
|
40
|
+
if (!URL.canParse(originValue))
|
|
41
|
+
return false;
|
|
42
|
+
const origin = new URL(originValue);
|
|
43
|
+
return (origin.hostname === "127.0.0.1" &&
|
|
44
|
+
origin.port !== "0" &&
|
|
45
|
+
origin.pathname === "/" &&
|
|
46
|
+
origin.search === "" &&
|
|
47
|
+
origin.hash === "");
|
|
48
|
+
}
|
|
@@ -7,4 +7,6 @@ export declare function firstOriginFromEnv(keys: readonly string[], options: {
|
|
|
7
7
|
allowLoopback: boolean;
|
|
8
8
|
}): string | undefined;
|
|
9
9
|
export declare function getConfiguredOriginAllowlist(): Set<string>;
|
|
10
|
+
/** The allowlist minus platform-assigned origins, in declaration order. */
|
|
11
|
+
export declare function getAppDeclaredOrigins(): string[];
|
|
10
12
|
export declare function requestForwardedOrigin(request: Request | undefined): string | undefined;
|
|
@@ -72,9 +72,14 @@ export function firstOriginFromEnv(keys, options) {
|
|
|
72
72
|
}
|
|
73
73
|
return undefined;
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
// Netlify fills these with its own hostnames. A site served through a proxy
|
|
76
|
+
// (Builder Cloud) is never browsed at them, so they do not declare where the app lives.
|
|
77
|
+
const PLATFORM_ORIGIN_ENV_KEYS = new Set(["URL", "DEPLOY_URL"]);
|
|
78
|
+
function collectConfiguredOrigins(options) {
|
|
76
79
|
const out = new Set();
|
|
77
80
|
for (const key of EXPLICIT_PUBLIC_ORIGIN_ENV_KEYS) {
|
|
81
|
+
if (!options.includePlatform && PLATFORM_ORIGIN_ENV_KEYS.has(key))
|
|
82
|
+
continue;
|
|
78
83
|
addNormalizedOrigin(out, process.env[key], { allowLoopback: true });
|
|
79
84
|
}
|
|
80
85
|
for (const origin of (process.env[BETTER_AUTH_TRUSTED_ORIGINS_ENV_KEY] ?? "").split(",")) {
|
|
@@ -85,6 +90,13 @@ export function getConfiguredOriginAllowlist() {
|
|
|
85
90
|
}
|
|
86
91
|
return out;
|
|
87
92
|
}
|
|
93
|
+
export function getConfiguredOriginAllowlist() {
|
|
94
|
+
return collectConfiguredOrigins({ includePlatform: true });
|
|
95
|
+
}
|
|
96
|
+
/** The allowlist minus platform-assigned origins, in declaration order. */
|
|
97
|
+
export function getAppDeclaredOrigins() {
|
|
98
|
+
return [...collectConfiguredOrigins({ includePlatform: false })];
|
|
99
|
+
}
|
|
88
100
|
export function requestForwardedOrigin(request) {
|
|
89
101
|
if (!request)
|
|
90
102
|
return undefined;
|
|
@@ -54,8 +54,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
|
|
|
54
54
|
expiresAt?: undefined;
|
|
55
55
|
ttlSeconds?: undefined;
|
|
56
56
|
} | {
|
|
57
|
+
error?: undefined;
|
|
57
58
|
token: string;
|
|
58
59
|
expiresAt: string;
|
|
59
60
|
ttlSeconds: number;
|
|
60
|
-
error?: undefined;
|
|
61
61
|
}>>;
|
|
@@ -77,6 +77,8 @@ export interface RequestContext {
|
|
|
77
77
|
userName?: string;
|
|
78
78
|
orgId?: string;
|
|
79
79
|
orgScope?: "personal";
|
|
80
|
+
/** Explicitly selects deployment-wide agent defaults for shared no-org jobs. */
|
|
81
|
+
defaultAgentEngineScope?: "deployment";
|
|
80
82
|
/**
|
|
81
83
|
* Narrow authorization capability verified from an embed session. This is
|
|
82
84
|
* deliberately separate from user identity: capability-only sessions must
|
|
@@ -113,6 +115,8 @@ export interface RequestContext {
|
|
|
113
115
|
* radius (a resource that is itself local-only, NODE_ENV, etc.).
|
|
114
116
|
*/
|
|
115
117
|
isLoopbackRequest?: boolean;
|
|
118
|
+
/** True only when this server received the request directly on 127.0.0.1. */
|
|
119
|
+
isDirectLoopbackRequest?: boolean;
|
|
116
120
|
isIntegrationCaller?: boolean;
|
|
117
121
|
integration?: {
|
|
118
122
|
taskId: string;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
useChatThreads,
|
|
6
6
|
type ChatThreadSummary,
|
|
7
7
|
} from "@agent-native/toolkit/app/chat/agentkit-chat/rail";
|
|
8
|
+
import { ReportProblemButton } from "@agent-native/toolkit/app/feedback";
|
|
8
9
|
import { OrgSwitcher } from "@agent-native/toolkit/app/org";
|
|
9
10
|
import { openCommandMenu } from "@agent-native/toolkit/app/shared";
|
|
10
11
|
import { AgentNativeIcon } from "@agent-native/toolkit/app/shared/AgentNativeIcon";
|
|
@@ -567,7 +568,16 @@ export function Sidebar({
|
|
|
567
568
|
<ChatThreadsSection collapsed={collapsed} />
|
|
568
569
|
</nav>
|
|
569
570
|
|
|
570
|
-
<div
|
|
571
|
+
<div
|
|
572
|
+
className={cn(
|
|
573
|
+
"mt-auto shrink-0 px-2 pb-1",
|
|
574
|
+
collapsed && "flex justify-center",
|
|
575
|
+
)}
|
|
576
|
+
>
|
|
577
|
+
<ReportProblemButton variant={collapsed ? "icon" : "sidebar"} />
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
<div className="shrink-0 border-t border-sidebar-border p-2">
|
|
571
581
|
<div
|
|
572
582
|
className={cn(
|
|
573
583
|
collapsed
|
|
@@ -126,8 +126,8 @@ record source, target, owner, exact action, and verification. Don't reopen
|
|
|
126
126
|
closed fixes.
|
|
127
127
|
**Clustered** closes one row but retains it.
|
|
128
128
|
|
|
129
|
-
Enumerate channel parents newest
|
|
130
|
-
|
|
129
|
+
Enumerate channel parents newest-first for five days. For a permalink plus
|
|
130
|
+
newer scope, follow the [checked inventory procedure](references/channel-inventory.md).
|
|
131
131
|
|
|
132
132
|
**`slack_search` can rank and truncate.** Use channel reads to enumerate parents
|
|
133
133
|
and put their count in the recap. Sort targeted searches oldest-first and follow
|
package/dist/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Channel parent inventory
|
|
2
|
+
|
|
3
|
+
Use this procedure before investigating, claiming, or using a CI rollup in
|
|
4
|
+
place of Slack source messages.
|
|
5
|
+
|
|
6
|
+
- Read each scoped channel newest-first, following `next_cursor` until the
|
|
7
|
+
five-day boundary, the inclusive permalink cutoff, or Slack exhaustion.
|
|
8
|
+
- For “this permalink and newer,” use the parent message's `ts` as the inclusive
|
|
9
|
+
`cutoffTs` and `permalinkTs`. Slack's `oldest` parameter is exclusive, so
|
|
10
|
+
query from at least one second earlier and filter locally by `ts >= cutoff`.
|
|
11
|
+
- Record every top-level parent returned in every fetched page before filtering
|
|
12
|
+
or triage. This includes duplicates, owned items, bots, and boundary overlap.
|
|
13
|
+
Reactions and ownership determine what to investigate, not what to inventory.
|
|
14
|
+
- Save `.tmp/feedback-inventory.json` with `cutoffTs`, optional `permalinkTs`,
|
|
15
|
+
and `channels`. Set `permalinkTs` when a supplied message anchors the scan. A channel
|
|
16
|
+
has `channelId`, `oldestQuery`, and `pages`. A page has its requested `cursor`,
|
|
17
|
+
returned `nextCursor`, `exhausted`, the full Slack `parentCount`, the complete
|
|
18
|
+
`sourceTimestamps` list copied from that response, and `parents`. Each parent
|
|
19
|
+
records `ts`, `reactionChecked: true`, `reactionSummary` (`none` if empty),
|
|
20
|
+
and `disposition`.
|
|
21
|
+
- Keep the source manifest separate from disposition rows. Copy every timestamp
|
|
22
|
+
and the count from the full Slack response before filtering; the checker
|
|
23
|
+
compares both lists, catches missing parents and requires the requested
|
|
24
|
+
permalink timestamp to appear. Continue until a fetched page contains a
|
|
25
|
+
parent older than cutoff or Slack reports exhaustion.
|
|
26
|
+
- Run `pnpm feedback:inventory-check -- .tmp/feedback-inventory.json`. It checks
|
|
27
|
+
boundary overlap, page/cursor order, count parity, timestamps, reaction state,
|
|
28
|
+
and dispositions. Include each channel's parent and page counts in the recap.
|
|
29
|
+
|
|
30
|
+
A grouped CI report or attachment is a discovery index, never a replacement for
|
|
31
|
+
the channel-parent inventory.
|
|
@@ -3,7 +3,7 @@ import { isProductionServerlessFunctionRuntime } from "../db/client.js";
|
|
|
3
3
|
import { subscribeAll, unsubscribe } from "../event-bus/index.js";
|
|
4
4
|
import { countAutomationCredentialState, trackAutomationPaused, } from "../jobs/automation-events.js";
|
|
5
5
|
import { applyAutomationFailure, classifyAutomationFailure, CLEAR_FAILURE_STATE, CONFIG_INVALID_ERROR_CODE, OWNER_MISSING_ERROR_CODE, pauseNow, } from "../jobs/automation-outcome.js";
|
|
6
|
-
import { BackgroundAutomationRunError, checkBackgroundAutomationCredentials, isBackgroundAutomationRunActive, runBackgroundAutomation, } from "../jobs/background-automation-runner.js";
|
|
6
|
+
import { BackgroundAutomationRunError, checkBackgroundAutomationCredentials, isBackgroundAutomationRunActive, runBackgroundAutomation, validateAutomationRunIdentity, } from "../jobs/background-automation-runner.js";
|
|
7
7
|
import { buildJobResourceContent, jobBelongsToApp, parseJobResource, patchJobFrontmatterFields, } from "../jobs/frontmatter.js";
|
|
8
8
|
import { registerRecurringSweepHandler, } from "../jobs/sweep-hooks.js";
|
|
9
9
|
import { resourceFingerprintAllOwners, resourceGetByPath, resourceListAllOwnersWithFingerprint, resourcePutIfCurrent, } from "../resources/store.js";
|
|
@@ -853,12 +853,54 @@ async function dispatchQueuedAutomationEvent(queued, deps, hardDeadlineAt) {
|
|
|
853
853
|
return "retry";
|
|
854
854
|
let identity;
|
|
855
855
|
if (resource.owner === "__shared__") {
|
|
856
|
-
const userEmail = meta.createdBy
|
|
856
|
+
const userEmail = meta.createdBy?.trim().toLowerCase();
|
|
857
|
+
if (!userEmail) {
|
|
858
|
+
await recordTriggerExecutionOutcome(resource, { lastCheck: new Date().toISOString() }, {
|
|
859
|
+
failure: {
|
|
860
|
+
code: CONFIG_INVALID_ERROR_CODE,
|
|
861
|
+
message: "Shared automation has no creator identity.",
|
|
862
|
+
precondition: true,
|
|
863
|
+
},
|
|
864
|
+
pauseImmediately: true,
|
|
865
|
+
eventId: queued.eventId,
|
|
866
|
+
});
|
|
867
|
+
return "completed";
|
|
868
|
+
}
|
|
869
|
+
const orgId = meta.orgId?.trim() || undefined;
|
|
870
|
+
let validation;
|
|
871
|
+
try {
|
|
872
|
+
validation = await validateAutomationRunIdentity(userEmail, orgId);
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
await recordTriggerSkip(resource, "error", "Could not verify the automation execution identity.", "owner_unverifiable");
|
|
876
|
+
throw error;
|
|
877
|
+
}
|
|
878
|
+
if (!validation.ok) {
|
|
879
|
+
if (validation.code === OWNER_MISSING_ERROR_CODE ||
|
|
880
|
+
validation.code === CONFIG_INVALID_ERROR_CODE) {
|
|
881
|
+
await recordTriggerExecutionOutcome(resource, { lastCheck: new Date().toISOString() }, {
|
|
882
|
+
failure: {
|
|
883
|
+
code: validation.code,
|
|
884
|
+
message: validation.reason,
|
|
885
|
+
precondition: true,
|
|
886
|
+
},
|
|
887
|
+
pauseImmediately: true,
|
|
888
|
+
eventId: queued.eventId,
|
|
889
|
+
});
|
|
890
|
+
return "completed";
|
|
891
|
+
}
|
|
892
|
+
await recordTriggerSkip(resource, "error", validation.reason, validation.code);
|
|
893
|
+
return "completed";
|
|
894
|
+
}
|
|
857
895
|
identity = {
|
|
858
896
|
userEmail,
|
|
859
|
-
orgId
|
|
897
|
+
orgId,
|
|
898
|
+
...(!orgId ? { defaultAgentEngineScope: "deployment" } : {}),
|
|
860
899
|
eventOwner: userEmail.toLowerCase(),
|
|
861
900
|
};
|
|
901
|
+
if (!automationMatchesEventOwner(identity, queued.eventOwner)) {
|
|
902
|
+
return "completed";
|
|
903
|
+
}
|
|
862
904
|
}
|
|
863
905
|
else {
|
|
864
906
|
let resolved;
|
|
@@ -900,7 +942,16 @@ async function dispatchQueuedAutomationEvent(queued, deps, hardDeadlineAt) {
|
|
|
900
942
|
let classifierEngine;
|
|
901
943
|
let classifierModel;
|
|
902
944
|
if (meta.condition?.trim()) {
|
|
903
|
-
const credentialCheck = await checkBackgroundAutomationCredentials({
|
|
945
|
+
const credentialCheck = await checkBackgroundAutomationCredentials({
|
|
946
|
+
ownerEmail: identity.userEmail,
|
|
947
|
+
orgId: identity.orgId,
|
|
948
|
+
...(identity.orgScope === "personal"
|
|
949
|
+
? { orgScope: "personal" }
|
|
950
|
+
: {}),
|
|
951
|
+
...(identity.defaultAgentEngineScope === "deployment"
|
|
952
|
+
? { defaultAgentEngineScope: "deployment" }
|
|
953
|
+
: {}),
|
|
954
|
+
}, deps, meta.model);
|
|
904
955
|
if (!credentialCheck.ok) {
|
|
905
956
|
await recordTriggerExecutionOutcome(resource, { lastCheck: new Date().toISOString() }, { failure: credentialCheck.failure });
|
|
906
957
|
return "completed";
|
|
@@ -910,7 +961,16 @@ async function dispatchQueuedAutomationEvent(queued, deps, hardDeadlineAt) {
|
|
|
910
961
|
}
|
|
911
962
|
let matches;
|
|
912
963
|
try {
|
|
913
|
-
matches = await runWithRequestContext({
|
|
964
|
+
matches = await runWithRequestContext({
|
|
965
|
+
userEmail: identity.userEmail,
|
|
966
|
+
orgId: identity.orgId,
|
|
967
|
+
...(identity.orgScope === "personal"
|
|
968
|
+
? { orgScope: "personal" }
|
|
969
|
+
: {}),
|
|
970
|
+
...(identity.defaultAgentEngineScope === "deployment"
|
|
971
|
+
? { defaultAgentEngineScope: "deployment" }
|
|
972
|
+
: {}),
|
|
973
|
+
}, () => evaluateCondition(meta.condition, queued.payload, {
|
|
914
974
|
userEmail: identity.userEmail,
|
|
915
975
|
orgId: identity.orgId,
|
|
916
976
|
appId: deps.appId,
|
|
@@ -979,7 +1039,16 @@ export async function dispatchAutomationWebhookTask(task) {
|
|
|
979
1039
|
let classifierEngine;
|
|
980
1040
|
let classifierModel;
|
|
981
1041
|
if (meta.condition?.trim()) {
|
|
982
|
-
const credentialCheck = await checkBackgroundAutomationCredentials({
|
|
1042
|
+
const credentialCheck = await checkBackgroundAutomationCredentials({
|
|
1043
|
+
ownerEmail: identity.userEmail,
|
|
1044
|
+
orgId: identity.orgId,
|
|
1045
|
+
...(identity.orgScope === "personal"
|
|
1046
|
+
? { orgScope: "personal" }
|
|
1047
|
+
: {}),
|
|
1048
|
+
...(identity.defaultAgentEngineScope === "deployment"
|
|
1049
|
+
? { defaultAgentEngineScope: "deployment" }
|
|
1050
|
+
: {}),
|
|
1051
|
+
}, deps, meta.model);
|
|
983
1052
|
if (!credentialCheck.ok) {
|
|
984
1053
|
throw new BackgroundAutomationRunError(credentialCheck.failure.message, credentialCheck.failure.code);
|
|
985
1054
|
}
|
|
@@ -991,7 +1060,16 @@ export async function dispatchAutomationWebhookTask(task) {
|
|
|
991
1060
|
}
|
|
992
1061
|
let matches;
|
|
993
1062
|
try {
|
|
994
|
-
matches = await runWithRequestContext({
|
|
1063
|
+
matches = await runWithRequestContext({
|
|
1064
|
+
userEmail: identity.userEmail,
|
|
1065
|
+
orgId: identity.orgId,
|
|
1066
|
+
...(identity.orgScope === "personal"
|
|
1067
|
+
? { orgScope: "personal" }
|
|
1068
|
+
: {}),
|
|
1069
|
+
...(identity.defaultAgentEngineScope === "deployment"
|
|
1070
|
+
? { defaultAgentEngineScope: "deployment" }
|
|
1071
|
+
: {}),
|
|
1072
|
+
}, () => evaluateCondition(meta.condition, task.payload, {
|
|
995
1073
|
userEmail: identity.userEmail,
|
|
996
1074
|
orgId: identity.orgId,
|
|
997
1075
|
appId: deps.appId,
|
|
@@ -1083,32 +1161,40 @@ async function dispatchAgentic(resource, payload, eventMeta, identity, hardDeadl
|
|
|
1083
1161
|
body: latestTrigger.body,
|
|
1084
1162
|
resource: claimed,
|
|
1085
1163
|
};
|
|
1086
|
-
const requestContext =
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1164
|
+
const requestContext = {
|
|
1165
|
+
...(identity.orgScope === "personal"
|
|
1166
|
+
? { orgScope: "personal" }
|
|
1167
|
+
: {}),
|
|
1168
|
+
...(identity.defaultAgentEngineScope === "deployment"
|
|
1169
|
+
? { defaultAgentEngineScope: "deployment" }
|
|
1170
|
+
: {}),
|
|
1171
|
+
...(runningMeta.originScopeId &&
|
|
1172
|
+
runningMeta.deliveryPlatform &&
|
|
1173
|
+
runningMeta.deliveryDestination
|
|
1174
|
+
? {
|
|
1175
|
+
isIntegrationCaller: true,
|
|
1176
|
+
integration: {
|
|
1177
|
+
taskId: `automation:${triggerName}:${eventMeta.eventId}`,
|
|
1178
|
+
scopeId: runningMeta.originScopeId,
|
|
1179
|
+
principalType: "service",
|
|
1180
|
+
incoming: {
|
|
1181
|
+
platform: runningMeta.deliveryPlatform,
|
|
1182
|
+
externalThreadId: `${runningMeta.deliveryTenantId || "unknown"}:${runningMeta.deliveryDestination}:${runningMeta.deliveryThreadRef || "root"}`,
|
|
1183
|
+
text: "",
|
|
1184
|
+
tenantId: runningMeta.deliveryTenantId,
|
|
1185
|
+
integrationScopeId: runningMeta.originScopeId,
|
|
1186
|
+
platformContext: {
|
|
1187
|
+
channelId: runningMeta.deliveryDestination,
|
|
1188
|
+
threadTs: runningMeta.deliveryThreadRef,
|
|
1189
|
+
teamId: runningMeta.deliveryTenantId,
|
|
1190
|
+
},
|
|
1191
|
+
threadRef: runningMeta.deliveryThreadRef,
|
|
1192
|
+
timestamp: now.getTime(),
|
|
1105
1193
|
},
|
|
1106
|
-
threadRef: runningMeta.deliveryThreadRef,
|
|
1107
|
-
timestamp: now.getTime(),
|
|
1108
1194
|
},
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1195
|
+
}
|
|
1196
|
+
: {}),
|
|
1197
|
+
};
|
|
1112
1198
|
try {
|
|
1113
1199
|
const result = await runBackgroundAutomation({
|
|
1114
1200
|
automation,
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Recorder"
|
|
3
|
+
description: "The recorder is a short, redacted, in-memory record of what an app just did. It turns into a report for the app's agent, Clips, Sentry, or any destination you register."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Recorder
|
|
7
|
+
|
|
8
|
+
The recorder is a short, redacted, in-memory record of what an Agent-Native app just did. Every app records its actions, `application_state` writes, route changes, errors, and error toasts from the last two minutes, with no app code. When something goes wrong, that record becomes a report that the app's agent, a Clips recording, Sentry, or your own destination can read.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { recorder } from "@agent-native/core/client/recorder";
|
|
12
|
+
|
|
13
|
+
const report = recorder.report();
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Nothing is stored on the server and nothing leaves the browser until a report is sent somewhere. The recorder makes no network requests of its own.
|
|
17
|
+
|
|
18
|
+
## What it captures {#captured}
|
|
19
|
+
|
|
20
|
+
| Type | Source | Example |
|
|
21
|
+
| -------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
22
|
+
| `action` | Every `useActionQuery()`, `useActionMutation()`, and `callAction()` call. Each action in a batched read is its own event. | `list-folders` failed with `403` and `forbidden_scope` inside a batch that returned `200` |
|
|
23
|
+
| `state` | Every `application_state` write and delete, including the ones that failed or were cancelled | `design-selection` changed `state` from `hover` to `focus` |
|
|
24
|
+
| `nav` | Route changes. Only the path is kept. | `/design/d_8f2c1a` |
|
|
25
|
+
| `error` | Uncaught errors and unhandled promise rejections, with the first stack frame | `TypeError` at `/app/pages/design-editor/commands/undo.ts:1715` |
|
|
26
|
+
| `toast` | Error and warning toasts the user saw | `This screen changed elsewhere. Your edit was not saved.` |
|
|
27
|
+
| `log` | Events the app adds with `recorder.log()` | `screen.restored` from `f_1` to `f_9` |
|
|
28
|
+
|
|
29
|
+
Each event has the same fields:
|
|
30
|
+
|
|
31
|
+
| Field | Meaning |
|
|
32
|
+
| ----------- | ------------------------------------------------------------------------------------------------ |
|
|
33
|
+
| `t` | Seconds before the report was made. It is negative. |
|
|
34
|
+
| `at` | Epoch milliseconds. Clips diagnostics use the same clock. |
|
|
35
|
+
| `type` | `action`, `state`, `nav`, `error`, `toast`, or `log` |
|
|
36
|
+
| `name` | The action name, state key, route path, error type, toast level, or log name |
|
|
37
|
+
| `ok` | Whether an action or state write succeeded |
|
|
38
|
+
| `status` | The HTTP status, when there was one |
|
|
39
|
+
| `errorCode` | The action's error code. A call or write the app cancelled has `errorCode: "cancelled"`. |
|
|
40
|
+
| `ms` | How long an action or state write took |
|
|
41
|
+
| `requestId` | The server's request id, which also appears in its logs and in its Sentry event |
|
|
42
|
+
| `data` | Redacted, bounded details. An action has its HTTP method and, when it failed, its error message. |
|
|
43
|
+
|
|
44
|
+
A state write records only what changed since the last value that key saved. The first write of a key records `data.value`. Later writes record `data.changed`, where a removed field reads `"[removed]"`. A write that changed nothing records `data.unchanged`, and a delete records `data.deleted`. A failed write also has `data.error`, and the next write is still compared with the last value that saved.
|
|
45
|
+
|
|
46
|
+
## Reports {#reports}
|
|
47
|
+
|
|
48
|
+
A report is the events in the window plus the values the app watches and the work still in flight.
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"app": "design",
|
|
53
|
+
"build": "59929e9",
|
|
54
|
+
"url": "/design/d_8f2c1a?file=[redacted]",
|
|
55
|
+
"at": "2026-10-14T16:20:05.120Z",
|
|
56
|
+
"reason": "user",
|
|
57
|
+
"windowSeconds": 120,
|
|
58
|
+
"events": [
|
|
59
|
+
{
|
|
60
|
+
"t": -12.4,
|
|
61
|
+
"at": 1791994792720,
|
|
62
|
+
"type": "state",
|
|
63
|
+
"name": "design-selection",
|
|
64
|
+
"ok": true,
|
|
65
|
+
"ms": 41,
|
|
66
|
+
"data": { "changed": { "state": "hover" } }
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"t": -10.9,
|
|
70
|
+
"at": 1791994794220,
|
|
71
|
+
"type": "action",
|
|
72
|
+
"name": "update-file",
|
|
73
|
+
"ok": true,
|
|
74
|
+
"status": 200,
|
|
75
|
+
"ms": 180,
|
|
76
|
+
"requestId": "req_a1",
|
|
77
|
+
"data": { "method": "POST" }
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"watched": {
|
|
81
|
+
"inspector": {
|
|
82
|
+
"state": "focus",
|
|
83
|
+
"fields": [{ "label": "Opacity", "shown": "91%" }]
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"pending": [],
|
|
87
|
+
"dropped": 0
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- **app**: the app id. Without one it falls back to the page's host, such as `localhost:8080`, so it is never empty.
|
|
92
|
+
- **url**: the route path. Every query value is redacted and the hash is left out.
|
|
93
|
+
- **windowSeconds**: how many seconds the report covers.
|
|
94
|
+
- **pending**: actions and state writes still in flight, each with its `ageMs`.
|
|
95
|
+
- **dropped**: events lost to the caps since the page loaded or the user signed out. Events that aged out of the window are not counted.
|
|
96
|
+
|
|
97
|
+
## The SDK {#sdk}
|
|
98
|
+
|
|
99
|
+
### `recorder.report()` {#report}
|
|
100
|
+
|
|
101
|
+
`recorder.report()` returns the current report. It is synchronous. It accepts one optional object:
|
|
102
|
+
|
|
103
|
+
- **seconds**: keep only the last `seconds` of the window.
|
|
104
|
+
- **reason**: `"user"`, `"error"`, `"test"`, or `"manual"`. The default is `"manual"`.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const report = recorder.report();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Pass `seconds` for a shorter slice, such as the half minute before a recording started:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const before = recorder.report({ seconds: 30 });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Every reason except `"manual"` also sends the report to the `recorder.onReport()` listeners. A plain `recorder.report()` call never does, so a listener can build a report without looping.
|
|
117
|
+
|
|
118
|
+
### `recorder.log()` {#log}
|
|
119
|
+
|
|
120
|
+
`recorder.log()` adds an event in the app's own words. It accepts a name and optional data. Data that is not a plain object is recorded as `{ value }`.
|
|
121
|
+
|
|
122
|
+
Design logs the new id of each screen that undo brings back, because undo re-creates deleted screens with new ids:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
recorder.log("screen.restored", { from: file.id, to: result.id });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `recorder.watch()` {#watch}
|
|
129
|
+
|
|
130
|
+
`recorder.watch()` adds a live value to every report. It accepts a name and a function that returns the value, and it returns a function that stops watching.
|
|
131
|
+
|
|
132
|
+
Design's inspector adds what it is showing and the interaction state it is editing. Neither is in `application_state`, so without this a report could not show them:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
useEffect(
|
|
136
|
+
() =>
|
|
137
|
+
recorder.watch("inspector", () => ({
|
|
138
|
+
state: interactionState ?? "default",
|
|
139
|
+
fields: readVisualScrubInputs(),
|
|
140
|
+
})),
|
|
141
|
+
[interactionState],
|
|
142
|
+
);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A function that throws is reported as `{ unreadable: "<message>" }` rather than left out.
|
|
146
|
+
|
|
147
|
+
### `recorder.onReport()` {#on-report}
|
|
148
|
+
|
|
149
|
+
`recorder.onReport()` sends reports to a destination. It accepts a listener and returns a function that removes it. The listener receives every report built with a reason other than `"manual"`. That covers Report a problem (`"user"`), uncaught errors (`"error"`), and tests (`"test"`).
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
recorder.onReport((report) => posthog.capture("problem_report", report));
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
recorder.onReport((report) =>
|
|
157
|
+
navigator.sendBeacon("/api/logs", JSON.stringify(report)),
|
|
158
|
+
);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
An uncaught error sends at most one report every ten seconds, so an error loop cannot flood a destination. A listener that throws is logged and the other listeners still run.
|
|
162
|
+
|
|
163
|
+
### `recorder.redact()` {#redact}
|
|
164
|
+
|
|
165
|
+
`recorder.redact()` hides more fields from every event recorded after the call and from every watched value. Pass field names, or dotted paths that start with the event's name. A `*` matches any one field.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
recorder.redact("email");
|
|
169
|
+
recorder.redact("compose.body", "payment.*");
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `recorder.pending()` {#pending}
|
|
173
|
+
|
|
174
|
+
`recorder.pending()` lists the actions and state writes still in flight. Tests use it to wait for real idle instead of a fixed sleep. Design's end-to-end rails wait for it to be empty in `app.settled()`, and attach `recorder.json` to every failed test.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
await expect
|
|
178
|
+
.poll(() =>
|
|
179
|
+
page.evaluate(
|
|
180
|
+
() => window.__agentNativeRecorder?.report({ seconds: 0 }).pending,
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
.toEqual([]);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `recorder.disable()` {#disable}
|
|
187
|
+
|
|
188
|
+
`recorder.disable()` turns the recorder off. It does the same as `configureRecorder({ enabled: false })`.
|
|
189
|
+
|
|
190
|
+
## `configureRecorder()` {#configure}
|
|
191
|
+
|
|
192
|
+
`configureRecorder()` is one optional call. These are the defaults:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { configureRecorder } from "@agent-native/core/client/recorder";
|
|
196
|
+
|
|
197
|
+
configureRecorder({
|
|
198
|
+
windowSeconds: 120,
|
|
199
|
+
maxEvents: 2000,
|
|
200
|
+
sentry: true,
|
|
201
|
+
enabled: true,
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- **windowSeconds**: how far back the recorder keeps events.
|
|
206
|
+
- **maxEvents**: the most events it keeps. When the cap is reached, the oldest events are dropped and counted in `dropped`.
|
|
207
|
+
- **sentry**: whether Sentry errors get the recent events. It only acts when the app has browser Sentry.
|
|
208
|
+
- **enabled**: whether the recorder runs at all.
|
|
209
|
+
|
|
210
|
+
A window or cap that is not a positive number throws a `RangeError` instead of falling back to a default. The recorder also caps its total size at about 512 KB of JSON, whatever `maxEvents` is, which keeps it under 2 MB of memory per tab.
|
|
211
|
+
|
|
212
|
+
## Destinations {#destinations}
|
|
213
|
+
|
|
214
|
+
### Report a problem {#report-a-problem}
|
|
215
|
+
|
|
216
|
+
Every app has **Report a problem**. It is at the bottom of the sidebar, in the agent panel's options menu, and in the command menu (⌘K).
|
|
217
|
+
|
|
218
|
+
- It asks whether to attach what the app did in the last two minutes, and previews the counts, the failures, and the first events. It asks every time.
|
|
219
|
+
- When the user agrees, the full report is stored as a private file that only that user can read. The app's agent receives a short summary with a reference to the file.
|
|
220
|
+
- The agent reads more of the report with the `read-problem-report` action, filtered by event type or time.
|
|
221
|
+
- When the page has no agent chat, the button copies the summary and the reference instead.
|
|
222
|
+
- The report needs file storage. Without it, the dialog says the report was not attached, and nothing is sent.
|
|
223
|
+
|
|
224
|
+
### Sentry {#sentry}
|
|
225
|
+
|
|
226
|
+
When an app has browser Sentry configured through Agent-Native (see [Error reporting](/docs/observability#sentry)), every Sentry error event gets:
|
|
227
|
+
|
|
228
|
+
- the last 100 events as breadcrumbs in the `recorder.action`, `recorder.state`, and similar categories, each with its outcome, status, and request id
|
|
229
|
+
- the whole report as a `recorder.json` attachment
|
|
230
|
+
|
|
231
|
+
This needs no app code. `configureRecorder({ sentry: false })` turns it off.
|
|
232
|
+
|
|
233
|
+
### Your own destination {#own-destination}
|
|
234
|
+
|
|
235
|
+
Register a listener with [`recorder.onReport()`](#on-report).
|
|
236
|
+
|
|
237
|
+
### Clips and browser extensions {#extensions}
|
|
238
|
+
|
|
239
|
+
The page exposes `window.__agentNativeRecorder.report()`. A browser extension reads it from the page's main world.
|
|
240
|
+
|
|
241
|
+
When developer logs are on, the Clips extension reads it in the recorded tab when the recording starts (the last 30 seconds) and when it stops. The events join the recording's console and network entries on the same clock, and the clip's agent gets a summary of the failures. See [Clips browser logs](/docs/template-clips-features#chrome-extension-browser-logs).
|
|
242
|
+
|
|
243
|
+
## Privacy {#privacy}
|
|
244
|
+
|
|
245
|
+
- **It stays in the browser.** A report leaves only when a destination sends it: Report a problem after the user agrees, a Clips recording the user started, Sentry, or a listener the developer registered.
|
|
246
|
+
- **Secrets are removed.** A field whose name looks secret (`password`, `token`, `secret`, `apiKey`, `authorization`, `cookie`, `session`, `card`, `cvv`) reads `"[redacted]"`. Token-looking text inside any value, such as JWTs, bearer tokens, and provider API keys, is replaced with `[redacted]` too. These are the same redactors core uses for error reports.
|
|
247
|
+
- **Values are bounded.** Strings are cut at 200 characters and each event at about 2 KB. A cut says what it dropped. Request and response bodies are never recorded.
|
|
248
|
+
- **It forgets.** It keeps only the window, up to its caps. It is cleared on reload and on sign-out.
|
|
249
|
+
- **No new access.** The recorder only sees what already passes through the app. Action calls already go to the server, and `application_state` is already stored and read by the agent.
|
|
250
|
+
|
|
251
|
+
## Turning it off {#off}
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { configureRecorder } from "@agent-native/core/client/recorder";
|
|
255
|
+
|
|
256
|
+
configureRecorder({ enabled: false });
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
A disabled recorder forgets what it held, stops listening, and removes `window.__agentNativeRecorder` from the page. Its `recorder.report()` returns no events and no watched values, and it sends nothing to listeners. `configureRecorder({ enabled: true })` starts it again.
|