Skip to content

04 · ump-core from a notebook

You will learn

  • Read your portfolio and artifacts as DataFrames
  • File and submit a model, and see self-approval refused (G-INT-02)
  • Check an approval's log proof locally, then follow lineage

You need

  • Nothing to read it; rerunning needs the reference deployment, seeded once
  • Notebook 03, which builds the model this one files

fin.connect talks to ump-core over HTTP, so in the recorded session the gates, approvals and the sealed log ran for real.

In notebook 03 Priya checked her model on her laptop. Now she files it, and meets what a laptop cannot give her: a second person who must sign, a log entry that anyone can check, and lineage that ties her model to what it was built from.

Where this writes. The notebook files into northwind-e2e-live, the labelled test client the live e2e tests use, so the demo client northwind-outdoor stays clean. Set UMP_NOTEBOOK_CLIENT to change it. Each run adds one draft model and one approval; nothing is deleted.

In [1]:
import os
from pathlib import Path
import babelfin as fin
CLIENT = os.environ.get("UMP_NOTEBOOK_CLIENT", "northwind-e2e-live")
s = fin.connect(as_="maya.chen") # the POC's identity stub; IAP/OIDC in production
s
Out [1]:
<Session user:maya.chen on http://localhost:8000>
In [2]:
s.me
Out [2]:
Maya Chen (analyst)
analyst, experimentation_analyst, model_owner
details
principaluser:maya.chen
clientsacme-us, aurora, cinder, dune, harbor, northwind-e2e-live, northwind-outdoor, quill
cancreate drafts

Portfolio and artifact lists come back as DataFrames. Every number arrives with its interval and its range in words.

