Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/content/docs/platform/enterprise/audit-logs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Events use identifiers such as `resource.action`. These categories are represent

Authentication failures and password-reset requests use an anonymous actor and record the target account separately. Impersonation and administrative session revocations identify the operator and target organization. Table-row mutations record summaries and counts without copying row contents.

When a workflow run makes the same kind of change to a table many times in quick succession, such as inserting rows in a loop, the repeats are merged into one `table.updated` entry. The merged entry keeps every row ID in `metadata.rowIds`, sums the counts, and records in `metadata.rollup` how many changes it covers and when the first and last happened. The actor of any workflow-made entry names the workflow (`metadata.actor.workflowId`) and the run (`metadata.actor.executionId`). Credential use by a workflow run is merged the same way: one `credential.accessed` entry per credential, counting the repeats.
When a workflow run makes the same kind of change to a table many times in quick succession, such as inserting rows in a loop, the repeats are merged into one `table.updated` entry. The merged entry keeps every row ID in `metadata.rowIds`, sums the counts, and records in `metadata.rollup` how many changes it covers and when the first and last happened. The actor of any workflow-made entry names the workflow (`metadata.actor.workflowId`) and the run (`metadata.actor.executionId`). Credential use by a workflow run is merged the same way: one `credential.accessed` entry per credential, counting the repeats, that names the workflow (`metadata.workflowId`) and the run (`metadata.executionId`).

Most audit writes are best effort: an outage or process loss can prevent persistence after an action succeeds. Deployment activation uses an awaited, idempotent insert before its outbox checkpoint. The feed is not a complete tool-call stream or a guaranteed transactional ledger for every operation.

