Skip to content

feat(appkit): add on-behalf-of-user mode for agents - #631

Merged
MarioCadenas merged 4 commits into
execution-resource-guardsfrom
feat/agents-auth-mode
Oct 6, 2026
Merged

MarioCadenas merged 4 commits into
execution-resource-guardsfrom
feat/agents-auth-mode

Conversation

@MarioCadenas

Copy link
Copy Markdown
Collaborator

Stacked on #601.

Adds auth: "on-behalf-of-user" to the agents plugin. You can set it as the default for every agent with agents({ auth }), or per agent with createAgent({ auth }) or auth: in the agent.md frontmatter. If you leave it out, nothing changes: the model and hand-rolled tools run as the service principal, and plugin tools run as the user.

Behavior by mode

Piece Default (mixed) on-behalf-of-user
Model call service principal user
Plugin-toolkit tools user user
Hand-rolled tool({ execute }) service principal user
Sub-agents own mode own mode, never service principal
Standalone runAgent SP, or user tools with caller requires caller (or an ambient user scope)
MLflow tracing, thread store, catalog skills service principal service principal (exceptions)
Missing token plugin tools reject 401 before any model or tool call

How it works

  • DatabricksAdapter (model serving and AI Gateway) now also accepts a client provider () => WorkspaceClientLike, resolved on every call. Agents that AppKit builds from a model string get a provider. In an OBO run it returns the caller's client; otherwise it returns the same eagerly built SP client as before.
  • The routes check for a user token first and return 401 if there isn't one. Then they open createRequestScope(req) once per request and set an AsyncLocalStorage marker for the OBO run.
  • Sub-agents: an OBO child opens the user scope itself. Under an OBO parent the marker is already set, so every child stays the user (it never widens to the SP).
  • Thread-store writes and read_skill_file run through a new internal runOutsideCallerScope, so they stay SP.
  • A model 401 already goes through normalizeIdentityError, so it surfaces as IDENTITY_EXPIRED and the run is not retried as the SP.
  • An unknown frontmatter value such as auth: obo throws at boot. Otherwise a typo would silently fall back to mixed mode.

Differences from the design doc

  • The mode isn't stored on RunState. The ALS marker already gives sub-agents "never widen", and the adapter (built at setup) needs the marker anyway to pick its client.
  • Catalog skills stay SP, decided by the owner. Discovery runs once at boot as the SP, and skillCredentialMode: "obo" is documented as not wired yet. Reading skills as the user would let the SP list a skill the user then can't read.
  • No user-id attribute on traces, decided by the owner. Tracing auth is resolved at boot (SP). Thread rows are keyed by the user id.
  • An adapter you build yourself keeps the client you gave it. To get per-call resolution, pass workspaceClient: () => getWorkspaceClient().

Tests

Each new test was checked to fail when its change is reverted. They cover the HTTP routes (/invocations, /responses, /api/agents/chat), standalone runAgent, the frontmatter loader, the adapter, and skill reads. The existing mixed-mode HTTP identity tests stay unchanged and act as the regression guard.

Gates: build, pnpm -r typecheck, pnpm test (5544 passed), pnpm docs:build, and pnpm check all pass. The registry round-trip repro passes 20/20 against this build.

This pull request and its description were written by Isaac.

MarioCadenas and others added 3 commits October 6, 2026 15:33
Add `auth: "on-behalf-of-user"` on agents({ auth }), createAgent({ auth })
and agent.md frontmatter. An OBO agent runs the model call, plugin tools,
hand-rolled tools and sub-agent dispatch as the user. Omitting it keeps
today's mixed behavior unchanged.

- DatabricksAdapter accepts a client provider resolved per call; agents
  built from a model string use the caller's client in an OBO run.
- Routes reject an OBO request without a user token with 401 before any
  model or tool call, then open the user scope once per request.
- Sub-agents use their own mode but never widen to the service principal
  under an OBO parent. Standalone runAgent requires a caller.
- A model 401 mid-run surfaces as IDENTITY_EXPIRED with no SP retry.
- Thread-store writes and MLflow tracing stay service principal.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Catalog skills are discovered at boot as the service principal and form a
shared pool. Run read_skill_file outside the caller scope so an
on-behalf-of-user agent cannot list a skill as the app and then fail to
read it as the user. Document it as the third always-SP exception.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
An on-behalf-of-user agent whose DatabricksAdapter was built with a fixed
workspaceClient would silently run the model as the service principal.
Throw at boot instead and point to `workspaceClient: () =>
getWorkspaceClient()` or a model string. Adapters built from a model
string or with a client provider, and mixed agents, are unaffected.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas
MarioCadenas changed the base branch from execution-resource-guards to main October 6, 2026 13:51
@MarioCadenas MarioCadenas reopened this Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

