Skip to content

feat(appkit): surface caller expiry and support local OBO - #597

Merged
MarioCadenas merged 17 commits into
execution-standalone-userfrom
execution-identity-lifecycle
Oct 6, 2026
Merged

MarioCadenas merged 17 commits into
execution-standalone-userfrom
execution-identity-lifecycle

Conversation

@MarioCadenas

@MarioCadenas MarioCadenas commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Caller operations expose downstream credential rejection as IdentityExpiredError (IDENTITY_EXPIRED), and local apps can exercise real user execution through the normal npm run dev workflow.

This is layer 4 of the stack, based on #595 (execution-standalone-user). Design: design-docs/execution-identity-e2e.md, section 5.1.4. Cache partitioning is in #594.

  • AppKit-managed spans record appkit.execution.principal, appkit.execution.principal_id, and the initiating user's appkit.execution.actor_id when present. Tokens are never span attributes.
  • The server plugin injects forwarded user headers before plugin and custom routes in development. It uses DATABRICKS_TOKEN with DATABRICKS_HOST first, otherwise the explicitly configured DATABRICKS_CONFIG_PROFILE. No separate proxy command, target, or port is needed.
  • Credential lookup and refresh are shared and kept in memory. Lookup and refresh failures return 401. Existing forwarded tokens are preserved. Injection accepts same-origin loopback requests and can be disabled with APPKIT_DEV_OBO=false.
  • Header injection does not open a caller scope. asUser(req) selects user execution; unscoped calls keep the app's configured credentials. SP remains the ordinary default, group is deferred, and the marked DEV_OBO_FALLBACK remains when injection is disabled or unconfigured.
  • Validate relative imports while packaging the CLI, with the validator dependency and Knip entry declared explicitly.

Local emulation supplies user credentials; it does not emulate platform consent, scope enforcement, or resource provisioning. A real SP-versus-user comparison requires the app's default credentials to belong to an SP.

Compatibility

Plugin execute() preserves its existing failed-result envelope and adds an optional typed error field. Throwing APIs expose IdentityExpiredError, and HTTP/SSE agent responses carry its stable code. The error retains only a token fingerprint, not credential-bearing upstream errors. Non-401 failures and SP execution retain their existing behavior. Existing public compatibility aliases remain available.

Verification

  • pnpm -r typecheck, full monorepo unit suite: 5,301 passed, one existing skip.
  • Original dev-playground OBO regression tests pass unchanged.
  • pnpm build, pnpm docs:build, formatting/lint, and Knip.
  • AppKit tarball build and packaged CLI --help smoke check.
  • Coverage includes typed expiry, token-free errors, stream failures, span identity, token priority, explicit profile selection, refresh failure, header preservation, local request restrictions, and actual HTTP middleware injection.
  • Tests use fake credentials and loopback servers. No live Databricks credentials were used.

@MarioCadenas
MarioCadenas requested a review from a team as a code owner September 23, 2026 18:22
@MarioCadenas
MarioCadenas requested review from calvarjorge and removed request for a team September 23, 2026 18:22
@MarioCadenas
MarioCadenas added this pull request to stack #602 September 24, 2026 08:14
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

📦 Bundle size report

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

@databricks/appkit

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