Expand Down
1 change: 1 addition & 0 deletions apps/sim/executor/handlers/agent/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2988,6 +2988,7 @@ export class AgentBlockHandler implements BlockHandler {
actingUserId: ctx.userId,
workspaceId: ctx.workspaceId,
workflowId: ctx.workflowId,
executionId: ctx.executionId,
callerLabel: 'vertex-agent',
})
}
Expand Down
1 change: 1 addition & 0 deletions apps/sim/executor/handlers/evaluator/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export class EvaluatorBlockHandler implements BlockHandler {
actingUserId: ctx.userId,
workspaceId: ctx.workspaceId,
workflowId: ctx.workflowId,
executionId: ctx.executionId,
callerLabel: 'vertex-evaluator',
})
}
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/executor/handlers/router/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export class RouterBlockHandler implements BlockHandler {
actingUserId: ctx.userId,
workspaceId: ctx.workspaceId,
workflowId: ctx.workflowId,
executionId: ctx.executionId,
callerLabel: 'vertex-router',
})
}
Expand Down Expand Up @@ -289,6 +290,7 @@ export class RouterBlockHandler implements BlockHandler {
actingUserId: ctx.userId,
workspaceId: ctx.workspaceId,
workflowId: ctx.workflowId,
executionId: ctx.executionId,
callerLabel: 'vertex-router',
})
}
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/executor/utils/credential-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ResolveExecutorCredentialTokenParams {
credentialId: string
userId?: string
workflowId?: string
executionId?: string
/** Tool consuming the token; required by the managed-OAuth scope policy. */
toolId?: string
/** Display label for the thrown failure ("Failed to obtain credential for X: ..."). */
Expand Down Expand Up @@ -112,6 +113,7 @@ export async function resolveExecutorCredentialToken(
requestId,
credentialId,
workflowId,
executionId: params.executionId,
toolId,
scopes: params.scopes,
impersonateEmail: params.impersonateEmail,
Expand Down
13 changes: 13 additions & 0 deletions apps/sim/executor/utils/vertex-credential.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { canUseCredential, getCredentialActorContext } from '@/lib/credentials/access'
import { getServiceAccountToken } from '@/lib/oauth/credential-service'
import { recordCredentialAccess } from '@/lib/oauth/token-resolution'
import { resolveExecutorCredentialToken } from '@/executor/utils/credential-token'

const logger = createLogger('VertexCredential')
Expand All @@ -12,6 +13,7 @@ export interface ResolveVertexCredentialParams {
workspaceId: string | null | undefined
/** Pins the token request to this workflow's workspace. */
workflowId?: string
executionId?: string
callerLabel?: string
}

Expand All @@ -27,6 +29,7 @@ export async function resolveVertexCredential({
actingUserId,
workspaceId,
workflowId,
executionId,
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
callerLabel = 'vertex',
}: ResolveVertexCredentialParams): Promise<string> {
const requestId = `${callerLabel}-${Date.now()}`
Expand Down Expand Up @@ -57,6 +60,15 @@ export async function resolveVertexCredential({
const accessToken = await getServiceAccountToken(cred.id, [
'https://www.googleapis.com/auth/cloud-platform',
])
recordCredentialAccess({
actorId: actingUserId,
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
workspaceId: cred.workspaceId,
resourceId: cred.id,
providerId: cred.providerId,
credentialType: 'service_account',
workflowId,
executionId,
})
logger.info(`[${requestId}] Successfully resolved Vertex AI service account credential`)
return accessToken
}
Expand All @@ -70,6 +82,7 @@ export async function resolveVertexCredential({
credentialId,
userId: actingUserId,
workflowId,
executionId,
toolLabel: 'Vertex AI',
})

Expand Down
30 changes: 28 additions & 2 deletions apps/sim/lib/oauth/token-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export interface ResolveCredentialTokenInput {
requestId: string
credentialId?: string
workflowId?: string
/** Workflow run the token is for; recorded on the credential-access audit entry. */
executionId?: string
/** Canonical provider scopes, used only by service-account token minting. */
scopes?: string[]
/** Google domain-wide-delegation subject for service-account credentials. */
Expand Down Expand Up @@ -112,9 +114,12 @@ export function recordCredentialAccess(params: {
resourceId: string
providerId: string | null | undefined
credentialType: 'oauth' | 'service_account'
workflowId?: string
executionId?: string
auditRequest?: CredentialAuditRequest
}): void {
const { actorId, workspaceId, resourceId, providerId, credentialType } = params
const { actorId, workspaceId, resourceId, providerId, credentialType, workflowId, executionId } =
params
const credentialLabel = `${credentialType === 'oauth' ? 'OAuth' : 'service account'} credential for provider ${providerId ?? 'unknown'}`
const audit = {
workspaceId,
Expand All @@ -126,6 +131,8 @@ export function recordCredentialAccess(params: {
metadata: {
provider: providerId,
credentialType,
...(workflowId ? { workflowId } : {}),
...(executionId ? { executionId } : {}),
},
}
if (params.auditRequest) {
Expand Down Expand Up @@ -211,9 +218,20 @@ export async function completeOAuthCredentialToken(params: {
resolvedCredentialId: string
actorId?: string
workspaceId: string | null
workflowId?: string
executionId?: string
auditRequest?: CredentialAuditRequest
}): Promise<ResolveCredentialTokenResult> {
const { requestId, credential, resolvedCredentialId, actorId, workspaceId, auditRequest } = params
const {
requestId,
credential,
resolvedCredentialId,
actorId,
workspaceId,
workflowId,
executionId,
auditRequest,
} = params
const contextValidation = validateOAuthCredentialContext(credential)
if (!contextValidation.ok) {
return { ok: false, status: 401, error: contextValidation.error }
Expand All @@ -229,6 +247,8 @@ export async function completeOAuthCredentialToken(params: {
resourceId: resolvedCredentialId,
providerId: credential.providerId,
credentialType: 'oauth',
workflowId,
executionId,
auditRequest,
})
}
Expand Down Expand Up @@ -301,6 +321,7 @@ export async function resolveCredentialToken(
requestId,
credentialId,
workflowId,
executionId,
scopes,
impersonateEmail,
callerUserId,
Expand Down Expand Up @@ -355,6 +376,8 @@ export async function resolveCredentialToken(
resourceId: resolved.credentialId,
providerId: resolved.providerId,
credentialType: 'service_account',
workflowId,
executionId,
auditRequest,
})
}
Expand Down Expand Up @@ -433,6 +456,8 @@ export async function resolveCredentialToken(
resolvedCredentialId,
actorId: authz.requesterUserId,
workspaceId: authz.workspaceId ?? null,
workflowId,
executionId,
auditRequest,
})
} catch (error) {
Expand Down Expand Up @@ -477,6 +502,7 @@ export async function resolveCredentialAccessToken(
requestId,
credentialId,
workflowId: input.workflowId,
executionId: input.executionId,
scopes: input.scopes,
/**
* In-process callers forward raw subblock state, where an untouched
Expand Down
1 change: 1 addition & 0 deletions apps/sim/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1931,6 +1931,7 @@ async function executeToolImplementation(
credentialId,
userId,
workflowId,
executionId: scope.executionId,
toolId,
toolLabel,
scopes: providerScopes,
Expand Down
35 changes: 30 additions & 5 deletions packages/audit/src/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ const ROLLUP_MAX_ENTRIES = 100
/** Pending rollups held by the process; reaching it writes every one of them. */
const ROLLUP_MAX_PENDING = 1_000

/** Rows inserted at once by one write, so a large flush never floods the pool. */
/**
* Rollup rows the process inserts at once, across every window timer, size
* flush, and scope end, so a burst of distinct rollups never floods the pool.
*/
const ROLLUP_WRITE_CONCURRENCY = 10

export interface AuditRollupSummary {
Expand Down Expand Up @@ -67,6 +70,8 @@ interface PendingRollup {
}

const pending = new Map<string, PendingRollup>()
const queuedWrites: Array<{ rollup: PendingRollup; written: () => void }> = []
let activeWrites = 0
const scopeStorage = new AsyncLocalStorage<RollupScope>()
let nextScopeId = 0

Expand Down Expand Up @@ -196,11 +201,31 @@ function toAuditParams(rollup: PendingRollup): AuditLogParams {

/**
* One insert per row, so a row that cannot be written never takes others with
* it, at most {@link ROLLUP_WRITE_CONCURRENCY} at a time.
* it. Rows wait in one process-wide queue, at most
* {@link ROLLUP_WRITE_CONCURRENCY} in flight.
*/
async function writePending(rollups: PendingRollup[]): Promise<void> {
for (let index = 0; index < rollups.length; index += ROLLUP_WRITE_CONCURRENCY) {
await Promise.all(rollups.slice(index, index + ROLLUP_WRITE_CONCURRENCY).map(writeRollup))
function writePending(rollups: PendingRollup[]): Promise<void> {
return Promise.all(
rollups.map(
(rollup) =>
new Promise<void>((written) => {
queuedWrites.push({ rollup, written })
drainWrites()
})
)
).then(() => undefined)
}

function drainWrites(): void {
while (activeWrites < ROLLUP_WRITE_CONCURRENCY) {
const next = queuedWrites.shift()
if (!next) return
activeWrites += 1
void writeRollup(next.rollup).finally(() => {
activeWrites -= 1
next.written()
drainWrites()
})
}
}

Expand Down
Loading