You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Document, with worked examples, how to declare a t-digest (and similar sketches) user-side as a declarative numpy expression β running on unmodified Lambda workers with no layer redeploy.
Use that as the anchor for the larger design conversation: how do we define and allow user-side code, where the line sits between "declarative numpy" and "arbitrary code," and what the security model is.
Context: #279 ships t-digest as a vectorized built-in (build_tdigest / build_tdigest_pairwise), and #213 established "no user code on Lambda β built-ins + declarative numpy config only." But zagg does have a user-side extension path today, and it's underdocumented. This issue makes it concrete.
The expression travels as data in the user's config and runs on unmodified workers β no code shipped, no redeploy. This is the "extend aggregations as numpy operations" intent from #213, and it's the answer to "can a user run a custom aggregation without permission to update the Lambda layer?" β yes, if it's expressible as a control-flow-free numpy expression.
Examples to write (docs template / notebook)
A short template showing the same sketch at increasing optimization, so users can see the pattern and the ceiling:
Exact baseline β np.quantile / fixed-value-bin histogram per cell as a kind: ragged expression. Simplest, exact, but payload grows with resolution (no Ξ΄ budget).
Loop-free t-digest as a ragged expression β the ideal-threshold form (straight-line numpy: k1_inv(arange(Ξ΄)) β searchsorted β reduceat, no control flow), inlinable into a single expression. This is the real worked example: a full Ξ΄-budgeted t-digest a user can drop into a config with zero deploy. (Prototype lives in the t-digest agg hot path is unvectorized: ~15Γ build + ~6Γ fold available in pure numpyΒ #279 scratch bench as build_fullvec.)
Why the shipped built-in differs β the built-in uses the greedy form because loop-free shrinks the loss-free (exact) regime from ~1.27Ξ΄ to ~0.64Ξ΄, turning the typical ~500-obs cell at Ξ΄=512 from exact into approximate:
n obs
greedy (built-in)
loop-free (expression)
300
300 exact
300 exact
500
500 exact
401 approx
650
650 exact
429 approx
So: the built-in is the accurate default; the loop-free expression is the deploy-free, user-injectable option, with a documented accuracy caveat. Good teaching example of the tradeoff.
All examples must be binder-runnable with public/in-tree data (per CLAUDE.md Β§4 docs rule).
The larger conversation β user-side code policy
eval(expr, {"__builtins__": {}, "np": np, ...}) is a soft sandbox, not a security boundary. numpy alone can escape it (np.load β pickle β arbitrary code; other vectors exist). So today's expression path is safe when the job author is trusted (the operator submitting configs) but is not isolation for untrusted/multi-tenant users. Questions to settle here:
Do we want to bless the expression path as a documented, supported extension surface (with its trust assumptions stated), or keep it de-facto-internal?
Acceptance
A docs page or notebook template with examples (1)β(3), binder-runnable.
A written decision on the user-side-code trust model (even if the decision is "trusted authors only, documented as such").
Labeled discuss β no code until the trust model and doc shape are agreed.
π€ from Claude
Purpose
Two things in one place:
expressionβ running on unmodified Lambda workers with no layer redeploy.Context: #279 ships t-digest as a vectorized built-in (
build_tdigest/build_tdigest_pairwise), and #213 established "no user code on Lambda β built-ins + declarative numpy config only." But zagg does have a user-side extension path today, and it's underdocumented. This issue makes it concrete.The mechanism (what exists today)
A reducer can be declared two ways:
function:β a dotted path resolved byimportlib.import_modulefrom the installed zagg. Built-in only; the body must be packaged β needs a layer redeploy. (This is where t-digest agg hot path is unvectorized: ~15Γ build + ~6Γ fold available in pure numpyΒ #279's t-digest lives.)expression:β a numpy stringeval'd by_eval_expression_raw(src/zagg/config.py:2323) in a restricted namespace, and it already supportskind: raggedoutput (issue Tier-2 vectors: CSR ragged (values/offsets/cell_ids) + t-digest as List<FixedSizeList<2>>Β #48):The
expressiontravels as data in the user's config and runs on unmodified workers β no code shipped, no redeploy. This is the "extend aggregations as numpy operations" intent from #213, and it's the answer to "can a user run a custom aggregation without permission to update the Lambda layer?" β yes, if it's expressible as a control-flow-free numpy expression.Examples to write (docs template / notebook)
A short template showing the same sketch at increasing optimization, so users can see the pattern and the ceiling:
Exact baseline β
np.quantile/ fixed-value-bin histogram per cell as akind: raggedexpression. Simplest, exact, but payload grows with resolution (no Ξ΄ budget).Loop-free t-digest as a ragged expression β the ideal-threshold form (straight-line numpy:
k1_inv(arange(Ξ΄))βsearchsortedβreduceat, no control flow), inlinable into a single expression. This is the real worked example: a full Ξ΄-budgeted t-digest a user can drop into a config with zero deploy. (Prototype lives in the t-digest agg hot path is unvectorized: ~15Γ build + ~6Γ fold available in pure numpyΒ #279 scratch bench asbuild_fullvec.)Why the shipped built-in differs β the built-in uses the greedy form because loop-free shrinks the loss-free (exact) regime from ~1.27Ξ΄ to ~0.64Ξ΄, turning the typical ~500-obs cell at Ξ΄=512 from exact into approximate:
So: the built-in is the accurate default; the loop-free expression is the deploy-free, user-injectable option, with a documented accuracy caveat. Good teaching example of the tradeoff.
All examples must be binder-runnable with public/in-tree data (per CLAUDE.md Β§4 docs rule).
The larger conversation β user-side code policy
eval(expr, {"__builtins__": {}, "np": np, ...})is a soft sandbox, not a security boundary. numpy alone can escape it (np.loadβ pickle β arbitrary code; other vectors exist). So today'sexpressionpath is safe when the job author is trusted (the operator submitting configs) but is not isolation for untrusted/multi-tenant users. Questions to settle here:expressioneval blurs it β it's data that is also code.load/__*__), a small restricted DSL, or keeping user reducers strictly local + an upstreaming funnel (status quo per Event interchange + catalog for the temporal pipeline (antarctic_AR_dataset integration)Β #213).expressionpath as a documented, supported extension surface (with its trust assumptions stated), or keep it de-facto-internal?Acceptance
Labeled
discussβ no code until the trust model and doc shape are agreed.