@agent-native/core 0.203.2-nightly-20261007213109 → 0.204.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 (121) hide show
  1. package/dist/a2a/handlers.js +57 -1
  2. package/dist/a2a/server.js +37 -1
  3. package/dist/action.d.ts +1 -0
  4. package/dist/action.js +26 -3
  5. package/dist/agent/contain-principal.d.ts +9 -0
  6. package/dist/agent/contain-principal.js +53 -0
  7. package/dist/agent/production-agent.js +32 -2
  8. package/dist/agent/run-manager.js +14 -2
  9. package/dist/agent/run-store.js +66 -12
  10. package/dist/audit/actions/export-audit-events.d.ts +1 -1
  11. package/dist/audit/actions/export-audit-events.js +2 -2
  12. package/dist/audit/actions/export-audit-ocsf.d.ts +11 -0
  13. package/dist/audit/actions/export-audit-ocsf.js +183 -0
  14. package/dist/audit/actions/list-audit-events.d.ts +1 -1
  15. package/dist/audit/actions/list-audit-events.js +3 -3
  16. package/dist/audit/config.d.ts +1 -1
  17. package/dist/audit/config.js +5 -1
  18. package/dist/audit/index.d.ts +1 -0
  19. package/dist/audit/index.js +1 -0
  20. package/dist/audit/ocsf.d.ts +70 -0
  21. package/dist/audit/ocsf.js +228 -0
  22. package/dist/audit/record.js +1 -1
  23. package/dist/audit/store.js +9 -2
  24. package/dist/audit/types.d.ts +12 -1
  25. package/dist/cli/agent.js +2 -11
  26. package/dist/cli/connect.js +7 -2
  27. package/dist/client/use-db-sync.d.ts +13 -0
  28. package/dist/client/use-db-sync.js +22 -1
  29. package/dist/coding-tools/run-code.js +22 -1
  30. package/dist/coding-tools/sandbox/local-child-process-adapter.js +20 -0
  31. package/dist/collab/client.js +36 -3
  32. package/dist/collab/routes.d.ts +9 -4
  33. package/dist/collab/routes.js +15 -1
  34. package/dist/collab/struct-routes.d.ts +1 -1
  35. package/dist/deploy/build.js +15 -8
  36. package/dist/deploy/workspace-deploy.js +88 -13
  37. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  38. package/dist/file-upload/builder.js +18 -4
  39. package/dist/framework-tools.js +3 -0
  40. package/dist/identity/rekey.js +6 -0
  41. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  42. package/dist/localization/core-messages/supplemental/ar-SA.js +15 -0
  43. package/dist/localization/core-messages/supplemental/de-DE.js +15 -0
  44. package/dist/localization/core-messages/supplemental/en-US.js +15 -0
  45. package/dist/localization/core-messages/supplemental/es-ES.js +15 -0
  46. package/dist/localization/core-messages/supplemental/fr-FR.js +15 -0
  47. package/dist/localization/core-messages/supplemental/hi-IN.js +15 -0
  48. package/dist/localization/core-messages/supplemental/ja-JP.js +15 -0
  49. package/dist/localization/core-messages/supplemental/ko-KR.js +15 -0
  50. package/dist/localization/core-messages/supplemental/pt-BR.js +15 -0
  51. package/dist/localization/core-messages/supplemental/zh-CN.js +15 -0
  52. package/dist/localization/core-messages/supplemental/zh-TW.js +15 -0
  53. package/dist/localization/mcp-settings-messages.d.ts +15 -0
  54. package/dist/mcp/actions/create-org-service-token.d.ts +8 -0
  55. package/dist/mcp/actions/create-org-service-token.js +93 -4
  56. package/dist/mcp/actions/list-org-service-tokens.d.ts +2 -0
  57. package/dist/mcp/actions/list-org-service-tokens.js +36 -8
  58. package/dist/mcp/actions/service-principal-input.d.ts +48 -0
  59. package/dist/mcp/actions/service-principal-input.js +102 -0
  60. package/dist/mcp/actions/set-service-principal-lifecycle.d.ts +15 -0
  61. package/dist/mcp/actions/set-service-principal-lifecycle.js +150 -0
  62. package/dist/mcp/actions/set-service-principal-policy.d.ts +14 -0
  63. package/dist/mcp/actions/set-service-principal-policy.js +105 -0
  64. package/dist/mcp/build-server.js +117 -13
  65. package/dist/mcp/connect-route.js +116 -4
  66. package/dist/mcp/connect-store.d.ts +7 -0
  67. package/dist/mcp/connect-store.js +21 -14
  68. package/dist/mcp-client/oauth-routes.js +2 -1
  69. package/dist/notifications/routes.d.ts +3 -3
  70. package/dist/observability/routes.d.ts +7 -7
  71. package/dist/org/index.d.ts +2 -2
  72. package/dist/org/service-principal-guard.d.ts +64 -0
  73. package/dist/org/service-principal-guard.js +140 -0
  74. package/dist/org/service-principal-policy.d.ts +104 -0
  75. package/dist/org/service-principal-policy.js +272 -0
  76. package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
  77. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  78. package/dist/scripts/dev/index.js +2 -8
  79. package/dist/scripts/parse-args.d.ts +7 -0
  80. package/dist/scripts/parse-args.js +76 -0
  81. package/dist/scripts/utils.d.ts +1 -1
  82. package/dist/scripts/utils.js +1 -1
  83. package/dist/secrets/routes.d.ts +11 -11
  84. package/dist/server/action-discovery.js +16 -42
  85. package/dist/server/agent-chat/script-entries.js +3 -8
  86. package/dist/server/agent-chat-plugin.js +8 -42
  87. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  88. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  89. package/dist/server/auth.js +5 -2
  90. package/dist/server/core-routes-plugin.js +25 -6
  91. package/dist/server/google-oauth.js +8 -4
  92. package/dist/server/identity-sso.js +2 -1
  93. package/dist/server/index.d.ts +1 -0
  94. package/dist/server/index.js +1 -0
  95. package/dist/server/poll.d.ts +9 -0
  96. package/dist/server/poll.js +86 -7
  97. package/dist/server/query-echo-safe-redirect.d.ts +10 -0
  98. package/dist/server/query-echo-safe-redirect.js +102 -0
  99. package/dist/server/release-schema.js +4 -0
  100. package/dist/server/workspace-provider-oauth.js +2 -1
  101. package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
  102. package/dist/templates/default/.agents/skills/actions/references/action-fields.md +1 -0
  103. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
  104. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
  105. package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +1 -0
  106. package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -0
  107. package/dist/triggers/actions/manage-automation.d.ts +7 -7
  108. package/dist/vite/client.js +1 -0
  109. package/docs/content/audit-log.mdx +15 -0
  110. package/docs/content/external-agents-catalog.mdx +19 -0
  111. package/docs/content/http-api.mdx +21 -2
  112. package/docs/content/pr-visual-recap.mdx +3 -1
  113. package/docs/content/template-clips-embed.mdx +1 -1
  114. package/docs/content/turn-into-app.mdx +118 -0
  115. package/package.json +4 -4
  116. package/src/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
  117. package/src/templates/default/.agents/skills/actions/references/action-fields.md +1 -0
  118. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
  119. package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
  120. package/src/templates/headless/.agents/skills/actions/references/action-fields.md +1 -0
  121. package/src/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -0
