Skip to content

Document user-side t-digest via declarative numpy expression + define the user-side-code policyΒ #281

Description

@espg

πŸ€– from Claude

Purpose

Two things in one place:

  1. 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.
  2. 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 mechanism (what exists today)

A reducer can be declared two ways:

ns = {"__builtins__": {}, "np": np, "numpy": np, "len": len,
      "float": float, "int": int, "abs": abs, "sum": sum, **columns}
return eval(expression, ns)

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:

  1. Exact baseline β€” np.quantile / fixed-value-bin histogram per cell as a kind: ragged expression. Simplest, exact, but payload grows with resolution (no Ξ΄ budget).

  2. 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.)

  3. 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:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions