0016. Trace-source connectors pull from MLflow, Langfuse and Phoenix
- Status: Accepted, 2026-10-09; built 2026-10-10 under milestone M3 (PRD S1 to S8). “As built” at the end lists where the code differs from the items below; the code wins.
Context
Section titled “Context”OTLP is a push protocol. An agent harness that already keeps its traces in MLflow, Langfuse or Phoenix can add Evals.si as a second exporter, but cannot get history from before the install, and cannot see Evals.si’s scores in the store its people already read. The PRD (journey 4, A4, S1 to S8) asks for a TraceSource that backfills, tails, feeds the same OnlineEvalPolicy objects, and writes scores back. The first customer, agent harness, traces to MLflow 3.x (open source, Databricks, SageMaker or Azure ML), and the Deep Agents demo (R6) is the first test.
What the code gives us:
- Pulled traces enter the policy engine through
watch.Engine.IngestBatch([]ingest.Trace), which already stores them (an upsert on(project, trace_id)ininternal/store/traces.go) and queues them for policies. ingest.Spanwraps an OTLPtracepb.Span.ingest.ToRecordalready reads the OTel GenAI, OpenInference, OpenLLMetry and MLflow LangChain conventions (normalize.go,langchain.go). MLflow and Phoenix both return OTLP spans.- Sinks (
internal/sinks/{mlflow,langfuse,phoenix}.go) already write online scores back to a trace whentrace_feedbackis on, so the write path exists. - One replica runs the policy engine, chosen by the
policy-enginelease (internal/store/leases.go,internal/server/cluster.go). - Credentials follow 0015: a named variable, granted per project and host.
Facts below come from the pinned sources listed at the end. Where a source does not say, this record says so rather than guess.
Decision
Section titled “Decision”1. The resource and the API
Section titled “1. The resource and the API”TraceSourceis a new message inproto/evalsi/v1alpha1/source_service.proto, a CRD (the operator syncs it to the API as it doesOnlineEvalPolicy), and aSourceServicewith REST under/v1alpha1/sources: apply (create or replace, withvalidate_only, as in 0015 item 10 and like webhooks and policies), get, list, delete,:pause,:resume,:backfill. The PRD’s example is the shape, with these changes:credentialsreplacescredentialsSecretRef. It names a variable or a mounted file, never a value (item 22).mappingbecomesprofileplusoverrides(items 19 and 20).variantis added for MLflow (item 7).
- Spec:
project,connector(mlflow,langfuse,phoenix),variant,endpoint,locations(MLflow experiment IDs or UC schemas, a Langfuse environment filter, a Phoenix project),credentials,auth(sigv4.service,sigv4.region, for SageMaker;databricks.sqlWarehouseId, for UC search),backfill.since,poll.interval,poll.maxRecordsPerSecond,maxTraceDuration,maxInProgressAge,profile,overrides,policies(names; empty means every policy of the project, because a pulled trace is an ordinary trace in the project),writeBack,labels. - Status:
watermark,lagSeconds,recordsPerSecond,deferred(traces waiting to finish),lastError,lastWriteBack,phase(Backfilling,Tailing,Paused,Error). S8 asks for these on the resource. - A source belongs to one project and is authorized like a policy: creating one needs the project’s editor role, and the endpoint and credentials are checked against grants (item 22) at the API, not only when the loop starts.
2. The Go interface
Section titled “2. The Go interface”-
Package
internal/source:type Connector interface {// List returns traces that started at or after req.Since, oldest first.List(ctx context.Context, req ListRequest) (ListResult, error)// WriteBack records scores on the source's own traces. It is idempotent per (trace, metric).WriteBack(ctx context.Context, scores []Score) error}type ListRequest struct{ Since time.Time; Cursor string; Limit int }type ListResult struct {Traces []Pulled // Pulled{Trace ingest.Trace; Started time.Time; State: Complete | InProgress}Next string // opaque page token; empty at the end}A connector owns its wire format, auth and paging; it returns
ingest.Trace(OTLP spans plus the source’s tags as resource attributes). The manager owns watermarks, lookback, dedup, rate limits, retries and status. Contract tests for each connector run against recorded responses, asinternal/ingest/demo_fixture_test.godoes for the demo trace. -
The manager runs on the policy-engine leader, which is what the PRD says: one replica owns every source, and a failover resumes from the stored watermark. It never runs in followers, so a source is never pulled twice at once. A per-source lease can come later if one replica cannot keep up; the interface does not change.
3. MLflow, per variant
Section titled “3. MLflow, per variant”-
A
variantselects the transport, because the variants do not share one API:Variant Search Spans Auth oss(MLflow 3.x)POST /api/3.0/mlflow/traces/searchGET /api/3.0/mlflow/traces/get,GET /api/3.0/mlflow/traces/batchGetnone, a bearer token, or what a reverse proxy adds databricksPOST /api/4.0/mlflow/traces/search-long-running, then poll.../search/operations/{name}; falls back to the v3 search for experiments/api/4.0/mlflow/traces/{location}/batchGetfor a UC schema location (the client builds the path; the HTTP method was not read)a PAT or OAuth M2M token sagemakerthe osscallsthe osscallsAWS SigV4 azuremlnot decided (item 11) not decided Azure identity -
Open source.
- Search.
SearchTracesV3takeslocations(experiment IDs),filter,max_results(at most 500, default 100),order_byandpage_token, and returnstraces(TraceInfoV3) andnext_page_token. The filter is a subset of SQL, ANDed, for exampletrace.timestamp_ms > 1711089570679, andorder_bytakes["timestamp_ms ASC"]. - Spans.
GetTraceandBatchGetTracesreturn spans as OTLPSpanmessages. The proto marks both “for OSS MLflow only”.BatchGetTracestakestrace_idsand optionalexperiment_ids. The connector searches forTraceInfoV3pages, then batch-fetches spans for the traces it will emit.TraceInfoV3.request_previewandresponse_previeware cut at 10 KB, so the connector never builds a record from them; they are foroverridesselectors only. - Limits. MLflow publishes no rate limit for the server. Back off on HTTP 429 and 503, honouring
Retry-After, because a reverse proxy in front of it may limit.
- Search.
-
Databricks.
- Search. The client (MLflow 3.17.0,
databricks_rest_store.py) searches through a long-running operation:search-long-runningreturns an operation; pollsearch/operations/{name}untildone. A UC-schema location needs a SQL warehouse (MLFLOW_TRACING_SQL_WAREHOUSE_IDin the client). When the V4 endpoint is missing, or says experiment locations are “not yet supported”, the client falls back to the v3 search; the connector does the same. - Spans. The client’s
get_traceraises “not implemented” against Databricks, andbatch_get_tracesthere requires alocationand rejectsexperiment_ids. Its request carrieslocation_idandsql_warehouse_id, which the open-source message does not have, so Databricks uses its own message and the connector builds it from a recorded response, not fromservice.proto. So the first cut reads spans for UC-schema locations withbatchGet. Spans for experiment-located traces on Databricks come through the trace-data artifact, which this record has not verified, so the connector does not claim them until a contract test against a recorded Databricks response passes. - Auth and limits. A personal access token or OAuth machine-to-machine token. Databricks says in its tracing FAQ that quotas and rate limits apply but gives no numbers; back off on 429 and honour
Retry-After. Its search returns the most recent 1,000 traces by default, so the connector always passesmax_resultsand a time filter.
- Search. The client (MLflow 3.17.0,
-
SageMaker. A managed tracking server is reached with the
sagemaker-mlflowplugin, which adds AWS Signature V4 headers to every request using the boto3 credential chain; the connector signs the same way, with the default AWS SDK for Go credential chain (IRSA or pod identity on EKS). The signing service name and the tracking-server header come from a community sample and not from the plugin’s code, so they are settings (auth.sigv4.service,auth.sigv4.region), not constants, and must be checked on a real server before M3 ships the variant. Whether a SageMaker tracking server serves the MLflow 3 trace API at all is unverified. -
Azure ML. The
azureml://tracking URI authenticates with Azure identity (DefaultAzureCredential), not a token. No source found for this record shows MLflow 3 tracing on an Azure ML workspace. The variant is out of the first cut until a trace round trip is shown on a real workspace (see “Found wrong in the PRD” below). -
Write-back to MLflow reuses
mlflowSink.ExportTrace(POST /api/3.0/mlflow/traces/{trace_id}/assessments, aFeedbackassessment withassessment_name,rationale, and asourceof typeCODEorLLM_JUDGEwithsource_id: evalsi/<policy>).CreateAssessmenthas no idempotency key, so re-delivery would add duplicates. The connector keepssource_writes(source, trace_id, metric, remote_id, digest)and, when it has the remote ID, sendsPATCH .../assessments/{id}with the changed fields; with no row (a lost table) it first readsTraceInfoV3.assessmentsand matchesassessment_nameandsource.source_id. The same table makes a retry after a crash do nothing.
4. Langfuse and Phoenix
Section titled “4. Langfuse and Phoenix”- Langfuse.
- Read. Basic auth (public key as user, secret key as password). The spec’s own description says the real-time read path is
GET /api/public/v2/observations(limitup to 1000, an opaque base64cursor,fromStartTime,traceId,isRootObservation,fieldsgroups such asio,model,usage), and that the older read endpoints, such asGET /api/public/traces(page,limit,fromTimestamp,orderBy), can delay data by about ten minutes. A ten-minute delay breaks the two-minute p95 lag target, so the connector reads v2 observations and groups them bytraceId; it never uses/tracesfor tailing. Observations are not OTLP, so the connector builds GenAI-convention spans from them (generation to model call with usage, span and tool to tool call). - Limits. The spec documents no rate limit and no 429 for the observations or scores endpoints (it lists 429 only on its evaluator, dashboard and rule endpoints). Back off on 429 and 503 anyway.
GET /api/public/tracesis also marked deprecated on Langfuse Cloud, to be removed on 16 November 2026, withv2/observationsthe named replacement. - Write.
POST /api/public/scoreswithtraceId,name,value,comment,dataType,sourceand a client-suppliedid. The connector setsidto a hash of source, trace and metric. That scores are replaced when a score with thatidis sent again is not stated in the spec and is unverified; until a test against a real Langfuse shows it, the connector also remembers scores insource_writesand does not re-send.
- Phoenix.
- Read.
GET /v1/projects/{project}/spans/otlpv1(summary “Search spans with simple filters”) andGET /v1/projects/{project}/spanstakestart_time(inclusive),end_time(exclusive),cursorandlimit(at most 1000);/spansalso takessort(id, which “orders by insertion”, orstart_time) andorder, and/otlpv1does not, so tailing uses/spanswithsort=idand/otlpv1serves backfill by time. The pinned spec declares no security scheme, so the auth header is unverified (Phoenix documents an API key; check the header before shipping). Phoenix returns spans, not traces, so the connector feeds them to the existingingest.Assembler(root-ended grace, idle, max age) and emits a trace when it is complete. The cursor is valid only “with the same sort”. - Write.
POST /v1/span_annotationsor/v1/trace_annotationswithname,annotator_kind(LLM,CODE,HUMAN),result(label,score,explanation),metadataandidentifier, which per the schema updates the annotation if it exists. That is a documented upsert, so Phoenix needs no local bookkeeping beyondsource_writes.
5. Watermarks and delivery
Section titled “5. Watermarks and delivery”- The watermark rule is per connector, because stores log traces at different times. MLflow’s
TraceInfoV3.StatesaysIN_PROGRESSexists only “for incremental/distributed tracing logging in contrast with when the full trace is logged only upon its completion”. The normal case is a trace that appears when it ends, filtered by its start time (timestamp_ms). A 20-minute run that starts at T and is logged at T+20 is missed by any cycle whoseSincehas passed T. So:
- MLflow (every variant) has no write-time field to filter on. Each cycle asks for
Since = watermark - maxTraceDuration.maxTraceDurationis a setting with the default 30 minutes, and a trace longer than it is missed; the guide says so. The cost is re-reading that window each poll, which dedup absorbs (item 17) and the rate cap bounds. Search returnsTraceInfoV3only; spans are fetched for unseen IDs, so a re-read costs one search page, not a span fetch. - Phoenix tails
/spanswithsort=id(insertion order), so the cursor is monotonic and a late root span arrives after the children it closes. The watermark is the last cursor. The assembler (item 14) decides completeness. - Langfuse reads
v2/observationsbyfromStartTime/toStartTime; the spec does not say what order its cursor follows, so the MLflow rule applies, withmaxTraceDurationas the lookback.
- Incremental exporters. A trace that a store reports as
IN_PROGRESSis deferred, counted instatus.deferred, and held back from the watermark, which does not pass the start of the oldest deferred trace. AftermaxInProgressAge(default 1 hour) it is emitted as it is, marked incomplete, so one stuck trace cannot freeze a source. This is an addition for exporters that log incrementally, not what makes completion safe. - At least once, with a durable commit.
- The problem.
IngestBatchreturns nothing. It logs a failed store write and counts it, and it drops a trace when the evaluation queue is full. Both are right for a live OTLP stream and wrong for a watermark: the manager would advance past traces that were never stored or never scored. - The change. Add
IngestBatchContext(ctx, traces) error(built), which returns the store error and, instead of dropping, waits for queue space (bounded by ctx and by the source’s rate limit).IngestBatchcalls it and keeps its present behaviour for OTLP. - The commit. The manager advances the stored watermark (
source_state) only after the call returns nil. That means the trace is stored and queued, not scored: a crash after enqueue and before evaluation leaves it stored and insource_seen, and it is not scored. Delivery to the store is at least once; scoring is at most once per crash (see “As built”: thescoredflag was not built). Dedup usessource_seen(source, trace_id, digest, scored), pruned pastwatermark - maxTraceDuration, so re-reads do not rescore. A crash between ingest and commit re-delivers, and the(project, trace_id)upsert makes the store write harmless. - Rescoring. A trace already scored is not scored again unless its content changed (a digest in
source_seen), as late spans can change a trajectory.
- Backfill is the same loop with
Since = now - backfill.sinceand a higher rate cap, paged oldest first;:backfillrestarts it from a given time. Thepoll.maxRecordsPerSecondcap applies to both, and a per-project quota (existinginternal/store/quota.gocounters) bounds a project’s pull throughput.
6. Mapping
Section titled “6. Mapping”- A profile layers on the existing normalization; it is not a second parser. The pipeline is: decode the source payload to OTLP spans, set the resource attributes the source gives (tags, metadata, experiment,
evalsi.label.*fromlabels), and callingest.ToRecord. The profile name picks the decoder and defaults:mlflow,phoenixandlangfuseeach;auto(the default) letsToRecorddetect OTel GenAI, OpenInference, OpenLLMetry and MLflow LangChain/LangGraph conventions, as it does for pushed traces. The five first-cut agent frameworks’ profiles are fixture tests ofToRecordon recorded traces, not new code paths. overridesare CEL expressions (the repository already usescel-gofor policies) that set what the conventions did not:input,output,reference,context,metadata.<key>andlabels.<key>. As built they see what policy selectors see (service,name,attributesof the root span,resource,labels) plusinputandoutputas text; MLflow tags are resource attributesmlflow.tag.<key>, so the PRD’s example isresource["mlflow.tag.agent_harness.workflow_id"]. Overrides run insideToRecord, after the conventions, and only on the listed fields, so a bad expression cannot change evaluation semantics.- ID encoding. OTLP JSON uses hex strings for IDs while protobuf JSON uses base64;
internal/ingest/otlp.goalready converts hex to base64. MLflow’s response encoding of span IDs is not verified here: the first connector commit records real v3.17.0 responses from the demo’s MLflow server and the decoder accepts both encodings.
7. Credentials and egress
Section titled “7. Credentials and egress”- Credentials follow 0015 and add no RBAC to evalsid. The spec names
credentials: {env: NAME}or{file: <secret>/<key>}. A name is checked as a grant (0015 keys grants by variable name; this adds a file form,file:<secret>/<key>, to the grant syntax, and nothing else about grants changes) for the source’s project and the endpoint’s host, over HTTPS (or loopback), exactly as for a judge. The value reaches evalsid as an environment variable or a file from a Secret the chart mounts (sources.secrets), kubelet refreshes mounted files without a restart, and the connector re-reads the file at each use. The values are never in the spec, the API or the logs. evalsid therefore needs noget secretspermission, and the operator stays an API client (0012). The cost: a new Secret name needs a chart upgrade. Reading Secrets through the Kubernetes API is a possible later mode with its own Role. - The standalone path
POST /v1alpha1/sourcesis the same message. Itscredentialsnames a variable or a file path undersources.dirin the server config; there is no Kubernetes there. - Endpoint checks. A source endpoint must be
httpsunless the grant setsallow_http; loopback and link-local addresses (including cloud metadata) are refused unless the config lists them, so a project editor cannot point a source at an internal service. - Egress. Connectors run in the evalsid process, so the pod’s egress is the process’s egress: judges, sinks, Postgres, NATS and S3 as well. The PRD says the operator generates a NetworkPolicy for each source; that is not done, for two reasons:
- Kubernetes
NetworkPolicyselects by pod, namespace and CIDR and cannot name a host. Databricks, SageMaker and most hosted MLflow have no stable addresses. - Any egress policy that selects the evalsid pod denies everything it does not list, so a policy generated per source would cut off the others unless something owns the whole list. The chart owns that list.
Instead: the chart takes networkPolicy.egress.sources (selectors for in-cluster stores, CIDRs for fixed ones) and, when networkPolicy.egress.enabled, renders one policy that lists the infrastructure egress it already knows (DNS, Postgres, NATS, S3, ClickHouse, sinks and judges from values) plus those sources. It is off by default so that existing installs do not lose traffic. For hosted stores reached by name, the guide says to restrict by FQDN in the mesh or CNI (an Istio ServiceEntry and egress gateway, or a Cilium FQDN policy). If a customer wants the pull path isolated, sources.dedicated: true runs the manager in its own Deployment (the same binary, --role sources, taking the engine’s lease over the API) with a policy that lists only sources and evalsid itself. That mode is a follow-up; the interface does not change.
26. Data handling. Redaction rules apply to pulled records before storage and before any judge call, as for pushed ones. Retention is the project’s.
Consequences
Section titled “Consequences”- The
watchpackage gainsIngestBatchContext; the OTLP path is unchanged. New tables:source_state,source_seen,source_writes. - The sinks’
trace_feedbacksettings andTraceSource.writeBackoverlap. A source’s write-back is the way to write to the store it reads; the sink setting stays for traces that arrive by OTLP and are also in a store. A project must not turn both on for one store (the manager refuses it when it can tell). - The first cut ships MLflow OSS and SageMaker (after a real-server check), then Databricks for UC-schema locations, then Langfuse and Phoenix. Azure ML and Databricks experiment-located spans wait for evidence.
- Egress isolation is weaker than the PRD wants until
sources.dedicatedor a mesh policy is in place. The guide must say so. - Connector contract tests need recorded responses from each store at a pinned version; an upstream API change is a failing test, not a silent change.
Found wrong in the PRD
Section titled “Found wrong in the PRD”- Azure ML is listed with MLflow in M3 and S2. Nothing found shows it serving MLflow 3 traces; treat it as unverified and out of the first cut.
- Langfuse is listed as a first-cut source with a p95 lag under two minutes. Its older read endpoints lag by about ten minutes by its own spec; only the v2 observations API is real time, and the connector has to build spans from observations.
- “The operator generates a NetworkPolicy for each source” cannot be done as written (item 25).
- Databricks is listed as one MLflow target. Its search is long-running and its span fetch differs from open source; it is a separate variant.
Sources (pinned)
Section titled “Sources (pinned)”- MLflow v3.17.0:
mlflow/protos/service.proto(searchTracesV3at line 893, message at 4926;getTrace817;batchGetTraces836;createAssessment1335;TraceInfoV34506),mlflow/protos/assessments.proto,mlflow/store/tracking/databricks_rest_store.py(search_traces,batch_get_traces,get_trace),mlflow/store/tracking/rest_store.py(search_traces,get_trace),mlflow/utils/rest_utils.py(_V4_TRACE_REST_API_PATH_PREFIX = /api/4.0/mlflow/traces), https://github.com/mlflow/mlflow/tree/v3.17.0. - Langfuse OpenAPI at https://github.com/langfuse/langfuse/blob/223978955ebfe6cbf6e8572524f64c2565d43039/web/public/generated/api/openapi.yml (info description;
/api/public/v2/observations,/api/public/traces,/api/public/scores). - Phoenix OpenAPI at https://github.com/Arize-ai/phoenix/blob/35f86e5726a5881ecb69851bc7b8d27a34602c77/schemas/openapi.json (
getSpans,spanSearch,annotateSpans,annotateTraces). - Databricks: Tracing FAQ (quotas, 1,000 recent traces, pagination), Programmatic access to traces (
locations, SQL warehouse). - SageMaker: aws/sagemaker-mlflow (SigV4,
service_nameis a constructor argument; its value is not read from the plugin here), MLflow tracking servers. - Azure ML: Configure MLflow for Azure Machine Learning.
Verified against MLflow 3.17.0
Section titled “Verified against MLflow 3.17.0”Recorded from a real server (internal/source/mlflow/testdata): span and trace IDs inside get and batchGet are base64 (protobuf JSON), the trace-level ID is tr-<hex> (the OTLP trace ID is that hex), span attributes are decoded OTLP AnyValue JSON (mlflow.spanInputs is a kvlist_value, not a JSON string), search returns each trace’s assessments, PATCH .../assessments/{id} with an update_mask updates a feedback value in place, and a search with no match returns {}. This settles item 21.
As built (2026-10-10)
Section titled “As built (2026-10-10)”Where the code differs from the items above:
- Item 1. The RPC is
ApplySource(create or replace), as for webhooks and policies.locationstakes MLflow experiment names as well as IDs (a name is resolved withGET /api/2.0/mlflow/experiments/get-by-name): MLflow assigns IDs, so a resource written before the experiment exists, such as the demo’s, can only know its name. - Item 14, Phoenix. It lists traces, not spans:
GET /v1/projects/{project}/tracestakes a start-time lower bound, sorts by start time and pages by cursor, which fits the MLflow rule in item 15 exactly; a trace whose root span has not arrived is in progress.include_spansreturns span summaries without attributes, so spans are fetched withGET /v1/projects/{project}/spans?trace_id=.... Phoenix movesopeninference.span.kindout of the attributes intospan_kindand returns no resource attributes; the connector puts the kind back. The annotationidentifierupserts (checked on Phoenix 20.20.0), so write-back needs no lookups. Auth is sent as a bearer API key; Phoenix’s spec declares no scheme and an authenticated Phoenix was not tried. - Item 13, Langfuse. The spec does not say in which order v2 observations come, so
Listreads one start-time window of root observations at a time (from an hour, doubling across empty windows up to a week), sorts it, and returns the next window as the cursor; pages therefore stay oldest first. Self-hosted 4.56.0 passed two-stage verification, including same-ID replacement and Secret/Service policy write-back with enforced egress. The credential is<public key>:<secret key>. - Databricks experiment transport. Numeric experiment IDs use the shared v3 search, metadata and assessment APIs with workspace bearer authentication. Fetch uses
GET /api/2.0/mlflow/traces/{id}/credentials-for-data-download, then a guarded HTTPS download with only the returned artifact headers and no redirects. MLflow 3.17.0’sdatabricks_artifact_repo_resources.py,databricks_artifacts.protoandentities/span.pydefine this contract; artifact span attributes contain JSON-encoded strings. Contract-tested, not verified on a Databricks workspace. Unity Catalog, warehouse tracing and automatic token acquisition are not built. - Item 15.
maxTraceDurationapplies to Phoenix and Langfuse too: all three list by start time. - Item 17. The
scoredflag and re-queueing after a restart were not built: a trace stored but not yet scored when the server stops is not scored later. The manager also ignores a listed trace that started before the query’s start, should a store ignore the filter. - Item 18. There is one rate cap (
poll.maxRecordsPerSecond, elsesources.max_records_per_second, default 200/s) for backfill and tailing alike, and no per-project pull quota yet. - Item 20. See the item: the variables are the policy selectors’ plus
inputandoutput. An expression that fails leaves its field alone. - Item 24. The rule is the server’s, not the grant’s: an endpoint must be
httpsto a public name unlesssources.allow_hostslists its host, which then may also be plain HTTP or a private, loopback or cluster-internal address. A credential’s own grant still applies on top (hosts,allow_http). Checking the URL string is not enough (a public name can resolve to169.254.169.254, an allowed store can redirect to an internal address), so the sources’ HTTP client resolves every request’s host, redirect hops included, refuses internal addresses for hosts not listed, and dials the address it checked. A proxy the environment configures (HTTPS_PROXY) is dialed as such; the target is still checked. - Item 25. Built as described:
networkPolicy.egress(off by default) andprometheusRule. Thesources.dedicateddeployment was not built. - Item 26. Not true as written: evalsid has no ingest-time redaction for pushed or pulled traces (guardrails redact, inline, at a gateway). Pulled traces are stored as the store returned them.
- Kubernetes. Only the
adminrole may writeTraceSources (an aggregatedevalsi-adminClusterRole), matchingsources.write, which only the API’sadminrole holds. The operator runs without the TraceSource controller when the CRD is missing, since Helm does not upgrade CRDs. - Consequences, first item. The manager does not refuse a project that turns on both a sink’s
trace_feedbackand a source’swriteBackfor the same store; the guide says not to. Not built. - The demo.
mlflow.langchain.autolog()frommlflow-skinny3.17.0 imports pandas, whichmlflow-skinnydoes not install; the demo image now pins it. Without it the Deep Agents service could not start with tracing on.
As built: Databricks Unity Catalog, SageMaker, Azure ML (2026-10-12)
Section titled “As built: Databricks Unity Catalog, SageMaker, Azure ML (2026-10-12)”Read from the pinned packages mlflow-skinny 3.17.0, sagemaker-mlflow 0.5.0, azureml-mlflow 1.62.0.post6 and databricks-sdk 0.151.0. Nothing here was run against a Databricks workspace, a SageMaker tracking server or an Azure ML workspace; the tests use derived fixtures (internal/source/mlflow/testdata/derived/README.md).
- Item 9, Databricks Unity Catalog.
locationsof the formcatalog.schema(withauth.databricks.sql_warehouse_id) use the V4 API:POST /api/4.0/mlflow/traces/search-long-running(locations asUC_SCHEMA),GET .../search/operations/{name}untildone,GET .../{location}/batchGet?trace_ids=...&sql_warehouse_id=...for spans (settling the “HTTP method was not read” note in item 9: it isGET), andPOST/PATCH .../{location}/{trace_id}/assessments[/{id}]?sql_warehouse_id=...for write-back. The source’s ID for a trace iscatalog.schema/<trace id>. A source reads experiments or schemas, never both, as the client refuses. The connector does not fall back to V3 for schemas (the client does not either: it reports that UC search is unsupported). The V4 filter andorder_bystrings are the V3 ones, which is an assumption. - Databricks OAuth. With
auth.databricks.client_idthe credential is a service principal’s secret, exchanged for an access token with the client-credentials grant at<endpoint>/oidc/v1/token(scopeall-apis, HTTP Basic client authentication), cached until a minute before expiry and replaced once after a 401. The token endpoint is derived from the source’s endpoint, never configurable, so the existing credential grant for that host covers the secret. - Item 10, SageMaker, changed. The record planned to sign with the server’s default AWS credential chain and to make the service name a setting. Both were wrong. The plugin (
auth_provider.py,mlflow_sagemaker_helpers.py,mlflow_sagemaker_request_header_provider.py) signs with servicesagemaker-mlflowfor a tracking server (sagemakerfor an app), sends the ARN inx-mlflow-sm-tracking-server-arn(x-sm-mlflow-app-arnfor an app) and talks tohttps://<region>.experiments.sagemaker.aws(https://mlflow.sagemaker.<region>.app.aws). The connector derives all of it fromauth.sigv4.arn;serviceandregionstay only as checks. The credential is a JSON object of access key, secret and optional session token from the source’s own credential. The server’s own AWS identity is not used: it would let a project editor sign as the server’s role, past decision 0015’s grants. The endpoint must be an https AWS host so a signature goes nowhere else. The signer is hand-written (the repository has no AWS SDK) and is checked against signatures botocore produced for the same requests. - Item 11, Azure ML, decided. Refused at apply. The
azureml-mlflowtracking store picks/mlflow/v2.0for MLflow 3 with the comment “V3 API not implemented yet”, and the trace search,batchGetand assessment endpoints the connector reads are all/api/3.0. Revisit only if Azure ML ships an MLflow 3 trace API; the work then is an Entra client-credentials token (the plugin’s scope ishttps://management.azure.com/.default) and the/mlflow/v2.0/subscriptions/.../workspaces/...base path. - Validation on apply.
ApplySourcevalidated a source by building its connector with an empty credential, which made every Databricks source fail to apply. It now passessource.ValidationTokenwhen the source names a credential, and connectors check the credential’s content when they connect.