@agent-native/core 0.203.1 → 0.204.0-nightly-20261008011018
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/a2a/handlers.js +57 -1
- package/dist/a2a/server.js +37 -1
- package/dist/action.js +22 -1
- package/dist/agent/contain-principal.d.ts +9 -0
- package/dist/agent/contain-principal.js +53 -0
- package/dist/agent/production-agent.js +32 -2
- package/dist/agent/run-manager.js +14 -2
- package/dist/agent/run-store.js +66 -12
- package/dist/agent/thread-message-projection.d.ts +1 -0
- package/dist/agent/thread-message-projection.js +56 -6
- package/dist/audit/actions/export-audit-events.d.ts +1 -1
- package/dist/audit/actions/export-audit-events.js +2 -2
- package/dist/audit/actions/export-audit-ocsf.d.ts +11 -0
- package/dist/audit/actions/export-audit-ocsf.js +183 -0
- package/dist/audit/actions/list-audit-events.d.ts +1 -1
- package/dist/audit/actions/list-audit-events.js +3 -3
- package/dist/audit/config.d.ts +1 -1
- package/dist/audit/config.js +5 -1
- package/dist/audit/index.d.ts +1 -0
- package/dist/audit/index.js +1 -0
- package/dist/audit/ocsf.d.ts +70 -0
- package/dist/audit/ocsf.js +228 -0
- package/dist/audit/record.js +1 -1
- package/dist/audit/store.js +9 -2
- package/dist/audit/types.d.ts +12 -1
- package/dist/cli/connect.js +7 -2
- package/dist/client/chat/agentkit-agent-native.js +104 -24
- package/dist/coding-tools/run-code.js +22 -1
- package/dist/coding-tools/sandbox/local-child-process-adapter.js +20 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +2 -2
- package/dist/file-upload/builder.js +18 -4
- package/dist/framework-tools.js +3 -0
- package/dist/identity/rekey.js +6 -0
- package/dist/localization/core-messages/supplemental/ar-SA.js +15 -0
- package/dist/localization/core-messages/supplemental/de-DE.js +15 -0
- package/dist/localization/core-messages/supplemental/en-US.js +15 -0
- package/dist/localization/core-messages/supplemental/es-ES.js +15 -0
- package/dist/localization/core-messages/supplemental/fr-FR.js +15 -0
- package/dist/localization/core-messages/supplemental/hi-IN.js +15 -0
- package/dist/localization/core-messages/supplemental/ja-JP.js +15 -0
- package/dist/localization/core-messages/supplemental/ko-KR.js +15 -0
- package/dist/localization/core-messages/supplemental/pt-BR.js +15 -0
- package/dist/localization/core-messages/supplemental/zh-CN.js +15 -0
- package/dist/localization/core-messages/supplemental/zh-TW.js +15 -0
- package/dist/localization/mcp-settings-messages.d.ts +15 -0
- package/dist/mcp/actions/create-org-service-token.d.ts +8 -0
- package/dist/mcp/actions/create-org-service-token.js +92 -4
- package/dist/mcp/actions/list-org-service-tokens.d.ts +2 -0
- package/dist/mcp/actions/list-org-service-tokens.js +36 -8
- package/dist/mcp/actions/service-principal-input.d.ts +48 -0
- package/dist/mcp/actions/service-principal-input.js +102 -0
- package/dist/mcp/actions/set-service-principal-lifecycle.d.ts +15 -0
- package/dist/mcp/actions/set-service-principal-lifecycle.js +150 -0
- package/dist/mcp/actions/set-service-principal-policy.d.ts +14 -0
- package/dist/mcp/actions/set-service-principal-policy.js +105 -0
- package/dist/mcp/build-server.js +105 -4
- package/dist/mcp/connect-route.js +116 -4
- package/dist/mcp/connect-store.d.ts +7 -0
- package/dist/mcp/connect-store.js +21 -14
- package/dist/observability/routes.d.ts +4 -4
- package/dist/observability/store.js +11 -5
- package/dist/observability/traces.js +6 -0
- package/dist/observability/types.d.ts +2 -0
- package/dist/org/index.d.ts +2 -2
- package/dist/org/service-principal-guard.d.ts +64 -0
- package/dist/org/service-principal-guard.js +140 -0
- package/dist/org/service-principal-policy.d.ts +104 -0
- package/dist/org/service-principal-policy.js +272 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/action-discovery.js +12 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.js +39 -12
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/release-schema.js +4 -0
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +59 -70
- package/dist/templates/factory/.agents/skills/review-latest-feedback/references/reproduction-and-verification.md +46 -0
- package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
- package/dist/templates/factory/.agents/skills/review-prs/SKILL.md +5 -3
- package/dist/triggers/actions/manage-automation.d.ts +2 -2
- package/dist/vite/client.js +1 -0
- package/docs/content/audit-log.mdx +15 -0
- package/docs/content/http-api.mdx +21 -2
- package/docs/content/pr-visual-recap.mdx +3 -1
- package/docs/content/template-clips-embed.mdx +1 -1
- package/docs/content/turn-into-app.mdx +118 -0
- package/package.json +4 -4
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +59 -70
- package/src/templates/factory/.agents/skills/review-latest-feedback/references/reproduction-and-verification.md +46 -0
- package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
- package/src/templates/factory/.agents/skills/review-prs/SKILL.md +5 -3
|
@@ -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
|
|
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
|
|
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"])
|
package/dist/audit/config.d.ts
CHANGED
|
@@ -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 };
|
package/dist/audit/config.js
CHANGED
|
@@ -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";
|
package/dist/audit/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/audit/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/audit/record.js
CHANGED
|
@@ -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,
|
package/dist/audit/store.js
CHANGED
|
@@ -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
|
|
269
|
+
ORDER BY created_at ${direction}, id ${direction}
|
|
263
270
|
LIMIT ? OFFSET ?`,
|
|
264
271
|
args: [...args, limit, offset],
|
|
265
272
|
});
|
package/dist/audit/types.d.ts
CHANGED
|
@@ -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/connect.js
CHANGED
|
@@ -1695,7 +1695,7 @@ export async function runServiceTokenMint(parsed, deps = {}) {
|
|
|
1695
1695
|
const serviceName = parsed.serviceToken?.trim() ?? "";
|
|
1696
1696
|
if (!parsed.url) {
|
|
1697
1697
|
logErr(" --service-token requires the app URL.");
|
|
1698
|
-
logErr(" Usage: npx @agent-native/core@latest connect <url> --service-token <name> [--ttl-days <1-
|
|
1698
|
+
logErr(" Usage: npx @agent-native/core@latest connect <url> --service-token <name> [--ttl-days <1-3650>]");
|
|
1699
1699
|
return false;
|
|
1700
1700
|
}
|
|
1701
1701
|
if (!serviceName) {
|
|
@@ -1790,7 +1790,7 @@ Usage:
|
|
|
1790
1790
|
No-browser fallback. Skip the device flow and write the entry with
|
|
1791
1791
|
the supplied token (get it from the app's Connect page).
|
|
1792
1792
|
|
|
1793
|
-
npx @agent-native/core@latest connect <url> --service-token <name> [--ttl-days <1-
|
|
1793
|
+
npx @agent-native/core@latest connect <url> --service-token <name> [--ttl-days <1-3650>]
|
|
1794
1794
|
Mint an ORG service token for CI (e.g. the PLAN_RECAP_TOKEN secret for
|
|
1795
1795
|
PR Visual Recap). Authenticates you via the browser device flow, then
|
|
1796
1796
|
mints a token owned by your ORGANIZATION — it keeps working if you
|
|
@@ -1831,6 +1831,11 @@ export async function runConnect(args, deps = {}) {
|
|
|
1831
1831
|
return;
|
|
1832
1832
|
}
|
|
1833
1833
|
const parsed = parseConnectArgs(args);
|
|
1834
|
+
if (parsed.ttlDays !== undefined && parsed.serviceToken === undefined) {
|
|
1835
|
+
logErr(" --ttl-days is only supported with --service-token. Set a personal token lifetime on the app's Connect page.");
|
|
1836
|
+
process.exitCode = 1;
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1834
1839
|
if (parsed.mode) {
|
|
1835
1840
|
let ok;
|
|
1836
1841
|
if (parsed.mode === "reconnect" || parsed.mode === "reauth") {
|