@databricks/appkit 0.35.0 → 0.35.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/appkit/package.js +1 -1
- package/dist/connectors/files/client.js +28 -1
- package/dist/connectors/files/client.js.map +1 -1
- package/dist/connectors/files/index.js +1 -1
- package/dist/connectors/index.js +1 -1
- package/dist/plugins/files/plugin.d.ts +216 -20
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js +618 -191
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/files/policy.d.ts +30 -1
- package/dist/plugins/files/policy.d.ts.map +1 -1
- package/dist/plugins/files/policy.js.map +1 -1
- package/dist/plugins/files/types.d.ts +136 -5
- package/dist/plugins/files/types.d.ts.map +1 -1
- package/docs/api/appkit/Interface.FilePolicyUser.md +15 -1
- package/docs/development/llm-guide.md +0 -1
- package/docs/plugins/files.md +199 -19
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
package/README.md
CHANGED
|
@@ -27,13 +27,13 @@ AppKit's power comes from its plugin system. Each plugin adds a focused capabili
|
|
|
27
27
|
|
|
28
28
|
## Getting started
|
|
29
29
|
|
|
30
|
-
Follow the [Getting Started](https://databricks.
|
|
30
|
+
Follow the [Getting Started](https://www.databricks.com/devhub/docs/appkit/v0/) guide to get started with AppKit.
|
|
31
31
|
|
|
32
|
-
🤖 For AI/code assistants, see the [AI-assisted development](https://databricks.
|
|
32
|
+
🤖 For AI/code assistants, see the [AI-assisted development](https://www.databricks.com/devhub/docs/appkit/v0/development/ai-assisted-development) guide.
|
|
33
33
|
|
|
34
34
|
## Documentation
|
|
35
35
|
|
|
36
|
-
📖 For full AppKit documentation, visit the [AppKit Documentation](https://databricks.
|
|
36
|
+
📖 For full AppKit documentation, visit the [AppKit Documentation](https://www.databricks.com/devhub/docs/appkit/v0/) website.
|
|
37
37
|
|
|
38
38
|
## Contributing
|
|
39
39
|
|
package/dist/appkit/package.js
CHANGED
|
@@ -3,9 +3,34 @@ import { TelemetryManager } from "../../telemetry/telemetry-manager.js";
|
|
|
3
3
|
import { SpanKind, SpanStatusCode } from "../../telemetry/index.js";
|
|
4
4
|
import { FILES_MAX_READ_SIZE, contentTypeFromPath, isTextContentType } from "./defaults.js";
|
|
5
5
|
import { ApiError } from "@databricks/sdk-experimental";
|
|
6
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
7
|
|
|
7
8
|
//#region src/connectors/files/client.ts
|
|
8
9
|
const logger = createLogger("connectors:files");
|
|
10
|
+
/**
|
|
11
|
+
* Ambient span-attribute propagation for `FilesConnector.traced()`.
|
|
12
|
+
*
|
|
13
|
+
* Callers (e.g. the plugin's `_withAuthModeAttributes` wrapper) set extra
|
|
14
|
+
* span attributes here via `runWithFilesSpanAttributes(attrs, fn)`. The
|
|
15
|
+
* connector's `traced()` decorator reads them and merges them into the
|
|
16
|
+
* span it creates around the SDK call. This lets the plugin tag spans with
|
|
17
|
+
* `files.auth_mode` without opening a duplicate `files.<op>` span.
|
|
18
|
+
*
|
|
19
|
+
* AsyncLocalStorage is used so concurrent requests don't see each other's
|
|
20
|
+
* attributes. Outside an active scope, `getStore()` returns `undefined` and
|
|
21
|
+
* the connector falls back to the static attribute set.
|
|
22
|
+
*/
|
|
23
|
+
const filesSpanAttributesStorage = new AsyncLocalStorage();
|
|
24
|
+
/**
|
|
25
|
+
* Run `fn` with the supplied attributes attached to whatever span the
|
|
26
|
+
* `FilesConnector` opens for its SDK call. Used to propagate request-scoped
|
|
27
|
+
* attributes (e.g. `files.auth_mode`) onto the connector's span without
|
|
28
|
+
* opening a parent span — avoids the 2x span allocation that
|
|
29
|
+
* `startActiveSpan` parented otherwise.
|
|
30
|
+
*/
|
|
31
|
+
function runWithFilesSpanAttributes(attributes, fn) {
|
|
32
|
+
return filesSpanAttributesStorage.run(attributes, fn);
|
|
33
|
+
}
|
|
9
34
|
var FilesConnector = class {
|
|
10
35
|
name = "files";
|
|
11
36
|
defaultVolume;
|
|
@@ -41,10 +66,12 @@ var FilesConnector = class {
|
|
|
41
66
|
async traced(operation, attributes, fn) {
|
|
42
67
|
const startTime = Date.now();
|
|
43
68
|
let success = false;
|
|
69
|
+
const ambient = filesSpanAttributesStorage.getStore();
|
|
44
70
|
return this.telemetry.startActiveSpan(`files.${operation}`, {
|
|
45
71
|
kind: SpanKind.CLIENT,
|
|
46
72
|
attributes: {
|
|
47
73
|
"files.operation": operation,
|
|
74
|
+
...ambient ?? {},
|
|
48
75
|
...attributes
|
|
49
76
|
}
|
|
50
77
|
}, async (span) => {
|
|
@@ -219,5 +246,5 @@ var FilesConnector = class {
|
|
|
219
246
|
};
|
|
220
247
|
|
|
221
248
|
//#endregion
|
|
222
|
-
export { FilesConnector };
|
|
249
|
+
export { FilesConnector, runWithFilesSpanAttributes };
|
|
223
250
|
//# sourceMappingURL=client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/files/client.ts"],"sourcesContent":["import { ApiError, type WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { TelemetryOptions } from \"shared\";\nimport { createLogger } from \"../../logging/logger\";\nimport type {\n DirectoryEntry,\n DownloadResponse,\n FileMetadata,\n FilePreview,\n} from \"../../plugins/files/types\";\nimport type { TelemetryProvider } from \"../../telemetry\";\nimport {\n type Counter,\n type Histogram,\n type Span,\n SpanKind,\n SpanStatusCode,\n TelemetryManager,\n} from \"../../telemetry\";\nimport {\n contentTypeFromPath,\n FILES_MAX_READ_SIZE,\n isTextContentType,\n} from \"./defaults\";\n\nconst logger = createLogger(\"connectors:files\");\n\ninterface FilesConnectorConfig {\n defaultVolume?: string;\n timeout?: number;\n telemetry?: TelemetryOptions;\n customContentTypes?: Record<string, string>;\n}\n\nexport class FilesConnector {\n private readonly name = \"files\";\n private defaultVolume: string | undefined;\n private readonly customContentTypes: Record<string, string> | undefined;\n\n private readonly telemetry: TelemetryProvider;\n private readonly telemetryMetrics: {\n operationCount: Counter;\n operationDuration: Histogram;\n };\n\n constructor(config: FilesConnectorConfig) {\n this.defaultVolume = config.defaultVolume;\n this.customContentTypes = config.customContentTypes;\n\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.telemetryMetrics = {\n operationCount: this.telemetry\n .getMeter()\n .createCounter(\"files.operation.count\", {\n description: \"Total number of file operations\",\n unit: \"1\",\n }),\n operationDuration: this.telemetry\n .getMeter()\n .createHistogram(\"files.operation.duration\", {\n description: \"Duration of file operations\",\n unit: \"ms\",\n }),\n };\n }\n\n resolvePath(filePath: string): string {\n if (filePath.length > 4096) {\n throw new Error(\n `Path exceeds maximum length of 4096 characters (got ${filePath.length}).`,\n );\n }\n if (filePath.includes(\"\\0\")) {\n throw new Error(\"Path must not contain null bytes.\");\n }\n\n const segments = filePath.split(\"/\");\n if (segments.some((s) => s === \"..\")) {\n throw new Error('Path traversal (\"../\") is not allowed.');\n }\n if (filePath.startsWith(\"/\")) {\n if (!filePath.startsWith(\"/Volumes/\")) {\n throw new Error(\n 'Absolute paths must start with \"/Volumes/\". ' +\n \"Unity Catalog volume paths follow the format: /Volumes/<catalog>/<schema>/<volume>/\",\n );\n }\n return filePath;\n }\n if (!this.defaultVolume) {\n throw new Error(\n \"Cannot resolve relative path: no default volume set. Use an absolute path or set a default volume.\",\n );\n }\n return `${this.defaultVolume}/${filePath}`;\n }\n\n private async traced<T>(\n operation: string,\n attributes: Record<string, string>,\n fn: (span: Span) => Promise<T>,\n ): Promise<T> {\n const startTime = Date.now();\n let success = false;\n\n return this.telemetry.startActiveSpan(\n `files.${operation}`,\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"files.operation\": operation,\n ...attributes,\n },\n },\n async (span: Span) => {\n try {\n const result = await fn(span);\n success = true;\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n throw error;\n } finally {\n span.end();\n const duration = Date.now() - startTime;\n const metricAttrs = {\n \"files.operation\": operation,\n success: String(success),\n };\n this.telemetryMetrics.operationCount.add(1, metricAttrs);\n this.telemetryMetrics.operationDuration.record(duration, metricAttrs);\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n async list(\n client: WorkspaceClient,\n directoryPath?: string,\n ): Promise<DirectoryEntry[]> {\n const resolvedPath = directoryPath\n ? this.resolvePath(directoryPath)\n : this.defaultVolume;\n if (!resolvedPath) {\n throw new Error(\"No directory path provided and no default volume set.\");\n }\n\n return this.traced(\"list\", { \"files.path\": resolvedPath }, async () => {\n const entries: DirectoryEntry[] = [];\n for await (const entry of client.files.listDirectoryContents({\n directory_path: resolvedPath,\n })) {\n entries.push(entry);\n }\n return entries;\n });\n }\n\n async read(\n client: WorkspaceClient,\n filePath: string,\n options?: { maxSize?: number },\n ): Promise<string> {\n const resolvedPath = this.resolvePath(filePath);\n const maxSize = options?.maxSize ?? FILES_MAX_READ_SIZE;\n return this.traced(\"read\", { \"files.path\": resolvedPath }, async () => {\n const response = await this.download(client, filePath);\n if (!response.contents) {\n return \"\";\n }\n const reader = response.contents.getReader();\n const decoder = new TextDecoder();\n let result = \"\";\n let bytesRead = 0;\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesRead += value.byteLength;\n if (bytesRead > maxSize) {\n await reader.cancel();\n throw new Error(\n `File exceeds maximum read size (${maxSize} bytes). Use download() for large files.`,\n );\n }\n result += decoder.decode(value, { stream: true });\n }\n result += decoder.decode();\n return result;\n });\n }\n\n async download(\n client: WorkspaceClient,\n filePath: string,\n ): Promise<DownloadResponse> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"download\", { \"files.path\": resolvedPath }, async () => {\n return client.files.download({\n file_path: resolvedPath,\n });\n });\n }\n\n async exists(client: WorkspaceClient, filePath: string): Promise<boolean> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"exists\", { \"files.path\": resolvedPath }, async () => {\n try {\n await this.metadata(client, filePath);\n return true;\n } catch (error) {\n if (error instanceof ApiError && error.statusCode === 404) {\n return false;\n }\n throw error;\n }\n });\n }\n\n async metadata(\n client: WorkspaceClient,\n filePath: string,\n ): Promise<FileMetadata> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"metadata\", { \"files.path\": resolvedPath }, async () => {\n const response = await client.files.getMetadata({\n file_path: resolvedPath,\n });\n return {\n contentLength: response[\"content-length\"],\n contentType: contentTypeFromPath(\n filePath,\n response[\"content-type\"],\n this.customContentTypes,\n ),\n lastModified: response[\"last-modified\"],\n };\n });\n }\n\n async upload(\n client: WorkspaceClient,\n filePath: string,\n contents: ReadableStream | Buffer | string,\n options?: { overwrite?: boolean },\n ): Promise<void> {\n const resolvedPath = this.resolvePath(filePath);\n\n return this.traced(\"upload\", { \"files.path\": resolvedPath }, async () => {\n const body = contents;\n const overwrite = options?.overwrite ?? true;\n\n // Workaround: The SDK's files.upload() has two bugs:\n // 1. It ignores the `contents` field (sets body to undefined)\n // 2. apiClient.request() checks `instanceof` against its own ReadableStream\n // subclass, so standard ReadableStream instances get JSON.stringified to \"{}\"\n // Bypass both by calling the REST API directly with SDK-provided auth.\n const hostValue = client.config.host;\n if (!hostValue) {\n throw new Error(\n \"Databricks host is not configured. Set DATABRICKS_HOST or configure client.config.host.\",\n );\n }\n const host = hostValue.startsWith(\"http\")\n ? hostValue\n : `https://${hostValue}`;\n const url = new URL(`/api/2.0/fs/files${resolvedPath}`, host);\n url.searchParams.set(\"overwrite\", String(overwrite));\n\n const headers = new Headers({\n \"Content-Type\": \"application/octet-stream\",\n });\n const fetchOptions: RequestInit = { method: \"PUT\", headers, body };\n\n if (body instanceof ReadableStream) {\n fetchOptions.duplex = \"half\";\n } else if (body instanceof Buffer) {\n headers.set(\"Content-Length\", String(body.length));\n } else if (typeof body === \"string\") {\n headers.set(\"Content-Length\", String(Buffer.byteLength(body)));\n }\n\n await client.config.authenticate(headers);\n\n const res = await fetch(url.toString(), fetchOptions);\n\n if (!res.ok) {\n const text = await res.text();\n logger.error(`Upload failed (${res.status}): ${text}`);\n const safeMessage = text.length > 200 ? `${text.slice(0, 200)}…` : text;\n throw new ApiError(\n `Upload failed: ${safeMessage}`,\n \"UPLOAD_FAILED\",\n res.status,\n undefined,\n [],\n );\n }\n });\n }\n\n async createDirectory(\n client: WorkspaceClient,\n directoryPath: string,\n ): Promise<void> {\n const resolvedPath = this.resolvePath(directoryPath);\n return this.traced(\n \"createDirectory\",\n { \"files.path\": resolvedPath },\n async () => {\n await client.files.createDirectory({\n directory_path: resolvedPath,\n });\n },\n );\n }\n\n async delete(client: WorkspaceClient, filePath: string): Promise<void> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"delete\", { \"files.path\": resolvedPath }, async () => {\n await client.files.delete({\n file_path: resolvedPath,\n });\n });\n }\n\n async preview(\n client: WorkspaceClient,\n filePath: string,\n options?: { maxChars?: number },\n ): Promise<FilePreview> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"preview\", { \"files.path\": resolvedPath }, async () => {\n const meta = await this.metadata(client, filePath);\n const isText = isTextContentType(meta.contentType);\n const isImage = meta.contentType?.startsWith(\"image/\") || false;\n\n if (!isText) {\n return { ...meta, textPreview: null, isText: false, isImage };\n }\n\n const response = await client.files.download({\n file_path: resolvedPath,\n });\n if (!response.contents) {\n return { ...meta, textPreview: \"\", isText: true, isImage: false };\n }\n\n const reader = response.contents.getReader();\n const decoder = new TextDecoder();\n let preview = \"\";\n const maxChars = options?.maxChars ?? 1024;\n\n while (preview.length < maxChars) {\n const { done, value } = await reader.read();\n if (done) break;\n preview += decoder.decode(value, { stream: true });\n }\n preview += decoder.decode();\n await reader.cancel();\n\n if (preview.length > maxChars) {\n preview = preview.slice(0, maxChars);\n }\n\n return { ...meta, textPreview: preview, isText: true, isImage: false };\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/files/client.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { ApiError, type WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { TelemetryOptions } from \"shared\";\nimport { createLogger } from \"../../logging/logger\";\nimport type {\n DirectoryEntry,\n DownloadResponse,\n FileMetadata,\n FilePreview,\n} from \"../../plugins/files/types\";\nimport type { TelemetryProvider } from \"../../telemetry\";\nimport {\n type Counter,\n type Histogram,\n type Span,\n SpanKind,\n SpanStatusCode,\n TelemetryManager,\n} from \"../../telemetry\";\nimport {\n contentTypeFromPath,\n FILES_MAX_READ_SIZE,\n isTextContentType,\n} from \"./defaults\";\n\nconst logger = createLogger(\"connectors:files\");\n\n/**\n * Ambient span-attribute propagation for `FilesConnector.traced()`.\n *\n * Callers (e.g. the plugin's `_withAuthModeAttributes` wrapper) set extra\n * span attributes here via `runWithFilesSpanAttributes(attrs, fn)`. The\n * connector's `traced()` decorator reads them and merges them into the\n * span it creates around the SDK call. This lets the plugin tag spans with\n * `files.auth_mode` without opening a duplicate `files.<op>` span.\n *\n * AsyncLocalStorage is used so concurrent requests don't see each other's\n * attributes. Outside an active scope, `getStore()` returns `undefined` and\n * the connector falls back to the static attribute set.\n */\nconst filesSpanAttributesStorage = new AsyncLocalStorage<\n Record<string, string>\n>();\n\n/**\n * Run `fn` with the supplied attributes attached to whatever span the\n * `FilesConnector` opens for its SDK call. Used to propagate request-scoped\n * attributes (e.g. `files.auth_mode`) onto the connector's span without\n * opening a parent span — avoids the 2x span allocation that\n * `startActiveSpan` parented otherwise.\n */\nexport function runWithFilesSpanAttributes<T>(\n attributes: Record<string, string>,\n fn: () => Promise<T>,\n): Promise<T> {\n return filesSpanAttributesStorage.run(attributes, fn);\n}\n\ninterface FilesConnectorConfig {\n defaultVolume?: string;\n timeout?: number;\n telemetry?: TelemetryOptions;\n customContentTypes?: Record<string, string>;\n}\n\nexport class FilesConnector {\n private readonly name = \"files\";\n private defaultVolume: string | undefined;\n private readonly customContentTypes: Record<string, string> | undefined;\n\n private readonly telemetry: TelemetryProvider;\n private readonly telemetryMetrics: {\n operationCount: Counter;\n operationDuration: Histogram;\n };\n\n constructor(config: FilesConnectorConfig) {\n this.defaultVolume = config.defaultVolume;\n this.customContentTypes = config.customContentTypes;\n\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.telemetryMetrics = {\n operationCount: this.telemetry\n .getMeter()\n .createCounter(\"files.operation.count\", {\n description: \"Total number of file operations\",\n unit: \"1\",\n }),\n operationDuration: this.telemetry\n .getMeter()\n .createHistogram(\"files.operation.duration\", {\n description: \"Duration of file operations\",\n unit: \"ms\",\n }),\n };\n }\n\n resolvePath(filePath: string): string {\n if (filePath.length > 4096) {\n throw new Error(\n `Path exceeds maximum length of 4096 characters (got ${filePath.length}).`,\n );\n }\n if (filePath.includes(\"\\0\")) {\n throw new Error(\"Path must not contain null bytes.\");\n }\n\n const segments = filePath.split(\"/\");\n if (segments.some((s) => s === \"..\")) {\n throw new Error('Path traversal (\"../\") is not allowed.');\n }\n if (filePath.startsWith(\"/\")) {\n if (!filePath.startsWith(\"/Volumes/\")) {\n throw new Error(\n 'Absolute paths must start with \"/Volumes/\". ' +\n \"Unity Catalog volume paths follow the format: /Volumes/<catalog>/<schema>/<volume>/\",\n );\n }\n return filePath;\n }\n if (!this.defaultVolume) {\n throw new Error(\n \"Cannot resolve relative path: no default volume set. Use an absolute path or set a default volume.\",\n );\n }\n return `${this.defaultVolume}/${filePath}`;\n }\n\n private async traced<T>(\n operation: string,\n attributes: Record<string, string>,\n fn: (span: Span) => Promise<T>,\n ): Promise<T> {\n const startTime = Date.now();\n let success = false;\n\n // Pull any ambient attributes set by `runWithFilesSpanAttributes` (e.g.\n // `files.auth_mode` from the plugin layer). Static `attributes` win on\n // collision so callers can override per-call.\n const ambient = filesSpanAttributesStorage.getStore();\n\n return this.telemetry.startActiveSpan(\n `files.${operation}`,\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"files.operation\": operation,\n ...(ambient ?? {}),\n ...attributes,\n },\n },\n async (span: Span) => {\n try {\n const result = await fn(span);\n success = true;\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n throw error;\n } finally {\n span.end();\n const duration = Date.now() - startTime;\n const metricAttrs = {\n \"files.operation\": operation,\n success: String(success),\n };\n this.telemetryMetrics.operationCount.add(1, metricAttrs);\n this.telemetryMetrics.operationDuration.record(duration, metricAttrs);\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n async list(\n client: WorkspaceClient,\n directoryPath?: string,\n ): Promise<DirectoryEntry[]> {\n const resolvedPath = directoryPath\n ? this.resolvePath(directoryPath)\n : this.defaultVolume;\n if (!resolvedPath) {\n throw new Error(\"No directory path provided and no default volume set.\");\n }\n\n return this.traced(\"list\", { \"files.path\": resolvedPath }, async () => {\n const entries: DirectoryEntry[] = [];\n for await (const entry of client.files.listDirectoryContents({\n directory_path: resolvedPath,\n })) {\n entries.push(entry);\n }\n return entries;\n });\n }\n\n async read(\n client: WorkspaceClient,\n filePath: string,\n options?: { maxSize?: number },\n ): Promise<string> {\n const resolvedPath = this.resolvePath(filePath);\n const maxSize = options?.maxSize ?? FILES_MAX_READ_SIZE;\n return this.traced(\"read\", { \"files.path\": resolvedPath }, async () => {\n const response = await this.download(client, filePath);\n if (!response.contents) {\n return \"\";\n }\n const reader = response.contents.getReader();\n const decoder = new TextDecoder();\n let result = \"\";\n let bytesRead = 0;\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesRead += value.byteLength;\n if (bytesRead > maxSize) {\n await reader.cancel();\n throw new Error(\n `File exceeds maximum read size (${maxSize} bytes). Use download() for large files.`,\n );\n }\n result += decoder.decode(value, { stream: true });\n }\n result += decoder.decode();\n return result;\n });\n }\n\n async download(\n client: WorkspaceClient,\n filePath: string,\n ): Promise<DownloadResponse> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"download\", { \"files.path\": resolvedPath }, async () => {\n return client.files.download({\n file_path: resolvedPath,\n });\n });\n }\n\n async exists(client: WorkspaceClient, filePath: string): Promise<boolean> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"exists\", { \"files.path\": resolvedPath }, async () => {\n try {\n await this.metadata(client, filePath);\n return true;\n } catch (error) {\n if (error instanceof ApiError && error.statusCode === 404) {\n return false;\n }\n throw error;\n }\n });\n }\n\n async metadata(\n client: WorkspaceClient,\n filePath: string,\n ): Promise<FileMetadata> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"metadata\", { \"files.path\": resolvedPath }, async () => {\n const response = await client.files.getMetadata({\n file_path: resolvedPath,\n });\n return {\n contentLength: response[\"content-length\"],\n contentType: contentTypeFromPath(\n filePath,\n response[\"content-type\"],\n this.customContentTypes,\n ),\n lastModified: response[\"last-modified\"],\n };\n });\n }\n\n async upload(\n client: WorkspaceClient,\n filePath: string,\n contents: ReadableStream | Buffer | string,\n options?: { overwrite?: boolean },\n ): Promise<void> {\n const resolvedPath = this.resolvePath(filePath);\n\n return this.traced(\"upload\", { \"files.path\": resolvedPath }, async () => {\n const body = contents;\n const overwrite = options?.overwrite ?? true;\n\n // Workaround: The SDK's files.upload() has two bugs:\n // 1. It ignores the `contents` field (sets body to undefined)\n // 2. apiClient.request() checks `instanceof` against its own ReadableStream\n // subclass, so standard ReadableStream instances get JSON.stringified to \"{}\"\n // Bypass both by calling the REST API directly with SDK-provided auth.\n const hostValue = client.config.host;\n if (!hostValue) {\n throw new Error(\n \"Databricks host is not configured. Set DATABRICKS_HOST or configure client.config.host.\",\n );\n }\n const host = hostValue.startsWith(\"http\")\n ? hostValue\n : `https://${hostValue}`;\n const url = new URL(`/api/2.0/fs/files${resolvedPath}`, host);\n url.searchParams.set(\"overwrite\", String(overwrite));\n\n const headers = new Headers({\n \"Content-Type\": \"application/octet-stream\",\n });\n const fetchOptions: RequestInit = { method: \"PUT\", headers, body };\n\n if (body instanceof ReadableStream) {\n fetchOptions.duplex = \"half\";\n } else if (body instanceof Buffer) {\n headers.set(\"Content-Length\", String(body.length));\n } else if (typeof body === \"string\") {\n headers.set(\"Content-Length\", String(Buffer.byteLength(body)));\n }\n\n await client.config.authenticate(headers);\n\n const res = await fetch(url.toString(), fetchOptions);\n\n if (!res.ok) {\n const text = await res.text();\n logger.error(`Upload failed (${res.status}): ${text}`);\n const safeMessage = text.length > 200 ? `${text.slice(0, 200)}…` : text;\n throw new ApiError(\n `Upload failed: ${safeMessage}`,\n \"UPLOAD_FAILED\",\n res.status,\n undefined,\n [],\n );\n }\n });\n }\n\n async createDirectory(\n client: WorkspaceClient,\n directoryPath: string,\n ): Promise<void> {\n const resolvedPath = this.resolvePath(directoryPath);\n return this.traced(\n \"createDirectory\",\n { \"files.path\": resolvedPath },\n async () => {\n await client.files.createDirectory({\n directory_path: resolvedPath,\n });\n },\n );\n }\n\n async delete(client: WorkspaceClient, filePath: string): Promise<void> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"delete\", { \"files.path\": resolvedPath }, async () => {\n await client.files.delete({\n file_path: resolvedPath,\n });\n });\n }\n\n async preview(\n client: WorkspaceClient,\n filePath: string,\n options?: { maxChars?: number },\n ): Promise<FilePreview> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"preview\", { \"files.path\": resolvedPath }, async () => {\n const meta = await this.metadata(client, filePath);\n const isText = isTextContentType(meta.contentType);\n const isImage = meta.contentType?.startsWith(\"image/\") || false;\n\n if (!isText) {\n return { ...meta, textPreview: null, isText: false, isImage };\n }\n\n const response = await client.files.download({\n file_path: resolvedPath,\n });\n if (!response.contents) {\n return { ...meta, textPreview: \"\", isText: true, isImage: false };\n }\n\n const reader = response.contents.getReader();\n const decoder = new TextDecoder();\n let preview = \"\";\n const maxChars = options?.maxChars ?? 1024;\n\n while (preview.length < maxChars) {\n const { done, value } = await reader.read();\n if (done) break;\n preview += decoder.decode(value, { stream: true });\n }\n preview += decoder.decode();\n await reader.cancel();\n\n if (preview.length > maxChars) {\n preview = preview.slice(0, maxChars);\n }\n\n return { ...meta, textPreview: preview, isText: true, isImage: false };\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { FILES_MAX_READ_SIZE, SAFE_INLINE_CONTENT_TYPES, contentTypeFromPath, isSafeInlineContentType, isTextContentType, validateCustomContentTypes } from "./defaults.js";
|
|
2
|
-
import { FilesConnector } from "./client.js";
|
|
2
|
+
import { FilesConnector, runWithFilesSpanAttributes } from "./client.js";
|
|
3
3
|
|
|
4
4
|
export { };
|
package/dist/connectors/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createLakebasePoolManager } from "./lakebase/pool-manager.js";
|
|
|
2
2
|
import { RoutingPool } from "./lakebase/routing-pool.js";
|
|
3
3
|
import { RequestedClaimsPermissionSet, createLakebasePool, generateDatabaseCredential, getLakebaseOrmConfig, getLakebasePgConfig, getUsernameWithApiLookup, getWorkspaceClient } from "./lakebase/index.js";
|
|
4
4
|
import { FILES_MAX_READ_SIZE, SAFE_INLINE_CONTENT_TYPES, contentTypeFromPath, isSafeInlineContentType, isTextContentType, validateCustomContentTypes } from "./files/defaults.js";
|
|
5
|
-
import { FilesConnector } from "./files/client.js";
|
|
5
|
+
import { FilesConnector, runWithFilesSpanAttributes } from "./files/client.js";
|
|
6
6
|
import "./files/index.js";
|
|
7
7
|
import { GenieConnector } from "./genie/client.js";
|
|
8
8
|
import "./genie/index.js";
|
|
@@ -30,13 +30,59 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
30
30
|
/**
|
|
31
31
|
* Generates resource requirements dynamically from discovered + configured volumes.
|
|
32
32
|
* Each volume key maps to a `DATABRICKS_VOLUME_{KEY_UPPERCASE}` env var.
|
|
33
|
+
*
|
|
34
|
+
* ## Per-volume permission scope (SP vs OBO)
|
|
35
|
+
*
|
|
36
|
+
* The returned manifest entries describe a single permission grant per
|
|
37
|
+
* volume, but the *grantee* depends on the volume's `auth` setting at
|
|
38
|
+
* runtime — and that distinction is **not** expressed in the manifest
|
|
39
|
+
* today:
|
|
40
|
+
*
|
|
41
|
+
* - **Service-principal volumes** (the default, `auth: "service-principal"`):
|
|
42
|
+
* the app's service principal needs `WRITE_VOLUME` (or read-equivalent)
|
|
43
|
+
* on the UC volume. This matches the manifest entry as written.
|
|
44
|
+
* - **On-behalf-of-user volumes** (`auth: "on-behalf-of-user"`): SDK calls
|
|
45
|
+
* execute as the **end user**, so the *user* — not the SP — must hold
|
|
46
|
+
* `WRITE_VOLUME` (or read-equivalent) on the UC volume. The SP only
|
|
47
|
+
* needs to be allowed to mint user-token requests; it does not need
|
|
48
|
+
* direct volume permissions.
|
|
49
|
+
*
|
|
50
|
+
* The static manifest cannot currently express this per-volume split, so
|
|
51
|
+
* callers configuring OBO volumes must communicate the per-user permission
|
|
52
|
+
* requirement out-of-band (docs, runbooks, deployment scripts) until the
|
|
53
|
+
* manifest schema gains a per-volume auth scope field.
|
|
33
54
|
*/
|
|
34
55
|
static getResourceRequirements(config: IFilesConfig): ResourceRequirement[];
|
|
35
56
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
57
|
+
* Extraction for `VolumeHandle.asUser(req)`. In production we require BOTH
|
|
58
|
+
* `x-forwarded-user` and `x-forwarded-access-token`, and throw
|
|
59
|
+
* `AuthenticationError.missingToken` if either is missing — otherwise a
|
|
60
|
+
* request with only `x-forwarded-user: alice` would let policies see Alice
|
|
61
|
+
* as a "real user" (`isServicePrincipal: false`) while the SDK call below
|
|
62
|
+
* falls through to the SP client because `_buildUserContextOrNull` returns
|
|
63
|
+
* `null`. Net effect: policy approves the user, SDK runs as SP, privilege
|
|
64
|
+
* confusion (CWE-639/863).
|
|
65
|
+
*
|
|
66
|
+
* In development (`NODE_ENV === "development"`) we keep a local-loop
|
|
67
|
+
* convenience: if either header is missing we emit a single warning and
|
|
68
|
+
* return a policy user explicitly marked `isServicePrincipal: true`, so
|
|
69
|
+
* even in dev a `usersOnly`-style policy that gates on
|
|
70
|
+
* `!user.isServicePrincipal` cannot be tricked. The matching SDK execution
|
|
71
|
+
* path also falls through to the SP client (no `runInUserContext` wrap),
|
|
72
|
+
* so the policy user and the SDK identity stay aligned.
|
|
38
73
|
*/
|
|
39
74
|
private _extractUser;
|
|
75
|
+
/**
|
|
76
|
+
* Extraction for OBO (on-behalf-of-user) volumes on the HTTP path. Both the
|
|
77
|
+
* `x-forwarded-access-token` and `x-forwarded-user` headers must be present
|
|
78
|
+
* for a valid end-user identity. When the token is missing:
|
|
79
|
+
* - In production we throw `AuthenticationError.missingToken` so the route
|
|
80
|
+
* responds with 401 (no SDK call is made).
|
|
81
|
+
* - In development (`NODE_ENV === "development"`) we emit a single warning
|
|
82
|
+
* and fall back to the service principal identity so local testing
|
|
83
|
+
* without a reverse proxy continues to work.
|
|
84
|
+
*/
|
|
85
|
+
private _extractOboUser;
|
|
40
86
|
/**
|
|
41
87
|
* Check the policy for a volume. No-op if no policy is configured.
|
|
42
88
|
* Throws `PolicyDeniedError` if denied.
|
|
@@ -44,8 +90,22 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
44
90
|
private _checkPolicy;
|
|
45
91
|
/**
|
|
46
92
|
* HTTP-level wrapper around `_checkPolicy`.
|
|
47
|
-
*
|
|
93
|
+
* Selects the policy user based on the volume's auth mode (resolved via
|
|
94
|
+
* `_resolveAuth`):
|
|
95
|
+
* - `"service-principal"` (default): use the `x-forwarded-user` header when
|
|
96
|
+
* present, otherwise fall back to the SP identity (legacy behavior).
|
|
97
|
+
* - `"on-behalf-of-user"`: require `x-forwarded-access-token` (and the
|
|
98
|
+
* matching `x-forwarded-user`); 401 in production when missing,
|
|
99
|
+
* dev-fallback to SP identity in development.
|
|
100
|
+
* Then runs the volume policy (403 on denial, 500 on unexpected error).
|
|
48
101
|
* Returns `true` if the request may proceed, `false` if a response was sent.
|
|
102
|
+
*
|
|
103
|
+
* NOTE: This method only selects which identity the *policy* sees. The
|
|
104
|
+
* matching SDK execution identity is selected separately by
|
|
105
|
+
* `_resolveAuthForRequest` and applied via `_runWithAuth` /
|
|
106
|
+
* `runInUserContext` in each handler. The two selections are designed to
|
|
107
|
+
* converge on the same identity per the policy-user matrix in the docs —
|
|
108
|
+
* see `docs/docs/plugins/files.md#policy-user-matrix`.
|
|
49
109
|
*/
|
|
50
110
|
private _enforcePolicy;
|
|
51
111
|
constructor(config: IFilesConfig);
|
|
@@ -55,20 +115,65 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
55
115
|
* or sends a 404 and returns `{ connector: undefined }`.
|
|
56
116
|
*/
|
|
57
117
|
private _resolveVolume;
|
|
118
|
+
/**
|
|
119
|
+
* Extract `req.query.path` as a single string when present.
|
|
120
|
+
*
|
|
121
|
+
* Express coerces repeated query parameters (`?path=a&path=b`) to a string
|
|
122
|
+
* array and dotted/nested params (`?path[k]=v`) to an object. Reject those
|
|
123
|
+
* with `400` instead of letting non-string values reach `_isValidPath` /
|
|
124
|
+
* `connector.resolvePath`, which would misbehave on arrays or objects.
|
|
125
|
+
*
|
|
126
|
+
* Returns `{ path }` (with `path` either a string or `undefined` when the
|
|
127
|
+
* query parameter was absent) on success. Returns `undefined` and writes a
|
|
128
|
+
* `400` response when the value is not a single string — callers must
|
|
129
|
+
* check the return for `undefined` before continuing.
|
|
130
|
+
*/
|
|
131
|
+
private _readPathQuery;
|
|
58
132
|
/**
|
|
59
133
|
* Validate a file/directory path from user input.
|
|
60
134
|
* Returns `true` if valid, or an error message string if invalid.
|
|
61
135
|
*/
|
|
62
136
|
private _isValidPath;
|
|
63
137
|
private _readSettings;
|
|
138
|
+
private _writeSettings;
|
|
139
|
+
private _downloadSettings;
|
|
64
140
|
/**
|
|
65
141
|
* Invalidate cached list entries for a directory after a write operation.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
142
|
+
* Must produce the SAME cache-key shape that `_handleList` stored under.
|
|
143
|
+
* `_handleList` builds its key from `req.query.path`: when `path` is
|
|
144
|
+
* provided it uses `connector.resolvePath(path)`, otherwise it uses the
|
|
145
|
+
* sentinel `"__root__"`. The invalidation here must derive the matching
|
|
146
|
+
* directory from the FILE path being written:
|
|
147
|
+
*
|
|
148
|
+
* - `"/Volumes/c/s/v/foo/bar.txt"` → `parentDirectory` returns
|
|
149
|
+
* `"/Volumes/c/s/v/foo"` → resolved path key.
|
|
150
|
+
* - `"/bar.txt"` and `"bar.txt"` → root-level files: matching list cache
|
|
151
|
+
* was a rootless `list()` call → `"__root__"` sentinel.
|
|
152
|
+
* - `"/Volumes/c/s/v/bar.txt"` → `parentDirectory` returns the UC
|
|
153
|
+
* volume path (`"/Volumes/c/s/v"`). That's also root-level — a
|
|
154
|
+
* rootless `list()` would have cached under `"__root__"`, while
|
|
155
|
+
* `list("/Volumes/c/s/v")` and `list("/Volumes/c/s/v/")` would have
|
|
156
|
+
* cached under the volume path with and without trailing slash. All
|
|
157
|
+
* three are invalidated.
|
|
158
|
+
*
|
|
159
|
+
* On OBO volumes the read cache is disabled (see `_readSettings`), so
|
|
160
|
+
* invalidation is a no-op here for `mode === "on-behalf-of-user"`. The
|
|
161
|
+
* cache layer is keyed by `getCurrentUserId()`, so user A's writes can
|
|
162
|
+
* only invalidate user A's cache entry — user B would otherwise see stale
|
|
163
|
+
* data for the same volume/path until TTL. Disabling the cache on OBO
|
|
164
|
+
* trades read performance for correctness; the alternative is a
|
|
165
|
+
* per-(volume, path) generation counter folded into the cache key on
|
|
166
|
+
* writes (a future enhancement).
|
|
68
167
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
168
|
+
* Best-effort: a thrown `connector.resolvePath` (e.g. on malformed input)
|
|
169
|
+
* is swallowed here. Invalidation is purely an optimization signal — a
|
|
170
|
+
* missed delete only costs read freshness, not correctness, and
|
|
171
|
+
* propagating the error would convert a successful write into an HTTP
|
|
172
|
+
* 500.
|
|
173
|
+
*
|
|
174
|
+
* Returns a `Promise<void>`; callers MUST `await` this before sending the
|
|
175
|
+
* HTTP success response so a follow-up `GET /list` issued in the same tick
|
|
176
|
+
* cannot race the underlying `cache.delete()` and observe stale data.
|
|
72
177
|
*/
|
|
73
178
|
private _invalidateListCache;
|
|
74
179
|
private _handleApiError;
|
|
@@ -89,19 +194,103 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
89
194
|
private _handleUpload;
|
|
90
195
|
private _handleMkdir;
|
|
91
196
|
private _handleDelete;
|
|
197
|
+
private _resolveAuth;
|
|
92
198
|
/**
|
|
93
|
-
*
|
|
199
|
+
* Build a `UserContext` from request headers when both
|
|
200
|
+
* `x-forwarded-access-token` and `x-forwarded-user` are present, otherwise
|
|
201
|
+
* return `null`. Used by OBO route handlers to wrap SDK calls in the
|
|
202
|
+
* end-user's identity. A `null` result means "fall back to the service
|
|
203
|
+
* principal client" — for OBO volumes in production, `_enforcePolicy` will
|
|
204
|
+
* already have responded 401 before we get here, so `null` is reachable
|
|
205
|
+
* only on the dev-fallback path.
|
|
206
|
+
*/
|
|
207
|
+
private _buildUserContextOrNull;
|
|
208
|
+
/**
|
|
209
|
+
* Build the telemetry attribute hash for the `files.auth_mode` span
|
|
210
|
+
* attribute. The value reflects what operationally happened — i.e.
|
|
211
|
+
* whether `runInUserContext` actually wrapped the SDK call:
|
|
212
|
+
* - HTTP route on OBO volume + valid token → `"on-behalf-of-user"`.
|
|
213
|
+
* - HTTP route on OBO volume + dev-fallback (no token) →
|
|
214
|
+
* `"service-principal"` (the route falls through to the SP client).
|
|
215
|
+
* - HTTP route on SP volume → `"service-principal"`.
|
|
216
|
+
* - `asUser(req)` programmatic calls with a real user context →
|
|
217
|
+
* `"on-behalf-of-user"`.
|
|
218
|
+
* - Any unwrapped path → `"service-principal"`.
|
|
219
|
+
*/
|
|
220
|
+
private _authModeAttributes;
|
|
221
|
+
/**
|
|
222
|
+
* One-shot resolver for HTTP route handlers. Builds the request's
|
|
223
|
+
* `UserContext` AT MOST ONCE (when the volume is OBO and the headers are
|
|
224
|
+
* present) and returns both the operationally-effective auth mode and the
|
|
225
|
+
* pre-built `UserContext`.
|
|
226
|
+
*
|
|
227
|
+
* Handlers thread the `userCtx` into `_runWithAuth(userCtx, fn)` to avoid
|
|
228
|
+
* a second `ServiceContext.createUserContext()` allocation — that call
|
|
229
|
+
* builds a fresh `WorkspaceClient` per invocation, so doing it twice per
|
|
230
|
+
* request was pure throwaway overhead.
|
|
231
|
+
*/
|
|
232
|
+
private _resolveAuthForRequest;
|
|
233
|
+
/**
|
|
234
|
+
* Run `fn` under the correct execution context.
|
|
235
|
+
* - `userCtx` is `null`: invokes `fn` directly so the service-principal
|
|
236
|
+
* `WorkspaceClient` and `getCurrentUserId()` are used — identical
|
|
237
|
+
* behavior to pre-OBO releases. This covers both SP volumes and the
|
|
238
|
+
* OBO dev-fallback path (where headers were missing).
|
|
239
|
+
* - `userCtx` is a `UserContext`: wraps `fn` in `runInUserContext(userCtx)`,
|
|
240
|
+
* so SDK calls execute as the end user and `getCurrentUserId()` (and
|
|
241
|
+
* therefore cache keys) resolve to the user's ID.
|
|
94
242
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
243
|
+
* The caller is responsible for building `userCtx` exactly once per
|
|
244
|
+
* request via `_resolveAuthForRequest`; this signature deliberately does
|
|
245
|
+
* NOT take a `req` so it cannot accidentally re-build the context.
|
|
246
|
+
*/
|
|
247
|
+
private _runWithAuth;
|
|
248
|
+
/**
|
|
249
|
+
* Tag the span that `FilesConnector.<operation>` opens with
|
|
250
|
+
* `files.auth_mode`. Programmatic VolumeAPI methods bypass
|
|
251
|
+
* `this.execute(...)` (and therefore the `TelemetryInterceptor`), so the
|
|
252
|
+
* connector's own `files.<operation>` span is the natural place to land
|
|
253
|
+
* this attribute. Rather than opening a parent `files.<operation>` span
|
|
254
|
+
* (which would duplicate the connector's span — same name, doubled
|
|
255
|
+
* allocation/export), we propagate the attribute via AsyncLocalStorage
|
|
256
|
+
* and let the connector merge it into its existing span at creation
|
|
257
|
+
* time.
|
|
258
|
+
*
|
|
259
|
+
* The `operation` parameter is unused by the propagation mechanism (the
|
|
260
|
+
* connector knows its own operation), but kept in the signature for API
|
|
261
|
+
* stability with the previous span-creation form.
|
|
262
|
+
*/
|
|
263
|
+
private _withAuthModeAttributes;
|
|
264
|
+
/**
|
|
265
|
+
* Wrap each `VolumeAPI` method so the `FilesConnector` span it produces is
|
|
266
|
+
* tagged with `files.auth_mode = "service-principal"`. Used for
|
|
267
|
+
* programmatic calls that don't go through `asUser(req)`.
|
|
268
|
+
*
|
|
269
|
+
* The attribute is attached to the connector's existing span via
|
|
270
|
+
* AsyncLocalStorage propagation (see `_withAuthModeAttributes`); no
|
|
271
|
+
* additional parent span is opened, so each call produces exactly one
|
|
272
|
+
* `files.<operation>` span instead of two.
|
|
273
|
+
*/
|
|
274
|
+
private _wrapVolumeAPIWithSPSpan;
|
|
275
|
+
/**
|
|
276
|
+
* Wrap each `VolumeAPI` method so its execution runs inside
|
|
277
|
+
* `runInUserContext(userCtx, ...)`. Used by `VolumeHandle.asUser(req)` to
|
|
278
|
+
* force the SDK identity to the end user regardless of the volume's
|
|
279
|
+
* `auth` setting. The policy check baked into each method (via
|
|
280
|
+
* `createVolumeAPI`) runs inside the same scope, so `getCurrentUserId()`
|
|
281
|
+
* and any cache `userKey` derived from it also resolve to the user.
|
|
282
|
+
*
|
|
283
|
+
* Each wrapped invocation tags the connector's span with
|
|
284
|
+
* `files.auth_mode = "on-behalf-of-user"` via AsyncLocalStorage
|
|
285
|
+
* propagation — no additional parent span is opened.
|
|
286
|
+
*/
|
|
287
|
+
private _wrapVolumeAPIInUserContext;
|
|
288
|
+
/**
|
|
289
|
+
* Creates a VolumeAPI for a specific volume key.
|
|
98
290
|
*
|
|
99
|
-
*
|
|
100
|
-
* Do not expose bypassed APIs to HTTP routes or end-user code paths.
|
|
291
|
+
* Enforces the volume's policy before each operation.
|
|
101
292
|
*/
|
|
102
|
-
protected createVolumeAPI(volumeKey: string, user: FilePolicyUser
|
|
103
|
-
bypassPolicy?: boolean;
|
|
104
|
-
}): VolumeAPI;
|
|
293
|
+
protected createVolumeAPI(volumeKey: string, user: FilePolicyUser): VolumeAPI;
|
|
105
294
|
/**
|
|
106
295
|
* Builds the agent-tool registry entries for a single volume. One set of
|
|
107
296
|
* tools per configured volume, keyed by `${volumeKey}.${method}`.
|
|
@@ -123,15 +312,22 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
123
312
|
* Returns the programmatic API for the Files plugin.
|
|
124
313
|
* Callable with a volume key to get a volume-scoped handle.
|
|
125
314
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
315
|
+
* SP volumes (`auth: "service-principal"`, the default) execute as the
|
|
316
|
+
* service principal. OBO volumes (`auth: "on-behalf-of-user"`) executed
|
|
317
|
+
* through the HTTP routes run as the end user; for programmatic calls
|
|
318
|
+
* outside a route, use `asUser(req)` to opt into per-user execution.
|
|
319
|
+
* `asUser(req)` is a hard override at the SDK level: it forces every
|
|
320
|
+
* subsequent call to execute as the end user inside `runInUserContext`,
|
|
321
|
+
* regardless of the volume's `auth` setting. Policies control per-user
|
|
322
|
+
* access in either mode.
|
|
128
323
|
*
|
|
129
324
|
* @example
|
|
130
325
|
* ```ts
|
|
131
326
|
* // Service principal access
|
|
132
327
|
* appKit.files("uploads").list()
|
|
133
328
|
*
|
|
134
|
-
* // With policy: pass user identity for access control
|
|
329
|
+
* // With policy: pass user identity for access control. The SDK call
|
|
330
|
+
* // also executes as the user (not the service principal).
|
|
135
331
|
* appKit.files("uploads").asUser(req).list()
|
|
136
332
|
* ```
|
|
137
333
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../../src/plugins/files/plugin.ts"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../../src/plugins/files/plugin.ts"],"mappings":";;;;;;;;;;;;;cAiEa,WAAA,SAAoB,MAAA,YAAkB,YAAA;EACjD,IAAA;;SAGO,QAAA,EAAuB,cAAA;EAAA,iBACb,WAAA;EAAA,UACC,MAAA,EAAQ,YAAA;EAAA,QAElB,gBAAA;EAAA,QACA,aAAA;EAAA,QACA,UAAA;EAAA,QACA,KAAA;;;;;;SAOD,eAAA,CAAgB,MAAA,EAAQ,YAAA,GAAe,MAAA,SAAe,YAAA;EA4CtB;;;;;;;;;;;;;;;;;;;;;;;;;EAAA,OAAhC,uBAAA,CAAwB,MAAA,EAAQ,YAAA,GAAe,mBAAA;EAxD5B;;;;;;;;;;;;;;;;;;EAAA,QA+FlB,YAAA;EA+JY;;;;;;;;;;EAAA,QA/HZ,eAAA;EAkcM;;;;EAAA,QA1aA,YAAA;EAmqBA;;;;;;;;;;;;;;;;;;;EAAA,QApnBA,cAAA;cAwDF,MAAA,EAAQ,YAAA;EAiDpB,YAAA,CAAa,MAAA,EAAQ,UAAA;EA8uCb;;;;EAAA,QAhnCA,cAAA;EAwuCR;;;;;;;;;;;;;EAAA,QAxsCQ,cAAA;EA+uCG;;;;EAAA,QA5tCH,YAAA;EAAA,QAQA,aAAA;EAAA,QAyBA,cAAA;EAAA,QAaA,iBAAA;EAuuCQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAA,QAprCF,oBAAA;EAAA,QAiFN,eAAA;EAAA,QAuCA,gBAAA;EAAA,QAOM,WAAA;EAAA,QAsCA,WAAA;EAAA,QAoFA,eAAA;EAAA,QAWA,UAAA;;;;;;UAgBA,UAAA;EAAA,QAqFA,aAAA;EAAA,QA0CA,eAAA;EAAA,QA0CA,cAAA;EAAA,QA0CA,aAAA;EAAA,QAkJA,YAAA;EAAA,QA8CA,aAAA;EAAA,QAgDN,YAAA;;;;;;;;;;UAmBA,uBAAA;;;;;;;;;;;;;UAmBA,mBAAA;;;;;;;;;;;;UAiBA,sBAAA;;;;;;;;;;;;;;;UA8BM,YAAA;;;;;;;;;;;;;;;;UAyBN,uBAAA;;;;;;;;;;;UAkBA,wBAAA;;;;;;;;;;;;;UAoCA,2BAAA;;;;;;YAgCE,eAAA,CACR,SAAA,UACA,IAAA,EAAM,cAAA,GACL,SAAA;;;;;;;;;;;UA8DK,kBAAA;EAAA,QA4FA,cAAA;EAAA,QAEA,UAAA;EAOF,QAAA,CAAA,GAAY,OAAA;EAmBlB,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CACJ,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;EAIH,OAAA,CAAQ,IAAA,GAJE,cAAA,GAIoD,MAAA,SAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;EA2B9D,OAAA,CAAA,GAAW,WAAA;EAiDX,YAAA,CAAA,GAAgB,MAAA;AAAA;;;;cAQL,KAAA,EAAK,QAAA,QAAA,WAAA,EAAA,YAAA;;gCAAA,UAAA"}
|