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.
import osfrom 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 productions<Session user:maya.chen on http://localhost:8000>
1. Who am I, and what can I see?
Section titled “1. Who am I, and what can I see?”s.medetails
| principal | user:maya.chen |
|---|---|
| clients | acme-us, aurora, cinder, dune, harbor, northwind-e2e-live, northwind-outdoor, quill |
| can | create drafts |
Portfolio and artifact lists come back as DataFrames. Every number arrives with its interval and its range in words.
s.portfolio()[["name", "maturity_stage", "live_model_count", "iroas.label", "iroas.value", "iroas.range_words"]]| 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 |
s.artifacts(kind="model_manifest", client=CLIENT).tail(5)| 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 |
2. Check on the server before filing
Section titled “2. Check on the server before filing”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.
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"] = CLIENTs.check(model)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 |
3. File and submit
Section titled “3. File and submit”Submitting runs the checks. An untested model gets a G-ACC-01 fallback: usable, labelled, and the approver has to accept it by name.
art = s.file(model).submit(reason="quick OLS from notebook 03, for comparison with the Bayesian model")artdetails
| kind | model_manifest |
|---|---|
| client | northwind-e2e-live |
| owner | user:maya.chen |
| version | 1 |
| content hash | sha256:7c382245063eb4ccd4cc73aeefe6d4d2e6dde0aee9d0f8775f1a87337d49329f |
| G-ACC-01 UNVALIDATED_METHOD | unvalidated method; trust level reduced by one; labelled |
art.fallbacks.to_frame()| gate | code | field | assumed | effect | |
|---|---|---|---|---|---|
| 0 | G-ACC-01 | UNVALIDATED_METHOD | None | unvalidated method | trust level reduced by one; labelled |
4. Two people, always
Section titled “4. Two people, always”G-INT-02 is a hard stop: nobody approves their own work, whatever the mode.
try: art.approve(accept=art.fallbacks)except fin.Forbidden as refused: print(refused.code, "-", refused.detail, "| gate:", refused.gate)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.
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()not sent: name every fallback you accept, e.g. accept=art.fallbacks; not accepted: G-ACC-01:UNVALIDATED_METHOD
details
| kind | model_manifest |
|---|---|
| client | northwind-e2e-live |
| owner | user:maya.chen |
| version | 1 |
| content hash | sha256:7c382245063eb4ccd4cc73aeefe6d4d2e6dde0aee9d0f8775f1a87337d49329f |
| G-ACC-01 UNVALIDATED_METHOD | unvalidated method; trust level reduced by one; labelled |
5. Check the log yourself
Section titled “5. Check the log yourself”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.
proof = art.proof()proofdetails
| leaf hash recomputed | yes |
|---|---|
| audit path reaches the root | yes |
| root is the signed checkpoint's | yes |
| checkpoint signature (ump.poc/log) | yes |
| checkpoint signature (ump.poc/tsa) | yes |
| the leaf holds | acknowledged_fallbacks, approval_id, approver_ref, artifact_id, artifact_version, at, content_hash, decision, key_version, kind, payload_digest, signature, submitter_ref |
proof.to_frame()| 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 |
6. Lineage
Section titled “6. Lineage”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.
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/)")| 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 |
s.close()Going deeper
Section titled “Going deeper”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).