dist raw gzip
JS (runtime) 1.2 MB (+25 KB) 437 KB (+9.3 KB)
Type declarations 452 KB (+6.4 KB) 164 KB (+2.4 KB)
Source maps 2.4 MB (+45 KB) 819 KB (+17 KB)
Other 11 KB 3.7 KB
Total 4.1 MB (+76 KB) 1.4 MB (+29 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 99 KB (+3.1 KB) 2.5 KB (-1 B) 102 KB (+3.1 KB) external 325 KB (+9.6 KB)
./beta 95 KB (+1.6 KB) 463 B (-15 B) 96 KB (+1.6 KB) external 288 KB (+5.2 KB)
./testing 41 KB (+2.2 KB) 32 KB (+1.0 KB) 73 KB (+3.2 KB) external 210 KB (+8.2 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 95 KB
. utils.js initial 4.6 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 78 KB
./beta stream-manager.js initial 5.9 KB
./beta service-context.js initial 4.1 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 155 B
./beta index.js lazy 115 B
./testing manifest.js initial 27 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 (+4 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 395 KB 132 KB
Type declarations 229 KB 84 KB (+1 B)
Source maps 766 KB 253 KB (+1 B)
CSS 16 KB 3.2 KB
Total 1.4 MB 472 KB (+2 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 Sep 24, 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 37492789246 -R databricks/appkit -n appkit-template-0.82.0-pr.889ede7-execution-identity-lifecycle-597 -D appkit-pr-597 \
  && unzip -o "appkit-pr-597/appkit-template-0.82.0-pr.889ede7-execution-identity-lifecycle-597.zip" -d "appkit-pr-597" \
  && databricks apps init --template "appkit-pr-597"

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 force-pushed the execution-identity-lifecycle branch from 2790860 to 25ecd06 Compare September 24, 2026 13:42
@MarioCadenas
MarioCadenas force-pushed the execution-identity-lifecycle branch 2 times, most recently from 45d2d28 to 81ff9a7 Compare September 25, 2026 08:36
@MarioCadenas
MarioCadenas force-pushed the execution-identity-lifecycle branch from e0c118d to d9da769 Compare September 29, 2026 15:03
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Use plain punctuation in the inherited error comment so generated pages follow repository style.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas
MarioCadenas force-pushed the execution-identity-lifecycle branch from d9da769 to 4b22685 Compare September 29, 2026 16:04
MarioCadenas and others added 9 commits October 1, 2026 10:51
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Resolves the agents.ts imports: keeps this branch's normalizeIdentityError
and drops createRequestScope, which the routes no longer use now that user
scope applies per plugin tool call.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
The identity-expiration route test made the model call fail with a 401.
The model call now runs as the app service principal, so its failure is
not a user identity expiry. The test now fails a plugin tool call, which
runs in user scope, and still checks that both agent routes return
IDENTITY_EXPIRED without leaking the token.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
The packaged-CLI import guard is useful but unrelated to local OBO, so it
moves to its own PR off main. Remove it here: the validate-package-imports
script and its test, the root knip entry, the root @ast-grep/napi
devDependency, and the dist-appkit import and call.

dist-appkit.ts now matches main again; the dev-obo move needed no change to
its copy logic. The appkit and shared @ast-grep/napi dependencies are
unchanged. Running the guard against this branch's packed CLI before the
removal found no missing relative imports.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
MarioCadenas and others added 3 commits October 5, 2026 16:30
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>

@atilafassina atilafassina left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be done in a separate PR.

I think Analytics swallows the IdentityError before hitting the normalization path, so identity issues in queries will surface as ExecutionError / generic 500s instead of 401s

snippet from warehouse connector:

  /**
   * Normalize an error caught during statement execution/polling and rethrow.
   * Abort and already-structured `AppKitError`s pass through untouched;
   * anything else is wrapped as an `ExecutionError`, preserving the SDK's
   * structured `ApiError.errorCode` (e.g. "INVALID_PARAMETER_VALUE",
   * "BAD_REQUEST") so callers can branch on a stable identifier rather than
   * substring-matching the message.
   */
  private _rethrowStatementError(error: unknown): never {
    if (error instanceof Error && error.name === "AbortError") {
      throw error;
    }
    if (error instanceof AppKitError) {
      throw error;
    }
    const sdkErrorCode =
      error && typeof error === "object" && "errorCode" in error
        ? (error as { errorCode?: unknown }).errorCode
        : undefined;
    throw ExecutionError.statementFailed(
      error instanceof Error ? error.message : String(error),
      typeof sdkErrorCode === "string" ? sdkErrorCode : undefined,
    );
  }

We could quickly add an extra check here for now, but probably a good follow-up is to have different heuristics for Analytics to surface errors.

…cycle

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas
MarioCadenas merged commit 561c948 into main Oct 6, 2026
12 checks passed
@MarioCadenas
MarioCadenas deleted the execution-identity-lifecycle 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