In [3]:
s.portfolio()[["name", "maturity_stage", "live_model_count", "iroas.label", "iroas.value", "iroas.range_words"]]
Out [3]:
name maturity_stage live_model_count iroas.label iroas.value iroas.range_words
0 Acme US model_live 1 search iROAS 2.222222 likely between $1.56 and $2.89
1 Aurora model_live 2 search iROAS (A) 2.000000 likely between $1.40 and $2.60
2 Cinder model_live 1 NaN NaN NaN
3 Dune model_live 1 search iROAS (seed readout) 3.384876 likely between $2.54 and $4.23
4 Harbor calibrated 3 tv iROAS (C) 1.100000 likely between $0.60 and $1.60
5 Northwind Outdoor [northwind-e2e-live] reallocating 2 tv iROAS 1.235399 likely between $0.60 and $1.87
6 Northwind Outdoor reallocating 2 tv iROAS 1.235399 likely between $0.60 and $1.87
7 Quill model_live 5 social iROAS (A) 1.500000 likely between $0.90 and $2.10
In [4]:
s.artifacts(kind="model_manifest", client=CLIENT).tail(5)
Out [4]:
id kind title state version client_id owner approved_by updated_at
0 mdl_d6b843bfde3848f2886d model_manifest Northwind MMM (Bayesian NUTS) [northwind-e2e-l... validated 2 northwind-e2e-live user:maya.chen user:sam.patel 2026-10-30T20:39:55.908017Z
1 mdl_a03aad21a7f6431c9709 model_manifest Northwind MMM (Bayesian NUTS) [northwind-e2e-l... validated 3 northwind-e2e-live user:maya.chen user:sam.patel 2026-10-30T20:39:54.850813Z

Load the model notebook 03 saved (or build a small one when 03 has not run) and dry-run the gates on the server, where core adds context from the client’s contract.

In [5]:
saved = Path("out/northwind_quick_ols.manifest.json")
if saved.exists():
model = fin.Model.load(saved)
else:
model = fin.Model.from_table({"tv": fin.number(1.37, (1.26, 1.50), "roi")}, estimand="roi",
name="Northwind quick OLS (synthetic data)", engine="notebook:numpy-ols")
model.doc["client_id"] = CLIENT
s.check(model)
Out [5]:
Would pass · 0 stops · 1 fallbacks · 0 warnings · 14 gates (2 fired)
next state: checks_passed
details
gate outcome message codes
G-ACC-01 fallback acceptance untested: usable, labelled unvalidated method, trust reduced UNVALIDATED_METHOD
G-CAP-01 route sandbox output: experimental label and a named approver needed EXPERIMENTAL_LABEL

Submitting runs the checks. An untested model gets a G-ACC-01 fallback: usable, labelled, and the approver has to accept it by name.

In [6]:
art = s.file(model).submit(reason="quick OLS from notebook 03, for comparison with the Bayesian model")
art
Out [6]:
Northwind quick OLS (synthetic data) · awaiting_approval
mdl_ba797fa8ee224e26ae82
details
kindmodel_manifest
clientnorthwind-e2e-live
owneruser:maya.chen
version1
content hashsha256:7c382245063eb4ccd4cc73aeefe6d4d2e6dde0aee9d0f8775f1a87337d49329f
G-ACC-01 UNVALIDATED_METHODunvalidated method; trust level reduced by one; labelled
In [7]:
art.fallbacks.to_frame()
Out [7]:
gate code field assumed effect
0 G-ACC-01 UNVALIDATED_METHOD None unvalidated method trust level reduced by one; labelled

G-INT-02 is a hard stop: nobody approves their own work, whatever the mode.

In [8]:
try:
art.approve(accept=art.fallbacks)
except fin.Forbidden as refused:
print(refused.code, "-", refused.detail, "| gate:", refused.gate)
Out [8]:
SELF_APPROVAL_FORBIDDEN - an approver cannot approve their own work (owner or submitter) | gate: G-INT-02

The approver signs, and has to name every fallback they accept. Leaving one out is refused before anything is sent.

In [9]:
with s.as_("sam.patel") as sam:
mine = sam.artifact(art.id)
try:
mine.approve(comment="looks fine")
except fin.UnacknowledgedFallbacks as missing:
print("not sent:", missing.detail)
mine.approve(accept=art.fallbacks, comment="Tier D comparison model; untested, acknowledged.")
art.refresh()
Out [9]:
not sent: name every fallback you accept, e.g. accept=art.fallbacks; not accepted: G-ACC-01:UNVALIDATED_METHOD
Northwind quick OLS (synthetic data) · validated
mdl_ba797fa8ee224e26ae82
details
kindmodel_manifest
clientnorthwind-e2e-live
owneruser:maya.chen
version1
content hashsha256:7c382245063eb4ccd4cc73aeefe6d4d2e6dde0aee9d0f8775f1a87337d49329f
G-ACC-01 UNVALIDATED_METHODunvalidated method; trust level reduced by one; labelled

The approval is a leaf in the sealed Merkle log. art.proof() fetches the leaf, its audit path and the signed checkpoint, then recomputes everything here with ump_spec.integrity, so you do not have to trust the platform.

In [10]:
proof = art.proof()
proof
Out [10]:
Leaf 48: verified here
ump/approval/v1 · tree size 49
details
leaf hash recomputedyes
audit path reaches the rootyes
root is the signed checkpoint'syes
checkpoint signature (ump.poc/log)yes
checkpoint signature (ump.poc/tsa)yes
the leaf holdsacknowledged_fallbacks, approval_id, approver_ref, artifact_id, artifact_version, at, content_hash, decision, key_version, kind, payload_digest, signature, submitter_ref
In [11]:
proof.to_frame()
Out [11]:
check passed
0 leaf hash recomputed True
1 audit path reaches the root True
2 root is the signed checkpoint's True
3 checkpoint signature (ump.poc/log) True
4 checkpoint signature (ump.poc/tsa) True

A fresh notebook model has no parents unless you file it with parents=. The platform’s calibrated model for the same client shows a full chain: data, transform, fit, test and calibration.

In [12]:
calibrated = s.artifacts(kind="model_manifest", client=CLIENT, state="validated")
calibrated = calibrated[calibrated.title.str.contains("calibrated", na=False)]
if len(calibrated):
display(s.artifact(calibrated.iloc[-1]["id"]).lineage().to_frame())
else:
print("no calibrated model yet; run the e2e story (uv run pytest -m live in e2e/)")
Out [12]:
from_id to_id link_type role
0 cal_5c1132d5f8934c5599af mdl_d6b843bfde3848f2886d calibrated_by calibrated_version
1 cap_5c4ce9b38b104421bb4c mdl_a03aad21a7f6431c9709 derived_from capability
2 ds_71cbf9a755a04359a1f4 ds_de70dac71fae481baffb derived_from raw
3 ds_71cbf9a755a04359a1f4 tfm_ce4cd8717612486e9d4e derived_from input
4 ds_de70dac71fae481baffb mdl_a03aad21a7f6431c9709 derived_from training_data
5 mdl_a03aad21a7f6431c9709 cal_5c1132d5f8934c5599af derived_from NaN
6 mdl_a03aad21a7f6431c9709 mdl_d6b843bfde3848f2886d derived_from NaN
7 mdl_a03aad21a7f6431c9709 tpl_a75d899efca94c6ab4db designed_from sized_from
8 tfm_ce4cd8717612486e9d4e ds_de70dac71fae481baffb mapped_by transform
9 tpl_a75d899efca94c6ab4db trs_153dbfcc17be459baac2 derived_from NaN
10 trs_153dbfcc17be459baac2 cal_5c1132d5f8934c5599af calibrated_by evidence
In [13]:
s.close()

s.core is the underlying ump_sdk.CoreClient for anything babelfin does not wrap yet, and art.envelope is the raw artifact JSON. Next: 05 · The apps from a notebook uses the apps (models, planner, experiments, console).