@@ -0,0 +1,183 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { auditEventToOcsf, OCSF_SCHEMA_VERSION } from "../ocsf.js";
4
+ import { AuditAccessError, resolveAuditReadScope } from "../read-scope.js";
5
+ import { MAX_LIMIT, queryAuditEventPage } from "../store.js";
6
+ // Audit timestamps are assigned before inserts become visible. The settle delay
7
+ // and replay window keep a delayed insert from falling permanently behind the cursor.
8
+ const SETTLE_MS = 5000;
9
+ const REPLAY_WINDOW_MS = 5 * 60 * 1000;
10
+ const timeInput = z.union([z.number(), z.string()]);
11
+ function badRequest(message) {
12
+ return Object.assign(new Error(message), { statusCode: 400 });
13
+ }
14
+ function parseTime(value, name) {
15
+ if (value === undefined)
16
+ return undefined;
17
+ const ms = typeof value === "number"
18
+ ? value
19
+ : /^\d+$/.test(value.trim())
20
+ ? Number(value)
21
+ : Date.parse(value);
22
+ if (!Number.isFinite(ms)) {
23
+ throw badRequest(`${name} must be an ISO 8601 timestamp or epoch ms.`);
24
+ }
25
+ return ms;
26
+ }
27
+ function encodeCursor(cursor) {
28
+ return Buffer.from(JSON.stringify(cursor)).toString("base64url");
29
+ }
30
+ function decodeCursor(cursor) {
31
+ try {
32
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
33
+ if (parsed && typeof parsed === "object") {
34
+ const value = parsed;
35
+ const latestSettledMs = Date.now() - SETTLE_MS;
36
+ if (value.version === 3 &&
37
+ (value.mode === "ready" || value.mode === "paging") &&
38
+ typeof value.orgId === "string" &&
39
+ value.orgId.length > 0 &&
40
+ Number.isFinite(value.watermarkMs) &&
41
+ Number(value.watermarkMs) <= latestSettledMs &&
42
+ (value.sinceMs === undefined || Number.isFinite(value.sinceMs)) &&
43
+ (value.untilMs === undefined || Number.isFinite(value.untilMs))) {
44
+ const bounds = {
45
+ orgId: value.orgId,
46
+ ...(typeof value.sinceMs === "number"
47
+ ? { sinceMs: value.sinceMs }
48
+ : {}),
49
+ ...(typeof value.untilMs === "number"
50
+ ? { untilMs: value.untilMs }
51
+ : {}),
52
+ };
53
+ if (value.mode === "ready") {
54
+ return {
55
+ version: 3,
56
+ mode: "ready",
57
+ watermarkMs: value.watermarkMs,
58
+ ...bounds,
59
+ };
60
+ }
61
+ const after = value.after;
62
+ if (Number.isFinite(value.beforeMs) &&
63
+ Number(value.beforeMs) <= latestSettledMs &&
64
+ after !== null &&
65
+ typeof after === "object" &&
66
+ Number.isFinite(after.createdAt) &&
67
+ typeof after.id === "string" &&
68
+ after.id) {
69
+ return {
70
+ version: 3,
71
+ mode: "paging",
72
+ after: { createdAt: after.createdAt, id: after.id },
73
+ beforeMs: value.beforeMs,
74
+ watermarkMs: value.watermarkMs,
75
+ ...bounds,
76
+ };
77
+ }
78
+ }
79
+ }
80
+ // coercion-ok: malformed cursors fall through to the typed 400 below.
81
+ }
82
+ catch {
83
+ // Falls through to the 400 below.
84
+ }
85
+ throw badRequest("cursor is not a cursor returned by this action.");
86
+ }
87
+ function assertCursorBounds(cursor, bounds) {
88
+ if (cursor.orgId !== bounds.orgId ||
89
+ (bounds.sinceMs !== undefined && bounds.sinceMs !== cursor.sinceMs) ||
90
+ (bounds.untilMs !== undefined && bounds.untilMs !== cursor.untilMs)) {
91
+ throw badRequest("The organization, since, and until values must match the values used with this cursor.");
92
+ }
93
+ }
94
+ export default defineAction({
95
+ description: `Export the organization's audit trail as OCSF ${OCSF_SCHEMA_VERSION} API Activity events (class 6003) for a SIEM. Owners and admins only; returns the organization's shared trail (org and admins events), oldest first. Pull incrementally: pass the previous nextCursor as cursor and store every returned nextCursor, including when events is empty; empty pages advance the ready watermark. Cursors are bound to the organization that issued them; restart the export after switching organizations. A five-minute rolling overlap can replay recent events to catch late commits; deduplicate across polls by metadata.uid. Use list-audit-events instead to browse or answer 'what changed'.`,
96
+ schema: z.object({
97
+ since: timeInput
98
+ .optional()
99
+ .describe("Only events at or after this ISO 8601 timestamp or epoch ms."),
100
+ until: timeInput
101
+ .optional()
102
+ .describe("Only events before this ISO 8601 timestamp or epoch ms."),
103
+ cursor: z
104
+ .string()
105
+ .optional()
106
+ .describe("The nextCursor from the previous response; store every returned nextCursor, including for empty pages."),
107
+ limit: z
108
+ .number()
109
+ .int()
110
+ .min(1)
111
+ .max(MAX_LIMIT)
112
+ .optional()
113
+ .describe("Max events per page (default 100, max 500)."),
114
+ }),
115
+ http: { method: "GET" },
116
+ audit: {
117
+ onRead: true,
118
+ summary: () => "Exported audit events as OCSF",
119
+ },
120
+ run: async (args, ctx) => {
121
+ const scope = await resolveAuditReadScope(ctx, "organization");
122
+ if (!scope.orgId) {
123
+ throw new AuditAccessError("Select an organization to export the organization audit log.");
124
+ }
125
+ const requestedSinceMs = parseTime(args.since, "since");
126
+ const requestedUntilMs = parseTime(args.until, "until");
127
+ const cursor = args.cursor ? decodeCursor(args.cursor) : undefined;
128
+ if (cursor) {
129
+ assertCursorBounds(cursor, {
130
+ orgId: scope.orgId,
131
+ sinceMs: requestedSinceMs,
132
+ untilMs: requestedUntilMs,
133
+ });
134
+ }
135
+ const sinceMs = cursor?.sinceMs ?? requestedSinceMs;
136
+ const untilMs = cursor?.untilMs ?? requestedUntilMs;
137
+ const now = Date.now();
138
+ const beforeMs = cursor?.mode === "paging"
139
+ ? cursor.beforeMs
140
+ : Math.min(untilMs ?? Infinity, now - SETTLE_MS);
141
+ const after = cursor?.mode === "paging"
142
+ ? cursor.after
143
+ : cursor
144
+ ? {
145
+ createdAt: Math.max(0, cursor.watermarkMs - REPLAY_WINDOW_MS),
146
+ id: "",
147
+ }
148
+ : undefined;
149
+ const watermarkMs = cursor?.watermarkMs ?? 0;
150
+ const page = await queryAuditEventPage(scope, {
151
+ order: "asc",
152
+ limit: args.limit,
153
+ ...(sinceMs !== undefined ? { sinceMs } : {}),
154
+ beforeMs,
155
+ ...(after ? { after } : {}),
156
+ });
157
+ const last = page.events[page.events.length - 1];
158
+ const nextCursor = page.hasMore && last
159
+ ? encodeCursor({
160
+ version: 3,
161
+ mode: "paging",
162
+ orgId: scope.orgId,
163
+ after: { createdAt: last.createdAt, id: last.id },
164
+ beforeMs,
165
+ watermarkMs,
166
+ ...(sinceMs !== undefined ? { sinceMs } : {}),
167
+ ...(untilMs !== undefined ? { untilMs } : {}),
168
+ })
169
+ : encodeCursor({
170
+ version: 3,
171
+ mode: "ready",
172
+ orgId: scope.orgId,
173
+ watermarkMs: Math.max(watermarkMs, beforeMs),
174
+ ...(sinceMs !== undefined ? { sinceMs } : {}),
175
+ ...(untilMs !== undefined ? { untilMs } : {}),
176
+ });
177
+ return {
178
+ events: page.events.map(auditEventToOcsf),
179
+ hasMore: page.hasMore,
180
+ nextCursor,
181
+ };
182
+ },
183
+ });
@@ -2,7 +2,7 @@ declare const _default: import("../../action.js").ActionDefinition<{
2
2
  scope?: "accessible" | "organization";
3
3
  targetType?: string;
4
4
  targetId?: string;
5
- actorKind?: "agent" | "human" | "system";
5
+ actorKind?: "agent" | "human" | "service" | "system";
6
6
  actorEmail?: string;
7
7
  status?: "denied" | "error" | "success";
8
8
  threadId?: string;
@@ -3,7 +3,7 @@ import { defineAction } from "../../action.js";
3
3
  import { resolveAuditReadScope } from "../read-scope.js";
4
4
  import { queryAuditApps, queryAuditEventPage } from "../store.js";
5
5
  export default defineAction({
6
- description: "List audit-log events (who changed what, when, and whether it was you or the agent) for resources you can access. Supports filtering by target resource, actor (agent vs human), status, agent thread/turn, app, and time range, with offset paging. Use this to answer 'what did the agent change', 'who edited this record', or 'show recent changes'. Organization owners and admins pass scope 'organization' to read the organization's settings and admin trail (default model, member roles, Builder.io, and other org settings changes, including refused attempts), which is the Settings audit log.",
6
+ description: "List audit-log events (who changed what, when, and whether it was a human, agent, service principal, or system) for resources you can access. Supports filtering by target resource, actor, status, agent thread/turn, app, and time range, with offset paging. Use this to answer 'what did the agent change', 'who edited this record', or 'show recent changes'. Organization owners and admins pass scope 'organization' to read the organization's settings and admin trail (default model, member roles, Builder.io, and other org settings changes, including refused attempts), which is the Settings audit log.",
7
7
  schema: z.object({
8
8
  scope: z
9
9
  .enum(["accessible", "organization"])
@@ -18,9 +18,9 @@ export default defineAction({
18
18
  .optional()
19
19
  .describe("Filter to one resource id (pair with targetType)."),
20
20
  actorKind: z
21
- .enum(["agent", "human", "system"])
21
+ .enum(["agent", "human", "service", "system"])
22
22
  .optional()
23
- .describe("Filter to changes made by the agent, a human, or the system."),
23
+ .describe("Filter to changes made by an agent, human, service principal, or the system."),
24
24
  actorEmail: z.string().optional().describe("Filter to one actor's email."),
25
25
  status: z
26
26
  .enum(["success", "error", "denied"])
@@ -2,6 +2,6 @@ import type { ActionAuditConfig, AuditActorKind, AuditStatus } from "./types.js"
2
2
  export declare function normalizeAuditConfig(raw: unknown): ActionAuditConfig | undefined;
3
3
  export declare function resolveAuditAttach(config: ActionAuditConfig | undefined, readOnly: boolean | undefined): boolean;
4
4
  export declare function shouldRecordAudit(config: ActionAuditConfig | undefined, actionName: string): boolean;
5
- export declare function deriveActorKind(caller: string | undefined, actorEmail: string | undefined | null): AuditActorKind;
5
+ export declare function deriveActorKind(caller: string | undefined, actorEmail: string | undefined | null, orgId?: string | null): AuditActorKind;
6
6
  export declare function isAuditDisabled(): boolean;
7
7
  export type { AuditStatus };
@@ -1,3 +1,4 @@
1
+ import { parseServiceIdentityEmail } from "../org/service-identity.js";
1
2
  const DEFAULT_SKIP_ACTIONS = new Set([
2
3
  "context-pin",
3
4
  "context-evict",
@@ -28,7 +29,10 @@ export function shouldRecordAudit(config, actionName) {
28
29
  return false;
29
30
  return true;
30
31
  }
31
- export function deriveActorKind(caller, actorEmail) {
32
+ export function deriveActorKind(caller, actorEmail, orgId) {
33
+ const serviceIdentity = parseServiceIdentityEmail(actorEmail);
34
+ if (serviceIdentity && serviceIdentity.orgId === orgId)
35
+ return "service";
32
36
  if (caller === "tool")
33
37
  return "agent";
34
38
  return actorEmail ? "human" : "system";
@@ -4,5 +4,6 @@ export { redactArgsToJson } from "./redact.js";
4
4
  export { ensureAuditTables, insertAuditEvent, queryAuditEvents, queryAuditEventPage, queryAuditApps, getAuditEventById, deleteOldAuditEvents, type AuditEventPage, type AuditReadScope, type AuditTrail, } from "./store.js";
5
5
  export { AuditAccessError, resolveAuditReadScope } from "./read-scope.js";
6
6
  export { orgAdminAudit, orgAdminAuditTarget, recordOrgAdminAuditEvent, type OrgAdminAuditEventInput, } from "./org-admin.js";
7
+ export { auditEventToOcsf, OCSF_SCHEMA_VERSION, type OcsfApiActivity, } from "./ocsf.js";
7
8
  export { recordActionAudit } from "./record.js";
8
9
  export { runAuditCleanupOnce, startAuditCleanupJob, stopAuditCleanupJob, } from "./cleanup-job.js";
@@ -3,5 +3,6 @@ export { redactArgsToJson } from "./redact.js";
3
3
  export { ensureAuditTables, insertAuditEvent, queryAuditEvents, queryAuditEventPage, queryAuditApps, getAuditEventById, deleteOldAuditEvents, } from "./store.js";
4
4
  export { AuditAccessError, resolveAuditReadScope } from "./read-scope.js";
5
5
  export { orgAdminAudit, orgAdminAuditTarget, recordOrgAdminAuditEvent, } from "./org-admin.js";
6
+ export { auditEventToOcsf, OCSF_SCHEMA_VERSION, } from "./ocsf.js";
6
7
  export { recordActionAudit } from "./record.js";
7
8
  export { runAuditCleanupOnce, startAuditCleanupJob, stopAuditCleanupJob, } from "./cleanup-job.js";
@@ -0,0 +1,70 @@
1
+ import type { AuditEvent } from "./types.js";
2
+ export declare const OCSF_SCHEMA_VERSION = "1.9.0";
3
+ /** OCSF `activity_id`: 1 Create, 2 Read, 3 Update, 4 Delete, 99 Other. */
4
+ type OcsfActivityId = 1 | 2 | 3 | 4 | 99;
5
+ export interface OcsfApiActivity {
6
+ class_uid: 6003;
7
+ class_name: "API Activity";
8
+ category_uid: 6;
9
+ category_name: "Application Activity";
10
+ activity_id: OcsfActivityId;
11
+ activity_name: string;
12
+ type_uid: number;
13
+ type_name: string;
14
+ /** Epoch milliseconds. */
15
+ time: number;
16
+ severity_id: 1 | 2 | 3;
17
+ severity: "Informational" | "Low" | "Medium";
18
+ status_id: 1 | 2;
19
+ status: "Success" | "Failure";
20
+ status_code?: string;
21
+ status_detail?: string;
22
+ /** 1 Allowed, 2 Denied. */
23
+ action_id: 1 | 2;
24
+ action: "Allowed" | "Denied";
25
+ message: string;
26
+ metadata: {
27
+ version: string;
28
+ uid: string;
29
+ log_name: string;
30
+ tenant_uid?: string;
31
+ product: {
32
+ name: string;
33
+ vendor_name: string;
34
+ uid?: string;
35
+ };
36
+ };
37
+ actor: {
38
+ user: {
39
+ uid?: string;
40
+ name: string;
41
+ email_addr?: string;
42
+ /** 1 User, 3 System, 4 Service. */
43
+ type_id: 1 | 3 | 4;
44
+ type: "User" | "System" | "Service";
45
+ };
46
+ session?: {
47
+ uid: string;
48
+ };
49
+ };
50
+ api: {
51
+ operation: string;
52
+ service?: {
53
+ name: string;
54
+ };
55
+ };
56
+ src_endpoint: {
57
+ name: string;
58
+ uid?: string;
59
+ };
60
+ resources?: Array<{
61
+ type?: string;
62
+ uid?: string;
63
+ owner?: {
64
+ email_addr: string;
65
+ };
66
+ }>;
67
+ unmapped: Record<string, string>;
68
+ }
69
+ export declare function auditEventToOcsf(event: AuditEvent): OcsfApiActivity;
70
+ export {};
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Map an audit event to an OCSF API Activity event (class 6003, category 6
3
+ * Application Activity) so a SIEM can ingest the trail.
4
+ *
5
+ * Pure: no I/O, no clock. Field names and enum values follow OCSF
6
+ * {@link OCSF_SCHEMA_VERSION}. Anything OCSF has no field for (run, task,
7
+ * thread, turn, app, lineage) goes under `unmapped` instead of an invented
8
+ * field. The redacted `input` payload is never exported.
9
+ */
10
+ import { parseServiceIdentityEmail } from "../org/service-identity.js";
11
+ import { redactTextToSummary } from "./redact.js";
12
+ export const OCSF_SCHEMA_VERSION = "1.9.0";
13
+ const CLASS_UID = 6003;
14
+ const CATEGORY_UID = 6;
15
+ const ACTIVITY_NAMES = {
16
+ 1: "Create",
17
+ 2: "Read",
18
+ 3: "Update",
19
+ 4: "Delete",
20
+ 99: "Other",
21
+ };
22
+ // Action names are verb-first kebab-case (`delete-recording`). A verb we do
23
+ // not recognise is `Other`, never a guess.
24
+ const ACTIVITY_VERBS = [
25
+ [1, /^(create|add|new|invite|upload|import|register|mint|connect)(-|$)/],
26
+ [2, /^(get|list|read|search|view|export|query|find|fetch)(-|$)/],
27
+ [4, /^(delete|remove|revoke|archive|disconnect|retire|purge)(-|$)/],
28
+ [
29
+ 3,
30
+ /^(update|set|change|edit|rename|move|save|enable|disable|suspend|resume|toggle)(-|$)/,
31
+ ],
32
+ ];
33
+ function activityId(action) {
34
+ const name = action.toLowerCase();
35
+ return ACTIVITY_VERBS.find(([, verb]) => verb.test(name))?.[0] ?? 99;
36
+ }
37
+ // A refused attempt is the signal a SIEM alerts on, so it ranks above an
38
+ // internal error; both outrank a normal success.
39
+ const SEVERITY = {
40
+ success: 1,
41
+ error: 2,
42
+ denied: 3,
43
+ };
44
+ function sanitizeLineageUrl(value) {
45
+ if (!value)
46
+ return undefined;
47
+ try {
48
+ const url = new URL(value.trim());
49
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
50
+ return undefined;
51
+ }
52
+ if (/%(?:2f|5c)/i.test(url.pathname))
53
+ return undefined;
54
+ url.username = "";
55
+ url.password = "";
56
+ url.search = "";
57
+ url.hash = "";
58
+ let redactNextPathSegment = false;
59
+ const safePath = url.pathname
60
+ .split("/")
61
+ .map((segment) => {
62
+ if (redactNextPathSegment) {
63
+ redactNextPathSegment = false;
64
+ return "redacted";
65
+ }
66
+ let decoded = segment;
67
+ try {
68
+ decoded = decodeURIComponent(segment);
69
+ }
70
+ catch {
71
+ return "redacted";
72
+ }
73
+ const pathKey = decoded.toLowerCase().replace(/[-_]/g, "");
74
+ if (/^(?:apikey|accesstoken|refreshtoken|token|secret|password|credential|signature|sig|authorization|auth|session)$/.test(pathKey)) {
75
+ redactNextPathSegment = true;
76
+ return segment;
77
+ }
78
+ const redacted = redactTextToSummary(decoded);
79
+ if (!redacted || redacted === decoded)
80
+ return segment;
81
+ return encodeURIComponent(redacted === "[redacted]" ? "redacted" : redacted);
82
+ })
83
+ .join("/");
84
+ const safeUrl = `${url.origin}${safePath === "/" ? "" : safePath}`;
85
+ return redactTextToSummary(safeUrl) ?? undefined;
86
+ // coercion-ok: malformed lineage URLs are omitted from the SIEM export.
87
+ }
88
+ catch {
89
+ return undefined;
90
+ }
91
+ }
92
+ function sanitizeNetworkPeer(value) {
93
+ if (!value)
94
+ return undefined;
95
+ const trimmed = value.trim();
96
+ const url = sanitizeLineageUrl(trimmed);
97
+ if (url)
98
+ return url;
99
+ if (/^(?:[a-z][a-z\d+.-]*:)?\/\//i.test(trimmed))
100
+ return undefined;
101
+ return redactTextToSummary(trimmed) ?? undefined;
102
+ }
103
+ function actorFor(event) {
104
+ const email = event.actorEmail;
105
+ const service = parseServiceIdentityEmail(email);
106
+ // A human can register a `@service.<orgId>` address, so the email alone is
107
+ // not proof of a service principal: it must name this event's org.
108
+ const session = event.threadId ? { session: { uid: event.threadId } } : {};
109
+ if (service && email && event.orgId && service.orgId === event.orgId) {
110
+ return {
111
+ user: {
112
+ uid: email,
113
+ name: `svc-${service.serviceName}`,
114
+ type_id: 4,
115
+ type: "Service",
116
+ },
117
+ ...session,
118
+ };
119
+ }
120
+ if (email) {
121
+ return {
122
+ user: {
123
+ uid: email,
124
+ name: email,
125
+ email_addr: email,
126
+ type_id: 1,
127
+ type: "User",
128
+ },
129
+ ...session,
130
+ };
131
+ }
132
+ return {
133
+ user: { name: "system", type_id: 3, type: "System" },
134
+ ...session,
135
+ };
136
+ }
137
+ function statusDetail(event) {
138
+ if (event.status === "denied") {
139
+ return event.errorCode
140
+ ? `Refused: ${event.errorCode}`
141
+ : "Refused: the caller was not permitted to perform this action";
142
+ }
143
+ if (event.status === "error") {
144
+ return event.errorCode ? `Failed: ${event.errorCode}` : "Failed";
145
+ }
146
+ return undefined;
147
+ }
148
+ export function auditEventToOcsf(event) {
149
+ const id = activityId(event.action);
150
+ const failed = event.status !== "success";
151
+ const detail = statusDetail(event);
152
+ const unmapped = {};
153
+ const keep = (key, value) => {
154
+ if (value)
155
+ unmapped[key] = value;
156
+ };
157
+ keep("actor_kind", event.actorKind);
158
+ keep("caller", event.caller);
159
+ keep("app", event.app);
160
+ keep("visibility", event.visibility);
161
+ keep("thread_id", event.threadId);
162
+ keep("turn_id", event.turnId);
163
+ keep("run_id", event.runId);
164
+ keep("task_id", event.taskId);
165
+ keep("parent_task_id", event.parentTaskId);
166
+ keep("source_kind", event.sourceKind);
167
+ keep("source_platform", event.sourcePlatform);
168
+ keep("source_id", event.sourceId);
169
+ keep("source_url", sanitizeLineageUrl(event.sourceUrl));
170
+ keep("network_protocol", event.networkProtocol);
171
+ keep("network_id", event.networkId);
172
+ const networkPeer = sanitizeNetworkPeer(event.networkPeer);
173
+ keep("network_peer", networkPeer);
174
+ const resource = event.targetType || event.targetId
175
+ ? [
176
+ {
177
+ ...(event.targetType ? { type: event.targetType } : {}),
178
+ ...(event.targetId ? { uid: event.targetId } : {}),
179
+ ...(event.ownerEmail
180
+ ? { owner: { email_addr: event.ownerEmail } }
181
+ : {}),
182
+ },
183
+ ]
184
+ : undefined;
185
+ return {
186
+ class_uid: CLASS_UID,
187
+ class_name: "API Activity",
188
+ category_uid: CATEGORY_UID,
189
+ category_name: "Application Activity",
190
+ activity_id: id,
191
+ activity_name: ACTIVITY_NAMES[id],
192
+ type_uid: CLASS_UID * 100 + id,
193
+ type_name: `API Activity: ${ACTIVITY_NAMES[id]}`,
194
+ time: event.createdAt,
195
+ severity_id: SEVERITY[event.status],
196
+ severity: ["Informational", "Low", "Medium"][SEVERITY[event.status] - 1],
197
+ status_id: failed ? 2 : 1,
198
+ status: failed ? "Failure" : "Success",
199
+ ...(event.errorCode ? { status_code: event.errorCode } : {}),
200
+ ...(detail ? { status_detail: detail } : {}),
201
+ action_id: event.status === "denied" ? 2 : 1,
202
+ action: event.status === "denied" ? "Denied" : "Allowed",
203
+ message: redactTextToSummary(event.summary ?? "") ||
204
+ `${event.action} (${event.status})`,
205
+ metadata: {
206
+ version: OCSF_SCHEMA_VERSION,
207
+ uid: event.id,
208
+ log_name: "agent_audit_log",
209
+ ...(event.orgId ? { tenant_uid: event.orgId } : {}),
210
+ product: {
211
+ name: "Agent-Native",
212
+ vendor_name: "Builder.io",
213
+ ...(event.app ? { uid: event.app } : {}),
214
+ },
215
+ },
216
+ actor: actorFor(event),
217
+ api: {
218
+ operation: event.action,
219
+ ...(event.app ? { service: { name: event.app } } : {}),
220
+ },
221
+ src_endpoint: {
222
+ name: networkPeer || event.caller,
223
+ ...(event.networkId ? { uid: event.networkId } : {}),
224
+ },
225
+ ...(resource ? { resources: resource } : {}),
226
+ unmapped,
227
+ };
228
+ }
@@ -117,7 +117,7 @@ export async function recordActionAudit(input) {
117
117
  createdAt: Date.now(),
118
118
  action: actionName,
119
119
  caller,
120
- actorKind: deriveActorKind(caller, actorEmail),
120
+ actorKind: deriveActorKind(caller, actorEmail, ctx?.orgId),
121
121
  actorEmail,
122
122
  orgId: ctx?.orgId ?? null,
123
123
  threadId: ctx?.threadId ?? null,
@@ -191,7 +191,9 @@ export const MAX_LIMIT = 500;
191
191
  const DEFAULT_LIMIT = 100;
192
192
  const LIST_COLUMNS = "id, created_at, action, caller, actor_kind, actor_email, org_id, " +
193
193
  "thread_id, turn_id, target_type, target_id, status, summary, " +
194
- "error_code, owner_email, visibility, app";
194
+ "error_code, owner_email, visibility, app, run_id, task_id, " +
195
+ "parent_task_id, source_kind, source_platform, source_id, source_url, " +
196
+ "network_protocol, network_id, network_peer";
195
197
  function clampLimit(limit) {
196
198
  return Math.min(Math.max(1, Math.floor(limit ?? DEFAULT_LIMIT)), MAX_LIMIT);
197
199
  }
@@ -254,12 +256,17 @@ async function selectAuditRows(scope, filters, limit) {
254
256
  if (typeof filters.beforeMs === "number") {
255
257
  push("created_at < ?", Math.floor(filters.beforeMs));
256
258
  }
259
+ if (filters.after) {
260
+ where.push("(created_at > ? OR (created_at = ? AND id > ?))");
261
+ args.push(Math.floor(filters.after.createdAt), Math.floor(filters.after.createdAt), filters.after.id);
262
+ }
257
263
  const offset = Math.max(0, Math.floor(filters.offset ?? 0));
264
+ const direction = filters.order === "asc" ? "ASC" : "DESC";
258
265
  // `id` breaks created_at ties so offset pages neither repeat nor skip rows.
259
266
  const result = await client.execute({
260
267
  sql: `SELECT ${LIST_COLUMNS} FROM agent_audit_log
261
268
  WHERE ${where.join(" AND ")}
262
- ORDER BY created_at DESC, id DESC
269
+ ORDER BY created_at ${direction}, id ${direction}
263
270
  LIMIT ? OFFSET ?`,
264
271
  args: [...args, limit, offset],
265
272
  });
@@ -1,5 +1,5 @@
1
1
  export type AuditStatus = "success" | "error" | "denied";
2
- export type AuditActorKind = "agent" | "human" | "system";
2
+ export type AuditActorKind = "agent" | "human" | "service" | "system";
3
3
  /**
4
4
  * Who can read an event besides its owner.
5
5
  * - `private` — the owner only (the default; personal content stays here).
@@ -81,4 +81,15 @@ export interface AuditQueryFilters {
81
81
  beforeMs?: number;
82
82
  limit?: number;
83
83
  offset?: number;
84
+ /** Sort direction on (createdAt, id). Defaults to newest first. */
85
+ order?: "asc" | "desc";
86
+ /**
87
+ * Keyset position for an incremental pull: only rows strictly after this
88
+ * (createdAt, id) in `asc` order. Stable under concurrent inserts, which
89
+ * `offset` is not.
90
+ */
91
+ after?: {
92
+ createdAt: number;
93
+ id: string;
94
+ };
84
95
  }
package/dist/cli/agent.js CHANGED
@@ -8,7 +8,7 @@ import { getAppConfig } from "../app-config/index.js";
8
8
  import { createGitHubRepoToolEntries } from "../provider-api/github-repo.js";
9
9
  import { loadCliBootstrap } from "../scripts/cli-bootstrap.js";
10
10
  import { resolveDevUserEmail } from "../scripts/dev-session.js";
11
- import { loadEnv } from "../scripts/utils.js";
11
+ import { loadEnv, serializeCliArgs } from "../scripts/utils.js";
12
12
  import { autoDiscoverActions } from "../server/action-discovery.js";
13
13
  import { captureCliOutput } from "../server/cli-capture.js";
14
14
  import { getRequestOrgId, getRequestUserEmail, runWithRequestContext, } from "../server/request-context.js";
@@ -213,16 +213,7 @@ function buildHeadlessSystemPrompt(actionNames) {
213
213
  .join("\n\n");
214
214
  }
215
215
  function cliArgsFromToolArgs(args) {
216
- const cliArgs = [];
217
- for (const [key, value] of Object.entries(args)) {
218
- if (value === undefined)
219
- continue;
220
- const normalized = value != null && typeof value === "object"
221
- ? JSON.stringify(value)
222
- : String(value);
223
- cliArgs.push(`--${key}`, normalized);
224
- }
225
- return cliArgs;
216
+ return serializeCliArgs(Object.fromEntries(Object.entries(args).filter(([, value]) => value !== undefined)));
226
217
  }
227
218
  export async function createHeadlessBuiltinActions() {
228
219
  const docsSearch = await import("../scripts/docs/search.js");