📦 Bundle size report

Compared against bundle-size-baseline.json (main).

@databricks/appkit

npm tarball (packed): 1.2 MB (+33 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 1.2 MB (+36 KB) 441 KB (+13 KB)
Type declarations 454 KB (+8.3 KB) 165 KB (+3.1 KB)
Source maps 2.4 MB (+66 KB) 826 KB (+24 KB)
Other 11 KB 3.7 KB
Total 4.1 MB (+111 KB) 1.4 MB (+40 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 100 KB (+4.0 KB) 2.5 KB (-1 B) 103 KB (+4.0 KB) external 327 KB (+12 KB)
./beta 96 KB (+2.9 KB) 485 B (+7 B) 97 KB (+2.9 KB) external 291 KB (+8.7 KB)
./testing 41 KB (+3.0 KB) 32 KB (+1.0 KB) 73 KB (+4.0 KB) external 212 KB (+10 KB)
./tsdown 520 B 0 B 520 B external 813 B
./type-generator 23 KB 0 B 23 KB external 65 KB

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 96 KB
. utils.js initial 4.6 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 79 KB
./beta stream-manager.js initial 5.9 KB
./beta service-context.js initial 4.2 KB
./beta databricks.js initial 3.3 KB
./beta wide-event-emitter.js initial 3.2 KB
./beta client.js initial 594 B
./beta index.js initial 20 B
./beta supervisor-api.js lazy 193 B
./beta databricks.js lazy 177 B
./beta index.js lazy 115 B
./testing manifest.js initial 28 KB
./testing index.js initial 10 KB
./testing wide-event-emitter.js initial 2.9 KB
./testing index.js lazy 28 KB
./testing remote-tunnel-manager.js lazy 2.5 KB
./testing utils.js lazy 1.8 KB
./tsdown index.js initial 520 B
./type-generator index.js initial 23 KB

@databricks/appkit-ui

npm tarball (packed): 350 KB — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 395 KB 132 KB
Type declarations 229 KB 84 KB
Source maps 766 KB 253 KB (+1 B)
CSS 16 KB 3.2 KB
Total 1.4 MB 472 KB (+1 B)
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
./js 5.3 KB 49 KB 55 KB 208 KB 14 KB
./js/beta 20 B 0 B 20 B 0 B 0 B
./react 432 KB 49 KB 481 KB 1.3 MB 177 KB
./react/beta 1.0 KB 0 B 1.0 KB 0 B 1.9 KB

Chunks:

Entry Chunk Load Size (gz)
./js index.js initial 5.2 KB
./js chunk initial 120 B
./js apache-arrow lazy 49 KB
./js/beta beta.js initial 20 B
./react index.js initial 430 KB
./react tslib initial 2.1 KB
./react apache-arrow lazy 49 KB
./react/beta beta.js initial 1.0 KB

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

🤖 AppKit PR bot

🔬 Run evals

Start an eval for this PR from the evals-monitor app: Go to Evals Monitor →

📦 Try this PR's app template

Scaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh auth login — and the Databricks CLI):

gh run download 37492802585 -R databricks/appkit -n appkit-template-0.82.0-pr.5f6fbd7-feat-agents-auth-mode-631 -D appkit-pr-631 \
  && unzip -o "appkit-pr-631/appkit-template-0.82.0-pr.5f6fbd7-feat-agents-auth-mode-631.zip" -d "appkit-pr-631" \
  && databricks apps init --template "appkit-pr-631"

The template pins @databricks/appkit and @databricks/appkit-ui to tarballs built from this branch, so the scaffolded app runs against this PR's code.

@MarioCadenas
MarioCadenas changed the base branch from main to execution-resource-guards October 6, 2026 14:02
@MarioCadenas
MarioCadenas added this pull request to stack #602 October 6, 2026 14:53
@MarioCadenas
MarioCadenas marked this pull request as ready for review October 6, 2026 15:54
@MarioCadenas
MarioCadenas requested a review from a team as a code owner October 6, 2026 15:54
@MarioCadenas
MarioCadenas requested a review from ditadi October 6, 2026 15:54
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas
MarioCadenas merged commit 97d7660 into main Oct 6, 2026
12 checks passed
@MarioCadenas
MarioCadenas deleted the feat/agents-auth-mode branch October 6, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants