diff --git a/docs/docs/api/appkit/TypeAlias.JobHandle.md b/docs/docs/api/appkit/TypeAlias.JobHandle.md deleted file mode 100644 index f20a11b95..000000000 --- a/docs/docs/api/appkit/TypeAlias.JobHandle.md +++ /dev/null @@ -1,28 +0,0 @@ -# Type Alias: JobHandle - -```ts -type JobHandle = JobAPI & { - asUser: (req: IAppRequest) => JobAPI; -}; -``` - -Job handle returned by `appkit.jobs("etl")`. -Supports OBO access via `.asUser(req)`. - -## Type Declaration - -### asUser() - -```ts -asUser: (req: IAppRequest) => JobAPI; -``` - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `req` | `IAppRequest` | - -#### Returns - -[`JobAPI`](Interface.JobAPI.md) diff --git a/docs/docs/api/appkit/TypeAlias.JobsExport.md b/docs/docs/api/appkit/TypeAlias.JobsExport.md index 3191346bd..53d2fbeff 100644 --- a/docs/docs/api/appkit/TypeAlias.JobsExport.md +++ b/docs/docs/api/appkit/TypeAlias.JobsExport.md @@ -1,7 +1,7 @@ # Type Alias: JobsExport() ```ts -type JobsExport = (jobKey: string) => JobHandle; +type JobsExport = (jobKey: string) => JobAPI; ``` Public API shape of the jobs plugin. @@ -15,7 +15,7 @@ Callable to select a job by key. ## Returns -[`JobHandle`](TypeAlias.JobHandle.md) +[`JobAPI`](Interface.JobAPI.md) ## Example @@ -27,7 +27,4 @@ const { run_id } = await appkit.jobs("etl").runNow(); for await (const status of appkit.jobs("etl").runAndWait()) { console.log(status.status, status.run); } - -// OBO access -await appkit.jobs("etl").asUser(req).runNow(); ``` diff --git a/docs/docs/api/appkit/index.md b/docs/docs/api/appkit/index.md index b7d394176..f39a52db2 100644 --- a/docs/docs/api/appkit/index.md +++ b/docs/docs/api/appkit/index.md @@ -111,7 +111,6 @@ surface with `@databricks/appkit/beta`. Not meant for application imports. | [FilePolicy](TypeAlias.FilePolicy.md) | A policy function that decides whether `user` may perform `action` on `resource`. Return `true` to allow, `false` to deny. | | [HostedTool](TypeAlias.HostedTool.md) | - | | [IAppRouter](TypeAlias.IAppRouter.md) | Express router type for plugin route registration | -| [JobHandle](TypeAlias.JobHandle.md) | Job handle returned by `appkit.jobs("etl")`. Supports OBO access via `.asUser(req)`. | | [JobsExport](TypeAlias.JobsExport.md) | Public API shape of the jobs plugin. Callable to select a job by key. | | [PluginData](TypeAlias.PluginData.md) | Tuple of plugin class, config, and name. Created by `toPlugin()` and passed to `createApp()`. | | [Plugins](TypeAlias.Plugins.md) | Plugin map passed to the function form of [AgentDefinition.tools](Interface.AgentDefinition.md#tools). Each entry exposes a `.toolkit(opts?)` method that returns a record of [ToolkitEntry](Interface.ToolkitEntry.md) markers ready to be spread into a tool record. | diff --git a/docs/docs/api/appkit/typedoc-sidebar.ts b/docs/docs/api/appkit/typedoc-sidebar.ts index 489424a2e..18a5333b1 100644 --- a/docs/docs/api/appkit/typedoc-sidebar.ts +++ b/docs/docs/api/appkit/typedoc-sidebar.ts @@ -468,11 +468,6 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/TypeAlias.IAppRouter", label: "IAppRouter" }, - { - type: "doc", - id: "api/appkit/TypeAlias.JobHandle", - label: "JobHandle" - }, { type: "doc", id: "api/appkit/TypeAlias.JobsExport", diff --git a/docs/docs/plugins/jobs.md b/docs/docs/plugins/jobs.md index 5f6cc33cd..d736dcfb7 100644 --- a/docs/docs/plugins/jobs.md +++ b/docs/docs/plugins/jobs.md @@ -12,7 +12,6 @@ Trigger and monitor [Databricks Lakeflow Jobs](https://docs.databricks.com/en/jo - Run-and-wait with SSE streaming status updates - Parameter validation with Zod schemas - Task-type-aware parameter mapping (notebook, python_wheel, sql, etc.) -- Optional on-behalf-of (OBO) user execution via `.asUser(req)` ## Basic usage @@ -123,18 +122,7 @@ When `taskType` is omitted, parameters are passed through to the SDK as-is. ## Execution context -HTTP routes run as the **app's service principal** by default. Jobs are typically shared infrastructure, and the app's resource binding (`databricks.yml`) grants `CAN_MANAGE_RUN` to the SP — so users trigger runs without needing individual grants. - -Per-run attribution in the Jobs UI will show the app's SP, not the human user. If you need user-level attribution (or want the Databricks permission check to use the user's grants), opt in to OBO explicitly in a custom handler via `.asUser(req)`: - -```ts -// Default: runs as the app's service principal -const result = await AppKit.jobs("etl").runNow({ startDate: "2025-01-01" }); - -// Opt-in: runs as the logged-in user (requires `jobs.jobs` in -// `databricks.yml` user_api_scopes AND the user's own CAN_MANAGE_RUN grant) -const result = await AppKit.jobs("etl").asUser(req).runNow({ startDate: "2025-01-01" }); -``` +Jobs always run as the **app's service principal**. The app's resource binding (`databricks.yml`) grants `CAN_MANAGE_RUN` to the SP, so users trigger runs without needing individual grants. Per-run attribution in the Jobs UI shows the app's SP, not the human user. ## HTTP endpoints diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts index b798256f9..eac0b27b9 100644 --- a/packages/appkit/src/index.ts +++ b/packages/appkit/src/index.ts @@ -74,7 +74,6 @@ export type { IJobsConfig, JobAPI, JobConfig, - JobHandle, JobsExport, } from "./plugins/jobs"; export type { diff --git a/packages/appkit/src/plugins/jobs/index.ts b/packages/appkit/src/plugins/jobs/index.ts index 9567342ca..b391baac7 100644 --- a/packages/appkit/src/plugins/jobs/index.ts +++ b/packages/appkit/src/plugins/jobs/index.ts @@ -1,8 +1,2 @@ export { jobs } from "./plugin"; -export type { - IJobsConfig, - JobAPI, - JobConfig, - JobHandle, - JobsExport, -} from "./types"; +export type { IJobsConfig, JobAPI, JobConfig, JobsExport } from "./types"; diff --git a/packages/appkit/src/plugins/jobs/plugin.ts b/packages/appkit/src/plugins/jobs/plugin.ts index 14ca754a9..24585dd5a 100644 --- a/packages/appkit/src/plugins/jobs/plugin.ts +++ b/packages/appkit/src/plugins/jobs/plugin.ts @@ -1,7 +1,6 @@ import { STATUS_CODES } from "node:http"; import type express from "express"; import type { - IAppRequest, IAppRouter, PluginExecutionSettings, StreamExecutionSettings, @@ -27,7 +26,6 @@ import type { IJobsConfig, JobAPI, JobConfig, - JobHandle, JobRunStatus, JobsExport, } from "./types"; @@ -256,10 +254,7 @@ class JobsPlugin extends Plugin { const jobConfig = this.jobConfigs[jobKey]; // Capture `this` for use in the async generator const self = this; - // Eagerly capture the client and userId so that when createJobAPI is - // called inside an asUser() proxy (which runs in user context), the - // closures below use the user-scoped client instead of falling back - // to the service principal when the ALS context has already exited. + // Capture client and userId eagerly: the closures below run later, after the ALS context may have exited. const client = this.client; const userKey = getCurrentUserId(); @@ -728,22 +723,14 @@ class JobsPlugin extends Plugin { } exports(): JobsExport { - const resolveJob = (jobKey: string): JobHandle => { + const resolveJob = (jobKey: string): JobAPI => { if (!this.jobKeys.includes(jobKey)) { throw new Error( `Unknown job "${jobKey}". Available jobs: ${this.jobKeys.join(", ")}`, ); } - const spApi = this.createJobAPI(jobKey); - - return { - ...spApi, - asUser: (req: IAppRequest) => { - const userPlugin = this.asUser(req) as JobsPlugin; - return userPlugin.createJobAPI(jobKey); - }, - }; + return this.createJobAPI(jobKey); }; return resolveJob as JobsExport; diff --git a/packages/appkit/src/plugins/jobs/tests/plugin.test.ts b/packages/appkit/src/plugins/jobs/tests/plugin.test.ts index 9c27960f2..532cb7024 100644 --- a/packages/appkit/src/plugins/jobs/tests/plugin.test.ts +++ b/packages/appkit/src/plugins/jobs/tests/plugin.test.ts @@ -2,7 +2,6 @@ import { mockServiceContext, setupDatabricksEnv } from "@tools/test-helpers"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { z } from "zod"; import { ServiceContext } from "../../../context/service-context"; -import { AuthenticationError } from "../../../errors"; import { ResourceType } from "../../../registry"; import { JOBS_READ_DEFAULTS, @@ -248,14 +247,13 @@ describe("JobsPlugin", () => { expect(typeof exported).toBe("function"); }); - test("returns job handle with asUser and direct JobAPI methods", () => { + test("returns job handle with direct JobAPI methods", () => { process.env.DATABRICKS_JOB_ETL = "123"; const plugin = new JobsPlugin({}); const exported = plugin.exports(); const handle = exported("etl"); - expect(typeof handle.asUser).toBe("function"); expect(typeof handle.runNow).toBe("function"); expect(typeof handle.runAndWait).toBe("function"); expect(typeof handle.lastRun).toBe("function"); @@ -741,76 +739,6 @@ describe("JobsPlugin", () => { }); }); - describe("OBO and service principal access", () => { - test("job handle exposes asUser and all JobAPI methods", () => { - process.env.DATABRICKS_JOB_ETL = "123"; - - const plugin = new JobsPlugin({}); - const handle = plugin.exports()("etl"); - - expect(typeof handle.asUser).toBe("function"); - - const jobMethods = [ - "runNow", - "runAndWait", - "lastRun", - "listRuns", - "getRun", - "getRunOutput", - "cancelRun", - "getJob", - ]; - for (const method of jobMethods) { - expect(typeof (handle as any)[method]).toBe("function"); - } - }); - - test("asUser throws AuthenticationError without token in production", () => { - const originalEnv = process.env.NODE_ENV; - process.env.NODE_ENV = "production"; - process.env.DATABRICKS_JOB_ETL = "123"; - - try { - const plugin = new JobsPlugin({}); - const handle = plugin.exports()("etl"); - const mockReq = { header: () => undefined } as any; - - expect(() => handle.asUser(mockReq)).toThrow(AuthenticationError); - } finally { - process.env.NODE_ENV = originalEnv; - } - }); - - test("asUser in dev mode returns JobAPI with all methods", () => { - const originalEnv = process.env.NODE_ENV; - process.env.NODE_ENV = "development"; - process.env.DATABRICKS_JOB_ETL = "123"; - - try { - const plugin = new JobsPlugin({}); - const handle = plugin.exports()("etl"); - const mockReq = { header: () => undefined } as any; - const api = handle.asUser(mockReq); - - const jobMethods = [ - "runNow", - "runAndWait", - "lastRun", - "listRuns", - "getRun", - "getRunOutput", - "cancelRun", - "getJob", - ]; - for (const method of jobMethods) { - expect(typeof (api as any)[method]).toBe("function"); - } - } finally { - process.env.NODE_ENV = originalEnv; - } - }); - }); - describe("clientConfig", () => { test("returns configured job keys with params schema", () => { process.env.DATABRICKS_JOB_ETL = "123"; @@ -1990,57 +1918,4 @@ describe("injectRoutes", () => { expect(mockRes.end).not.toHaveBeenCalled(); }); }); - - describe("execution context", () => { - test("HTTP routes run as service principal by default (no asUser)", async () => { - process.env.DATABRICKS_JOB_ETL = "123"; - - mockClient.jobs.listRuns.mockReturnValue((async function* () {})()); - - const plugin = new JobsPlugin({}); - const asUserSpy = vi.spyOn(plugin, "asUser"); - const routeSpy = vi.spyOn(plugin as any, "route"); - - const mockRouter = { get: vi.fn(), post: vi.fn(), delete: vi.fn() }; - plugin.injectRoutes(mockRouter as any); - - const runsRoute = routeSpy.mock.calls.find( - (call) => (call[1] as any).name === "runs", - ); - const handler = (runsRoute?.[1] as any).handler; - - const mockReq = { - params: { jobKey: "etl" }, - query: {}, - header: vi.fn().mockReturnValue("test-token"), - } as any; - - const mockRes = { - status: vi.fn().mockReturnThis(), - json: vi.fn(), - } as any; - - await handler(mockReq, mockRes); - - // Route handlers should not implicitly call asUser — callers opt into - // OBO via the programmatic `exports().asUser(req)` surface. - expect(asUserSpy).not.toHaveBeenCalled(); - }); - - test("programmatic exports().asUser(req) still delegates through asUser", () => { - process.env.DATABRICKS_JOB_ETL = "123"; - - const plugin = new JobsPlugin({}); - const asUserSpy = vi.spyOn(plugin, "asUser"); - const handle = plugin.exports()("etl"); - - const mockReq = { - header: vi.fn().mockReturnValue("test-token"), - } as any; - - handle.asUser(mockReq); - - expect(asUserSpy).toHaveBeenCalledWith(mockReq); - }); - }); }); diff --git a/packages/appkit/src/plugins/jobs/types.ts b/packages/appkit/src/plugins/jobs/types.ts index d10d5684b..0ad101df6 100644 --- a/packages/appkit/src/plugins/jobs/types.ts +++ b/packages/appkit/src/plugins/jobs/types.ts @@ -1,4 +1,4 @@ -import type { BasePluginConfig, IAppRequest } from "shared"; +import type { BasePluginConfig } from "shared"; import type { z } from "zod"; import type { ExecutionResult } from "../../plugin"; import type { jobs } from "../../workspace-client"; @@ -66,14 +66,6 @@ export interface IJobsConfig extends BasePluginConfig { jobs?: Record; } -/** - * Job handle returned by `appkit.jobs("etl")`. - * Supports OBO access via `.asUser(req)`. - */ -export type JobHandle = JobAPI & { - asUser: (req: IAppRequest) => JobAPI; -}; - /** * Public API shape of the jobs plugin. * Callable to select a job by key. @@ -87,9 +79,6 @@ export type JobHandle = JobAPI & { * for await (const status of appkit.jobs("etl").runAndWait()) { * console.log(status.status, status.run); * } - * - * // OBO access - * await appkit.jobs("etl").asUser(req).runNow(); * ``` */ -export type JobsExport = (jobKey: string) => JobHandle; +export type JobsExport = (jobKey: string) => JobAPI;