Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

252 lines
9.1 KiB
TypeScript

import { isDeepStrictEqual } from 'node:util'
import { isPlainRecord } from '@sim/utils/object'
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
import {
addModelInputProvenanceToRequest,
createModelInputProvenanceRequestMetadata,
createPrivateSecretProvenanceRequestMetadata,
markModelInputProjected,
} from '@/lib/execution/model-input-provenance'
import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
import type { ToolConfig } from '@/tools/types'
const MODEL_INPUT_PROJECTION_ERROR_MESSAGE = 'Model input could not be safely projected'
const PRIVATE_MODEL_INPUT_EXTERNAL_URL_ERROR_MESSAGE =
'Private model input provenance is only supported for internal routes'
const PRIVATE_SECRET_PROVENANCE_EXTERNAL_URL_ERROR_MESSAGE =
'Private secret provenance is only supported for internal routes'
export interface PreparedToolRequest {
url: string
method: string
headers: Headers
body?: string
timeout?: number
proxyUrl?: string
stripAuthOnRedirect?: boolean
isInternalRoute: boolean
}
function haveExactOwnKeys(
selected: Record<string, unknown>,
projected: Record<string, unknown>
): boolean {
const selectedKeys = Reflect.ownKeys(selected)
const projectedKeys = Reflect.ownKeys(projected)
return (
selectedKeys.length === projectedKeys.length &&
selectedKeys.every((key) => typeof key === 'string' && Object.hasOwn(projected, key)) &&
projectedKeys.every((key) => typeof key === 'string' && Object.hasOwn(selected, key))
)
}
export function getOwnEnumerableDataEntries(
record: Record<string, unknown>
): Array<[string, unknown]> | undefined {
const entries: Array<[string, unknown]> = []
for (const key of Reflect.ownKeys(record)) {
if (typeof key !== 'string') return undefined
const descriptor = Object.getOwnPropertyDescriptor(record, key)
if (!descriptor?.enumerable || !('value' in descriptor)) return undefined
entries.push([key, descriptor.value])
}
return entries
}
function inspectSelectedModelInputRecord(
tool: ToolConfig,
selected: unknown
): { record: Record<string, unknown>; entries: Array<[string, unknown]> } | undefined {
if (!isPlainRecord(selected)) return undefined
const entries = getOwnEnumerableDataEntries(selected)
if (!entries || entries.some(([key]) => !Object.hasOwn(tool.params, key))) return undefined
return { record: selected, entries }
}
export function projectToolModelInputParams(
tool: ToolConfig,
params: Record<string, any>,
registry: ResolvedSecretTraceRegistry | undefined
): Record<string, any> {
const modelInput = tool.request.modelInput
if (!registry || modelInput?.mode !== 'project') return params
try {
const selection = inspectSelectedModelInputRecord(tool, modelInput.select(params))
if (!selection) throw new Error(MODEL_INPUT_PROJECTION_ERROR_MESSAGE)
const { record: selected, entries } = selection
const originalSelectedParams: Record<string, unknown> = {}
for (const [key] of entries) originalSelectedParams[key] = params[key]
const projection = registry.projectResolvedInputSelection(originalSelectedParams)
if (!projection.complete) {
throw new Error(MODEL_INPUT_PROJECTION_ERROR_MESSAGE)
}
const projectedParams = { ...params, ...projection.value }
const projectedSelection = inspectSelectedModelInputRecord(
tool,
modelInput.select(projectedParams)
)
if (!projectedSelection || !haveExactOwnKeys(selected, projectedSelection.record)) {
throw new Error(MODEL_INPUT_PROJECTION_ERROR_MESSAGE)
}
JSON.stringify(projectedSelection.record)
if (!modelInput.applyProjected) return projectedParams
const selectedParamsClone = structuredClone(originalSelectedParams)
const patch = inspectSelectedModelInputRecord(
tool,
modelInput.applyProjected(selectedParamsClone, projectedSelection.record)
)
if (!patch || !haveExactOwnKeys(selected, patch.record)) {
throw new Error(MODEL_INPUT_PROJECTION_ERROR_MESSAGE)
}
const patchedParams = { ...projectedParams, ...patch.record }
const verification = inspectSelectedModelInputRecord(tool, modelInput.select(patchedParams))
if (
!verification ||
!haveExactOwnKeys(selected, verification.record) ||
!isDeepStrictEqual(verification.record, projectedSelection.record)
) {
throw new Error(MODEL_INPUT_PROJECTION_ERROR_MESSAGE)
}
return patchedParams
} catch {
throw new Error(MODEL_INPUT_PROJECTION_ERROR_MESSAGE)
}
}
function formatToolRequest(tool: ToolConfig, params: Record<string, any>): PreparedToolRequest {
const url = typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url
const method =
typeof tool.request.method === 'function'
? tool.request.method(params)
: params.method || tool.request.method || 'GET'
const headers = new Headers(tool.request.headers ? tool.request.headers(params) : {})
const hasBody = method !== 'GET' && method !== 'HEAD' && Boolean(tool.request.body)
const bodyResult = tool.request.body ? tool.request.body(params) : undefined
const contentType = headers.get('content-type')
const isPreformattedContent =
contentType === 'application/x-ndjson' || contentType === 'application/x-www-form-urlencoded'
let body: string | undefined
if (hasBody) {
if (isPreformattedContent && typeof bodyResult === 'string') {
body = bodyResult
} else if (
isPreformattedContent &&
bodyResult &&
typeof bodyResult === 'object' &&
'body' in bodyResult
) {
body = bodyResult.body as string
} else {
body = typeof bodyResult === 'string' ? bodyResult : JSON.stringify(bodyResult)
}
}
const rawTimeout = params.timeout
const timeout = rawTimeout != null ? Number(rawTimeout) : undefined
const validTimeout =
timeout != null && Number.isFinite(timeout) && timeout > 0
? Math.min(timeout, getMaxExecutionTimeout())
: undefined
const proxyUrl =
typeof params.proxyUrl === 'string' && params.proxyUrl.trim()
? params.proxyUrl.trim()
: undefined
return {
url,
method,
headers,
body,
timeout: validTimeout,
proxyUrl,
stripAuthOnRedirect: tool.request.stripAuthOnRedirect,
isInternalRoute: url.startsWith('/api/'),
}
}
/** Materializes one tool HTTP request and attaches all private provenance metadata. */
export function prepareToolRequest(
tool: ToolConfig,
params: Record<string, any>,
registry?: ResolvedSecretTraceRegistry
): PreparedToolRequest {
const configuredUrl =
typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url
const modelInput = tool.request.modelInput
const secretProvenance = tool.request.secretProvenance
const hasPrivateModelInputProvenance =
modelInput?.mode === 'private-provenance' ||
(modelInput?.mode === 'project' && modelInput.privateInputPaths !== undefined)
if (hasPrivateModelInputProvenance && !configuredUrl.startsWith('/api/')) {
throw new Error(PRIVATE_MODEL_INPUT_EXTERNAL_URL_ERROR_MESSAGE)
}
if (secretProvenance && !configuredUrl.startsWith('/api/')) {
throw new Error(PRIVATE_SECRET_PROVENANCE_EXTERNAL_URL_ERROR_MESSAGE)
}
const requestInput = projectToolModelInputParams(tool, params, registry)
const request = formatToolRequest(tool, requestInput)
if (hasPrivateModelInputProvenance && !request.isInternalRoute) {
throw new Error(PRIVATE_MODEL_INPUT_EXTERNAL_URL_ERROR_MESSAGE)
}
if (secretProvenance && !request.isInternalRoute) {
throw new Error(PRIVATE_SECRET_PROVENANCE_EXTERNAL_URL_ERROR_MESSAGE)
}
const selectedModelInputPaths =
modelInput?.mode === 'private-provenance'
? modelInput.inputPaths(requestInput)
: modelInput?.mode === 'project'
? modelInput.privateInputPaths?.(requestInput)
: undefined
const modelInputMetadata = hasPrivateModelInputProvenance
? createModelInputProvenanceRequestMetadata(registry, selectedModelInputPaths ?? [])
: undefined
const secretProvenanceMetadata = secretProvenance?.request
? createPrivateSecretProvenanceRequestMetadata(registry, secretProvenance.request(requestInput))
: undefined
if (modelInputMetadata || secretProvenanceMetadata) {
if (!request.body) throw new Error('Model input provenance requires a JSON request body')
let requestBody: unknown
try {
requestBody = JSON.parse(request.body)
} catch {
throw new Error('Model input provenance requires a JSON request body')
}
if (!isPlainRecord(requestBody)) {
throw new Error('Model input provenance request body is invalid')
}
const bodyWithModelInputProvenance = addModelInputProvenanceToRequest(
requestBody,
request.headers,
modelInputMetadata
)
if (modelInputMetadata && modelInput?.mode === 'project') {
markModelInputProjected(request.headers)
}
request.body = JSON.stringify(
addModelInputProvenanceToRequest(
bodyWithModelInputProvenance,
request.headers,
secretProvenanceMetadata
)
)
}
if (!request.headers.has('User-Agent')) request.headers.set('User-Agent', 'Sim')
return request
}