@databricks/appkit 0.83.0 → 0.84.0
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/CLAUDE.md +15 -1
- package/dist/agents/databricks.d.ts +15 -3
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js +25 -8
- package/dist/agents/databricks.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +2 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +6 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/commands/agent/eval.js +1 -1
- package/dist/cli/commands/generate-types.js +1 -1
- package/dist/cli/commands/plugin/sync/sync.js +28 -15
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/registry/add.js +3 -11
- package/dist/cli/commands/registry/add.js.map +1 -1
- package/dist/cli/commands/registry/config-writer.js +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -1
- package/dist/connectors/lakebase/routing-pool.js +4 -10
- package/dist/connectors/lakebase/routing-pool.js.map +1 -1
- package/dist/context/caller-context.d.ts +4 -1
- package/dist/context/caller-context.d.ts.map +1 -1
- package/dist/context/caller-context.js.map +1 -1
- package/dist/context/execution-context.d.ts +29 -1
- package/dist/context/execution-context.d.ts.map +1 -1
- package/dist/context/execution-context.js +60 -8
- package/dist/context/execution-context.js.map +1 -1
- package/dist/context/index.d.ts +2 -2
- package/dist/context/index.js +2 -1
- package/dist/context/request-scope.d.ts +2 -0
- package/dist/context/request-scope.js +39 -0
- package/dist/context/request-scope.js.map +1 -0
- package/dist/context/resource-capabilities.js +59 -0
- package/dist/context/resource-capabilities.js.map +1 -0
- package/dist/context/scoped-api.js +104 -0
- package/dist/context/scoped-api.js.map +1 -0
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js +1 -1
- package/dist/context/service-context.js.map +1 -1
- package/dist/context/user-context.d.ts +16 -19
- package/dist/context/user-context.d.ts.map +1 -1
- package/dist/context/user-context.js +30 -3
- package/dist/context/user-context.js.map +1 -1
- package/dist/core/agent/load-agents.d.ts.map +1 -1
- package/dist/core/agent/load-agents.js +5 -2
- package/dist/core/agent/load-agents.js.map +1 -1
- package/dist/core/agent/run-agent.d.ts +19 -6
- package/dist/core/agent/run-agent.d.ts.map +1 -1
- package/dist/core/agent/run-agent.js +53 -17
- package/dist/core/agent/run-agent.js.map +1 -1
- package/dist/core/agent/types.d.ts +18 -1
- package/dist/core/agent/types.d.ts.map +1 -1
- package/dist/core/agent/types.js.map +1 -1
- package/dist/core/appkit.d.ts +4 -3
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +30 -9
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/plugin-context.d.ts +14 -15
- package/dist/core/plugin-context.d.ts.map +1 -1
- package/dist/core/plugin-context.js +24 -14
- package/dist/core/plugin-context.js.map +1 -1
- package/dist/errors/base.d.ts +2 -2
- package/dist/errors/base.js +2 -2
- package/dist/errors/base.js.map +1 -1
- package/dist/errors/identity-expired.d.ts +14 -0
- package/dist/errors/identity-expired.d.ts.map +1 -0
- package/dist/errors/identity-expired.js +33 -0
- package/dist/errors/identity-expired.js.map +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.js +5 -2
- package/dist/logging/logger.js +1 -1
- package/dist/plugin/execution-result.d.ts +4 -1
- package/dist/plugin/execution-result.d.ts.map +1 -1
- package/dist/plugin/interceptors/telemetry.js +6 -4
- package/dist/plugin/interceptors/telemetry.js.map +1 -1
- package/dist/plugin/plugin.d.ts +13 -21
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +41 -122
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts +12 -0
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js +65 -15
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/agents/auth-mode.js +42 -0
- package/dist/plugins/agents/auth-mode.js.map +1 -0
- package/dist/plugins/agents/index.d.ts +1 -1
- package/dist/plugins/agents/mlflow.js +11 -3
- package/dist/plugins/agents/mlflow.js.map +1 -1
- package/dist/plugins/agents/tool-dispatch.js +9 -1
- package/dist/plugins/agents/tool-dispatch.js.map +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
- package/dist/plugins/ai-search/ai-search.js +4 -4
- package/dist/plugins/ai-search/ai-search.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts +2 -2
- package/dist/plugins/analytics/analytics.js +6 -6
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/database/crud/contract.js +2 -2
- package/dist/plugins/database/crud/contract.js.map +1 -1
- package/dist/plugins/files/plugin.d.ts +10 -4
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js +34 -13
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/genie/genie.d.ts +9 -0
- package/dist/plugins/genie/genie.d.ts.map +1 -1
- package/dist/plugins/genie/genie.js +12 -3
- package/dist/plugins/genie/genie.js.map +1 -1
- package/dist/plugins/genie/manifest.js +1 -0
- package/dist/plugins/jobs/plugin.js +2 -2
- package/dist/plugins/jobs/plugin.js.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts +10 -17
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +12 -17
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/server/client-config-sanitizer.js +1 -4
- package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
- package/dist/plugins/server/dev-obo-middleware.js +60 -0
- package/dist/plugins/server/dev-obo-middleware.js.map +1 -0
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js +5 -2
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +3 -3
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/plugins/server/static-server.js +3 -3
- package/dist/plugins/server/static-server.js.map +1 -1
- package/dist/plugins/server/utils.js +3 -3
- package/dist/plugins/server/utils.js.map +1 -1
- package/dist/plugins/server/vite-dev-server.js +4 -4
- package/dist/plugins/server/vite-dev-server.js.map +1 -1
- package/dist/plugins/serving/manifest.js +1 -0
- package/dist/plugins/serving/serving.js +6 -6
- package/dist/plugins/serving/serving.js.map +1 -1
- package/dist/schemas/manifest.d.ts +35 -1
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js +112 -4
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/shared/src/dev-obo.js +86 -0
- package/dist/shared/src/dev-obo.js.map +1 -0
- package/dist/shared/src/index.d.ts +1 -1
- package/dist/shared/src/plugin.d.ts +12 -1
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +41 -33
- package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.js +43 -4
- package/dist/shared/src/schemas/manifest.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js +5 -2
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/telemetry/execution-span-processor.js +21 -0
- package/dist/telemetry/execution-span-processor.js.map +1 -0
- package/dist/telemetry/telemetry-manager.js +2 -1
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/testing/create-test-app.d.ts +2 -2
- package/dist/testing/create-test-app.js.map +1 -1
- package/dist/testing/test-plugin-context.d.ts +3 -12
- package/dist/testing/test-plugin-context.d.ts.map +1 -1
- package/dist/testing/test-plugin-context.js +24 -16
- package/dist/testing/test-plugin-context.js.map +1 -1
- package/dist/type-generator/database/generate.js +3 -3
- package/dist/type-generator/database/generate.js.map +1 -1
- package/dist/type-generator/migration.js +2 -2
- package/dist/type-generator/migration.js.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.js +3 -3
- package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
- package/dist/utils/is-plain-object.js +11 -0
- package/dist/utils/is-plain-object.js.map +1 -0
- package/docs/api/appkit/Class.AppKitError.md +2 -1
- package/docs/api/appkit/Class.AuthenticationError.md +1 -1
- package/docs/api/appkit/Class.ConfigurationError.md +1 -1
- package/docs/api/appkit/Class.ConnectionError.md +1 -1
- package/docs/api/appkit/Class.DatabaseValidationError.md +1 -1
- package/docs/api/appkit/Class.ExecutionError.md +1 -1
- package/docs/api/appkit/Class.IdentityExpiredError.md +190 -0
- package/docs/api/appkit/Class.InitializationError.md +1 -1
- package/docs/api/appkit/Class.Plugin.md +8 -12
- package/docs/api/appkit/Class.ServerError.md +1 -1
- package/docs/api/appkit/Class.ServiceContext.md +169 -0
- package/docs/api/appkit/Class.TunnelError.md +1 -1
- package/docs/api/appkit/Class.ValidationError.md +1 -1
- package/docs/api/appkit/Function.createApp.md +12 -12
- package/docs/api/appkit/Function.getCallerContext.md +12 -0
- package/docs/api/appkit/Function.getCurrentUserId.md +14 -0
- package/docs/api/appkit/Function.getExecutionContext.md +1 -1
- package/docs/api/appkit/Function.getUserContext.md +16 -0
- package/docs/api/appkit/Function.isInUserContext.md +12 -0
- package/docs/api/appkit/Function.isUserContext.md +20 -0
- package/docs/api/appkit/Function.runAgent.md +1 -1
- package/docs/api/appkit/Function.runInCallerContext.md +27 -0
- package/docs/api/appkit/Function.runInUserContext.md +29 -0
- package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
- package/docs/api/appkit/Interface.AgentsPluginConfig.md +11 -0
- package/docs/api/appkit/Interface.CallerContext.md +13 -0
- package/docs/api/appkit/Interface.IndexConfig.md +1 -1
- package/docs/api/appkit/Interface.PluginManifest.md +9 -1
- package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
- package/docs/api/appkit/Interface.RunAgentInput.md +45 -1
- package/docs/api/appkit/TypeAlias.AgentAuth.md +8 -0
- package/docs/api/appkit/TypeAlias.AppKitApi.md +35 -0
- package/docs/api/appkit/TypeAlias.ExecutionContext.md +4 -1
- package/docs/api/appkit/TypeAlias.ExecutionResult.md +65 -0
- package/docs/api/appkit/TypeAlias.ScopedPluginMap.md +12 -0
- package/docs/api/appkit/TypeAlias.UserContext.md +109 -0
- package/docs/api/appkit/TypeAlias.UserScopedApp.md +39 -0
- package/docs/api/appkit.md +14 -0
- package/docs/plugins/agents.md +45 -0
- package/docs/plugins/execution-context.md +101 -50
- package/docs/plugins/lakebase.md +6 -82
- package/llms.txt +15 -1
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { APP_ONLY_RESOURCE_TYPES, SCOPE_BY_TYPE } from "../shared/src/schemas/manifest.js";
|
|
2
|
+
import { AppKitError } from "../errors/base.js";
|
|
3
|
+
import { getCallerContext, isLegacyCallerScope } from "./execution-context.js";
|
|
4
|
+
import { scopeApi } from "./scoped-api.js";
|
|
5
|
+
|
|
6
|
+
//#region src/context/resource-capabilities.ts
|
|
7
|
+
var AppOnlyResourceError = class extends AppKitError {
|
|
8
|
+
code = "APP_ONLY_RESOURCE";
|
|
9
|
+
statusCode = 400;
|
|
10
|
+
isRetryable = false;
|
|
11
|
+
constructor(type, name = type) {
|
|
12
|
+
const message = `Resource "${name}" is app-only in this version of AppKit and does not support OBO (on-behalf-of-user) execution. It runs as the service principal. Resource type: ${type}.`;
|
|
13
|
+
super(message, {
|
|
14
|
+
clientMessage: message,
|
|
15
|
+
context: { resourceType: type }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/** Read the same required/bound resource metadata used by provisioning. */
|
|
20
|
+
function getPluginResources(plugin) {
|
|
21
|
+
const ctor = plugin.constructor;
|
|
22
|
+
const config = plugin.config;
|
|
23
|
+
const resources = ctor.manifest?.resources;
|
|
24
|
+
const optional = resources?.optional?.filter((resource) => Object.values(resource.fields).some((field) => field.env && process.env[field.env]?.trim())) ?? [];
|
|
25
|
+
const runtime = config && ctor.getResourceRequirements ? ctor.getResourceRequirements(config) : [];
|
|
26
|
+
return [
|
|
27
|
+
...resources?.required ?? [],
|
|
28
|
+
...optional,
|
|
29
|
+
...runtime.filter((resource) => resource.required)
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
function getPluginResourceTypes(plugin) {
|
|
33
|
+
return [...new Set(getPluginResources(plugin).map((resource) => resource.type))];
|
|
34
|
+
}
|
|
35
|
+
function hasOboResource(types) {
|
|
36
|
+
return types.some((type) => Object.hasOwn(SCOPE_BY_TYPE, type));
|
|
37
|
+
}
|
|
38
|
+
function isAppOnly(type) {
|
|
39
|
+
return APP_ONLY_RESOURCE_TYPES.has(type);
|
|
40
|
+
}
|
|
41
|
+
function assertResourceExecution(type) {
|
|
42
|
+
if (getCallerContext() && !isLegacyCallerScope() && isAppOnly(type)) throw new AppOnlyResourceError(type);
|
|
43
|
+
}
|
|
44
|
+
function assertPluginExecution(plugin, callerRequested = false) {
|
|
45
|
+
if (!callerRequested && (!getCallerContext() || isLegacyCallerScope())) return;
|
|
46
|
+
for (const resource of getPluginResources(plugin)) if (isAppOnly(resource.type)) throw new AppOnlyResourceError(resource.type, resource.alias?.trim() || resource.type);
|
|
47
|
+
}
|
|
48
|
+
/** Cached app handles still check the principal at invocation time. */
|
|
49
|
+
function guardPluginApi(plugin, value) {
|
|
50
|
+
if (!getPluginResourceTypes(plugin).some(isAppOnly)) return value;
|
|
51
|
+
return scopeApi(value, { run: (fn) => {
|
|
52
|
+
assertPluginExecution(plugin);
|
|
53
|
+
return fn();
|
|
54
|
+
} }, plugin, true);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { assertPluginExecution, assertResourceExecution, getPluginResourceTypes, guardPluginApi, hasOboResource };
|
|
59
|
+
//# sourceMappingURL=resource-capabilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-capabilities.js","names":[],"sources":["../../src/context/resource-capabilities.ts"],"sourcesContent":["import {\n APP_ONLY_RESOURCE_TYPES,\n SCOPE_BY_TYPE,\n type BasePluginConfig,\n type PluginConstructor,\n} from \"shared\";\n\nimport { AppKitError } from \"../errors/base\";\nimport { getCallerContext, isLegacyCallerScope } from \"./execution-context\";\nimport { scopeApi } from \"./scoped-api\";\n\nclass AppOnlyResourceError extends AppKitError {\n readonly code = \"APP_ONLY_RESOURCE\";\n readonly statusCode = 400;\n readonly isRetryable = false;\n constructor(type: string, name = type) {\n const message = `Resource \"${name}\" is app-only in this version of AppKit and does not support OBO (on-behalf-of-user) execution. It runs as the service principal. Resource type: ${type}.`;\n super(message, { clientMessage: message, context: { resourceType: type } });\n }\n}\n\n/** Read the same required/bound resource metadata used by provisioning. */\nfunction getPluginResources(plugin: object) {\n const ctor = plugin.constructor as Partial<PluginConstructor>;\n const config = (plugin as { config?: BasePluginConfig }).config;\n const resources = ctor.manifest?.resources;\n const optional =\n resources?.optional?.filter((resource) =>\n Object.values(resource.fields).some(\n (field) => field.env && process.env[field.env]?.trim(),\n ),\n ) ?? [];\n const runtime =\n config && ctor.getResourceRequirements\n ? ctor.getResourceRequirements(config)\n : [];\n return [\n ...(resources?.required ?? []),\n ...optional,\n ...runtime.filter((resource) => resource.required),\n ];\n}\n\nexport function getPluginResourceTypes(plugin: object): string[] {\n return [\n ...new Set(getPluginResources(plugin).map((resource) => resource.type)),\n ];\n}\n\nexport function hasOboResource(types: readonly string[]): boolean {\n return types.some((type) => Object.hasOwn(SCOPE_BY_TYPE, type));\n}\n\nfunction isAppOnly(type: string): boolean {\n return (APP_ONLY_RESOURCE_TYPES as ReadonlySet<string>).has(type);\n}\n\nexport function assertResourceExecution(type: string): void {\n if (getCallerContext() && !isLegacyCallerScope() && isAppOnly(type))\n throw new AppOnlyResourceError(type);\n}\n\nexport function assertPluginExecution(\n plugin: object,\n callerRequested = false,\n): void {\n if (!callerRequested && (!getCallerContext() || isLegacyCallerScope()))\n return;\n for (const resource of getPluginResources(plugin)) {\n if (isAppOnly(resource.type)) {\n throw new AppOnlyResourceError(\n resource.type,\n resource.alias?.trim() || resource.type,\n );\n }\n }\n}\n\n/** Cached app handles still check the principal at invocation time. */\nexport function guardPluginApi<T>(plugin: object, value: T): T {\n if (!getPluginResourceTypes(plugin).some(isAppOnly)) return value;\n return scopeApi(\n value,\n {\n run: (fn) => {\n assertPluginExecution(plugin);\n return fn();\n },\n },\n plugin,\n true,\n );\n}\n"],"mappings":";;;;;;AAWA,IAAM,uBAAN,cAAmC,YAAY;CAC7C,AAAS,OAAO;CAChB,AAAS,aAAa;CACtB,AAAS,cAAc;CACvB,YAAY,MAAc,OAAO,MAAM;EACrC,MAAM,UAAU,aAAa,KAAK,mJAAmJ,KAAK;AAC1L,QAAM,SAAS;GAAE,eAAe;GAAS,SAAS,EAAE,cAAc,MAAM;GAAE,CAAC;;;;AAK/E,SAAS,mBAAmB,QAAgB;CAC1C,MAAM,OAAO,OAAO;CACpB,MAAM,SAAU,OAAyC;CACzD,MAAM,YAAY,KAAK,UAAU;CACjC,MAAM,WACJ,WAAW,UAAU,QAAQ,aAC3B,OAAO,OAAO,SAAS,OAAO,CAAC,MAC5B,UAAU,MAAM,OAAO,QAAQ,IAAI,MAAM,MAAM,MAAM,CACvD,CACF,IAAI,EAAE;CACT,MAAM,UACJ,UAAU,KAAK,0BACX,KAAK,wBAAwB,OAAO,GACpC,EAAE;AACR,QAAO;EACL,GAAI,WAAW,YAAY,EAAE;EAC7B,GAAG;EACH,GAAG,QAAQ,QAAQ,aAAa,SAAS,SAAS;EACnD;;AAGH,SAAgB,uBAAuB,QAA0B;AAC/D,QAAO,CACL,GAAG,IAAI,IAAI,mBAAmB,OAAO,CAAC,KAAK,aAAa,SAAS,KAAK,CAAC,CACxE;;AAGH,SAAgB,eAAe,OAAmC;AAChE,QAAO,MAAM,MAAM,SAAS,OAAO,OAAO,eAAe,KAAK,CAAC;;AAGjE,SAAS,UAAU,MAAuB;AACxC,QAAQ,wBAAgD,IAAI,KAAK;;AAGnE,SAAgB,wBAAwB,MAAoB;AAC1D,KAAI,kBAAkB,IAAI,CAAC,qBAAqB,IAAI,UAAU,KAAK,CACjE,OAAM,IAAI,qBAAqB,KAAK;;AAGxC,SAAgB,sBACd,QACA,kBAAkB,OACZ;AACN,KAAI,CAAC,oBAAoB,CAAC,kBAAkB,IAAI,qBAAqB,EACnE;AACF,MAAK,MAAM,YAAY,mBAAmB,OAAO,CAC/C,KAAI,UAAU,SAAS,KAAK,CAC1B,OAAM,IAAI,qBACR,SAAS,MACT,SAAS,OAAO,MAAM,IAAI,SAAS,KACpC;;;AAMP,SAAgB,eAAkB,QAAgB,OAAa;AAC7D,KAAI,CAAC,uBAAuB,OAAO,CAAC,KAAK,UAAU,CAAE,QAAO;AAC5D,QAAO,SACL,OACA,EACE,MAAM,OAAO;AACX,wBAAsB,OAAO;AAC7B,SAAO,IAAI;IAEd,EACD,QACA,KACD"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { isPlainObject } from "../utils/is-plain-object.js";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
|
|
4
|
+
//#region src/context/scoped-api.ts
|
|
5
|
+
const EXCLUDED_FROM_PROXY = new Set([
|
|
6
|
+
"setup",
|
|
7
|
+
"shutdown",
|
|
8
|
+
"attachContext",
|
|
9
|
+
"injectRoutes",
|
|
10
|
+
"getEndpoints",
|
|
11
|
+
"getSkipBodyParsingPaths",
|
|
12
|
+
"abortActiveOperations",
|
|
13
|
+
"clientConfig",
|
|
14
|
+
"constructor"
|
|
15
|
+
]);
|
|
16
|
+
const IDENTITY_METHODS = new Set(["asUser"]);
|
|
17
|
+
function isIdentityMethod(key) {
|
|
18
|
+
return IDENTITY_METHODS.has(key);
|
|
19
|
+
}
|
|
20
|
+
function isNativeStream(value) {
|
|
21
|
+
return typeof ReadableStream !== "undefined" && value instanceof ReadableStream || value instanceof Readable;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Whether a value can run code later and so must run in the caller scope:
|
|
25
|
+
* a function, a Promise, a non-native async iterable, or a plain object
|
|
26
|
+
* reaching one of those through its own properties. Getter properties count,
|
|
27
|
+
* and are not invoked here. Arrays and other objects are plain data, as before.
|
|
28
|
+
*/
|
|
29
|
+
function needsScope(value, seen = /* @__PURE__ */ new Set()) {
|
|
30
|
+
if (typeof value === "function" || value instanceof Promise) return true;
|
|
31
|
+
if (!value || typeof value !== "object" || isNativeStream(value)) return false;
|
|
32
|
+
if (Symbol.asyncIterator in value) return true;
|
|
33
|
+
if (!isPlainObject(value) || seen.has(value)) return false;
|
|
34
|
+
seen.add(value);
|
|
35
|
+
return Reflect.ownKeys(value).some((key) => {
|
|
36
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
37
|
+
return descriptor?.get !== void 0 || needsScope(descriptor?.value, seen);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Preserve identity across callable exports, returned handles, and lazy streams. */
|
|
41
|
+
function scopeApi(value, scope, receiver, preserveIdentityMethods = false) {
|
|
42
|
+
if (typeof value === "function") return new Proxy(value, {
|
|
43
|
+
apply: (fn, thisArg, args) => {
|
|
44
|
+
const result = scope.run(() => Reflect.apply(fn, receiver ?? thisArg, args));
|
|
45
|
+
if (preserveIdentityMethods) return result;
|
|
46
|
+
return scopeApi(result, scope);
|
|
47
|
+
},
|
|
48
|
+
get: (fn, key) => !preserveIdentityMethods && isIdentityMethod(key) ? void 0 : scopeApi(Reflect.get(fn, key), scope, fn, preserveIdentityMethods)
|
|
49
|
+
});
|
|
50
|
+
if (value instanceof Promise) return value.then((result) => scopeApi(result, scope, void 0, preserveIdentityMethods));
|
|
51
|
+
if (isNativeStream(value)) return value;
|
|
52
|
+
if (value && typeof value === "object" && Symbol.asyncIterator in value) {
|
|
53
|
+
const iterable = value;
|
|
54
|
+
const wrapIterator = (iterator) => {
|
|
55
|
+
const finish = iterator.return?.bind(iterator);
|
|
56
|
+
const fail = iterator.throw?.bind(iterator);
|
|
57
|
+
return {
|
|
58
|
+
[Symbol.asyncIterator]() {
|
|
59
|
+
return this;
|
|
60
|
+
},
|
|
61
|
+
next: (...args) => scope.run(() => iterator.next(...args)),
|
|
62
|
+
...finish && { return: (result) => scope.run(() => finish(result)) },
|
|
63
|
+
...fail && { throw: (error) => scope.run(() => fail(error)) }
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
if ("next" in value && typeof value.next === "function") return wrapIterator(value);
|
|
67
|
+
return { [Symbol.asyncIterator]: () => wrapIterator(scope.run(() => iterable[Symbol.asyncIterator]())) };
|
|
68
|
+
}
|
|
69
|
+
if (isPlainObject(value)) {
|
|
70
|
+
if (!needsScope(value)) return value;
|
|
71
|
+
const result = Object.create(Object.getPrototypeOf(value));
|
|
72
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
73
|
+
if (!preserveIdentityMethods && isIdentityMethod(key)) continue;
|
|
74
|
+
Object.defineProperty(result, key, {
|
|
75
|
+
enumerable: Object.getOwnPropertyDescriptor(value, key)?.enumerable,
|
|
76
|
+
configurable: true,
|
|
77
|
+
get: () => {
|
|
78
|
+
const member = scope.run(() => Reflect.get(value, key));
|
|
79
|
+
return needsScope(member) ? scopeApi(member, scope, receiver ?? value, preserveIdentityMethods) : member;
|
|
80
|
+
},
|
|
81
|
+
set: (next) => {
|
|
82
|
+
Reflect.set(value, key, next);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
/** Compatibility adapter for direct Plugin.asUser callers. */
|
|
91
|
+
function scopePlugin(plugin, scope) {
|
|
92
|
+
return new Proxy(plugin, { get(target, key) {
|
|
93
|
+
if (isIdentityMethod(key)) return void 0;
|
|
94
|
+
const value = Reflect.get(target, key, target);
|
|
95
|
+
if (typeof key === "string" && EXCLUDED_FROM_PROXY.has(key)) return value;
|
|
96
|
+
if (key === "exports" && typeof value === "function") return () => scopeApi(scope.run(() => value.call(target)) ?? {}, scope, target);
|
|
97
|
+
if (typeof value === "function") return (...args) => scope.run(() => Reflect.apply(value, target, args));
|
|
98
|
+
return scopeApi(value, scope, target);
|
|
99
|
+
} });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
export { scopeApi, scopePlugin };
|
|
104
|
+
//# sourceMappingURL=scoped-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scoped-api.js","names":[],"sources":["../../src/context/scoped-api.ts"],"sourcesContent":["import { Readable } from \"node:stream\";\n\nimport { isPlainObject } from \"../utils/is-plain-object\";\nimport type { RequestScope } from \"./request-scope\";\n\nconst EXCLUDED_FROM_PROXY = new Set([\n \"setup\",\n \"shutdown\",\n \"attachContext\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"getSkipBodyParsingPaths\",\n \"abortActiveOperations\",\n \"clientConfig\",\n \"constructor\",\n]);\n\n// Methods that would re-scope a scoped surface to another identity. Only\n// these are hidden; other `as*` names (asCsv, asArrow) are ordinary exports.\nconst IDENTITY_METHODS: ReadonlySet<PropertyKey> = new Set([\"asUser\"]);\n\nfunction isIdentityMethod(key: PropertyKey): boolean {\n return IDENTITY_METHODS.has(key);\n}\n\nfunction isNativeStream(value: unknown): boolean {\n return (\n (typeof ReadableStream !== \"undefined\" &&\n value instanceof ReadableStream) ||\n value instanceof Readable\n );\n}\n\n/**\n * Whether a value can run code later and so must run in the caller scope:\n * a function, a Promise, a non-native async iterable, or a plain object\n * reaching one of those through its own properties. Getter properties count,\n * and are not invoked here. Arrays and other objects are plain data, as before.\n */\nfunction needsScope(value: unknown, seen = new Set<object>()): boolean {\n if (typeof value === \"function\" || value instanceof Promise) return true;\n if (!value || typeof value !== \"object\" || isNativeStream(value)) {\n return false;\n }\n if (Symbol.asyncIterator in value) return true;\n if (!isPlainObject(value) || seen.has(value)) return false;\n seen.add(value);\n return Reflect.ownKeys(value).some((key) => {\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n return descriptor?.get !== undefined || needsScope(descriptor?.value, seen);\n });\n}\n\n/** Preserve identity across callable exports, returned handles, and lazy streams. */\nexport function scopeApi<T>(\n value: T,\n scope: RequestScope,\n receiver?: unknown,\n preserveIdentityMethods = false,\n): T {\n if (typeof value === \"function\") {\n return new Proxy(value, {\n apply: (fn, thisArg, args) => {\n const result = scope.run(() =>\n Reflect.apply(fn, receiver ?? thisArg, args),\n );\n // Ambient resource guards must not change ordinary SP result objects.\n if (preserveIdentityMethods) return result;\n return scopeApi(result, scope);\n },\n get: (fn, key) =>\n !preserveIdentityMethods && isIdentityMethod(key)\n ? undefined\n : scopeApi(Reflect.get(fn, key), scope, fn, preserveIdentityMethods),\n });\n }\n if (value instanceof Promise) {\n return value.then((result) =>\n scopeApi(result, scope, undefined, preserveIdentityMethods),\n ) as T;\n }\n // Native streams pass through unchanged. The authenticated request already\n // ran inside the caller scope; reading the body is pure data with no\n // deferred identity work, and wrapping would strip getReader/pipe/cancel.\n if (isNativeStream(value)) {\n return value;\n }\n if (value && typeof value === \"object\" && Symbol.asyncIterator in value) {\n const iterable = value as AsyncIterable<unknown>;\n const wrapIterator = (\n iterator: AsyncIterator<unknown>,\n ): AsyncIterableIterator<unknown> => {\n const finish = iterator.return?.bind(iterator);\n const fail = iterator.throw?.bind(iterator);\n return {\n [Symbol.asyncIterator]() {\n return this;\n },\n next: (...args: [] | [unknown]) =>\n scope.run(() => iterator.next(...args)),\n ...(finish && {\n return: (result?: unknown) => scope.run(() => finish(result)),\n }),\n ...(fail && {\n throw: (error?: unknown) => scope.run(() => fail(error)),\n }),\n };\n };\n if (\"next\" in value && typeof value.next === \"function\") {\n return wrapIterator(value as AsyncIterableIterator<unknown>) as T;\n }\n return {\n [Symbol.asyncIterator]: () =>\n wrapIterator(scope.run(() => iterable[Symbol.asyncIterator]())),\n } as T;\n }\n if (isPlainObject(value)) {\n // Plain data results come back unchanged: mutable and identity-stable.\n if (!needsScope(value)) return value;\n const result = Object.create(Object.getPrototypeOf(value));\n for (const key of Reflect.ownKeys(value)) {\n if (!preserveIdentityMethods && isIdentityMethod(key)) continue;\n Object.defineProperty(result, key, {\n enumerable: Object.getOwnPropertyDescriptor(value, key)?.enumerable,\n configurable: true,\n get: () => {\n const member = scope.run(() => Reflect.get(value, key));\n // Data members are returned as-is so repeated reads are identical.\n return needsScope(member)\n ? scopeApi(\n member,\n scope,\n receiver ?? value,\n preserveIdentityMethods,\n )\n : member;\n },\n set: (next: unknown) => {\n Reflect.set(value, key, next);\n },\n });\n }\n return result;\n }\n return value;\n}\n\n/** Compatibility adapter for direct Plugin.asUser callers. */\nexport function scopePlugin<T extends object>(\n plugin: T,\n scope: RequestScope,\n): T {\n return new Proxy(plugin, {\n get(target, key) {\n if (isIdentityMethod(key)) return undefined;\n const value = Reflect.get(target, key, target);\n if (typeof key === \"string\" && EXCLUDED_FROM_PROXY.has(key)) return value;\n if (key === \"exports\" && typeof value === \"function\") {\n return () =>\n scopeApi(scope.run(() => value.call(target)) ?? {}, scope, target);\n }\n if (typeof value === \"function\") {\n return (...args: unknown[]) =>\n scope.run(() => Reflect.apply(value, target, args));\n }\n return scopeApi(value, scope, target);\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-context.d.ts","names":[],"sources":["../../src/context/service-context.ts"],"mappings":";;;;;;;AAuBA;;UAAiB,mBAAA;EAEE;EAAA,SAAR,MAAA,EAAQ,eAAA;EASK;EAAA,SAPb,aAAA;EAOoB;;;;EAAA,SAFpB,WAAA,GAAc,OAAA;EAAA;EAAA,SAEd,WAAA,EAAa,OAAA;AAAA;;;AAUxB;;;;;cAAa,cAAA;EAAA,eACI,QAAA;EAAA,eACA,WAAA;
|
|
1
|
+
{"version":3,"file":"service-context.d.ts","names":[],"sources":["../../src/context/service-context.ts"],"mappings":";;;;;;;AAuBA;;UAAiB,mBAAA;EAEE;EAAA,SAAR,MAAA,EAAQ,eAAA;EASK;EAAA,SAPb,aAAA;EAOoB;;;;EAAA,SAFpB,WAAA,GAAc,OAAA;EAAA;EAAA,SAEd,WAAA,EAAa,OAAA;AAAA;;;AAUxB;;;;;cAAa,cAAA;EAAA,eACI,QAAA;EAAA,eACA,WAAA;EA4GZ;;;;;;;;EAAA,OAlGU,UAAA,CACX,OAAA;IAAY,WAAA;EAAA,GACZ,MAAA,GAAS,eAAA,GACR,OAAA,CAAQ,mBAAA;EADT;;;;EAAA,OAmBK,GAAA,CAAA,GAAO,mBAAA;EAaP;;;EAAA,OAAA,aAAA,CAAA;EAgBL;;;;;;;;;EAAA,OAHK,mBAAA,CACL,KAAA,UACA,MAAA,UACA,QAAA,WACA,SAAA,YACC,aAAA;EAiEI;EAAA,OAxBA,iBAAA,CACL,KAAA,UACA,MAAA,UACA,QAAA,WACA,SAAA,YACC,aAAA,GAAgB,WAAA;EAuBE;;;;EAAA,OAJd,gBAAA,CAAA,GAAoB,aAAA;EAAA,eAIN,aAAA;EAAA,eA4CA,cAAA;;;;SA0Bd,KAAA,CAAA;AAAA"}
|
|
@@ -62,7 +62,7 @@ var ServiceContext = class ServiceContext {
|
|
|
62
62
|
*/
|
|
63
63
|
static createCallerContext(token, userId, userName, userEmail) {
|
|
64
64
|
if (!token) throw AuthenticationError.missingToken("user token");
|
|
65
|
-
const host = process.env.DATABRICKS_HOST;
|
|
65
|
+
const host = process.env.DATABRICKS_HOST || (process.env.NODE_ENV === "development" && ServiceContext.isInitialized() ? ServiceContext.get().client.config?.host : void 0);
|
|
66
66
|
if (!host) throw ConfigurationError.missingEnvVar("DATABRICKS_HOST");
|
|
67
67
|
const serviceCtx = ServiceContext.get();
|
|
68
68
|
const userClient = createWorkspaceClient({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-context.js","names":[],"sources":["../../src/context/service-context.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport {\n AuthenticationError,\n ConfigurationError,\n InitializationError,\n} from \"../errors\";\nimport { WarehouseResource } from \"../resources/warehouse\";\nimport {\n type ClientOptions,\n ConfigError,\n createWorkspaceClient,\n type WorkspaceClient,\n} from \"../workspace-client\";\nimport { type CallerContext, snapshotCallerContext } from \"./caller-context\";\nimport { getClientOptions } from \"./client-options\";\nimport { warnContextDeprecation } from \"./deprecation\";\nimport { legacyUserContext, type UserContext } from \"./user-context\";\n\n/**\n * Service context holds the service principal identity and workspace client.\n * This is initialized once at app startup and shared across all requests.\n */\nexport interface ServiceContextState {\n /** WorkspaceClient authenticated as the service principal */\n readonly client: WorkspaceClient;\n /** The service principal's user ID */\n readonly serviceUserId: string;\n /**\n * @deprecated Use getWarehouseId() from @databricks/appkit.\n * Retained for backward compatibility.\n */\n readonly warehouseId?: Promise<string>;\n /** Promise that resolves to the workspace ID */\n readonly workspaceId: Promise<string>;\n}\n\n/**\n * ServiceContext is a singleton that manages the service principal's\n * WorkspaceClient and workspace ID. WarehouseResource owns warehouse bindings.\n *\n * It's initialized once at app startup and provides the foundation\n * for both service principal and user context execution.\n */\nexport class ServiceContext {\n private static instance: ServiceContextState | null = null;\n private static initPromise: Promise<ServiceContextState> | null = null;\n\n /**\n * Initialize the service context. Should be called once at app startup.\n * Safe to call multiple times - will return the same instance.\n *\n * @param options - A resolved warehouse ID, or a boolean enabling discovery.\n * @param client - Optional pre-configured WorkspaceClient to use instead\n * of creating one from environment credentials.\n */\n static async initialize(\n options?: { warehouseId?: boolean | string },\n client?: WorkspaceClient,\n ): Promise<ServiceContextState> {\n if (ServiceContext.instance) {\n return ServiceContext.instance;\n }\n\n if (ServiceContext.initPromise) {\n return ServiceContext.initPromise;\n }\n\n ServiceContext.initPromise = ServiceContext.createContext(options, client);\n ServiceContext.instance = await ServiceContext.initPromise;\n return ServiceContext.instance;\n }\n\n /**\n * Get the initialized service context.\n * @throws Error if not initialized\n */\n static get(): ServiceContextState {\n if (!ServiceContext.instance) {\n throw InitializationError.notInitialized(\n \"ServiceContext\",\n \"Call ServiceContext.initialize() first\",\n );\n }\n return ServiceContext.instance;\n }\n\n /**\n * Check if the service context has been initialized.\n */\n static isInitialized(): boolean {\n return ServiceContext.instance !== null;\n }\n\n /**\n * Create an immutable caller context from the existing user request headers.\n *\n * @param token - The user's access token from x-forwarded-access-token header\n * @param userId - The user's ID from x-forwarded-user header\n * @param userName - Optional user name\n * @param userEmail - Optional email from x-forwarded-email\n * @throws Error if token is not provided\n */\n static createCallerContext(\n token: string,\n userId: string,\n userName?: string,\n userEmail?: string,\n ): CallerContext {\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n const host = process.env.DATABRICKS_HOST;\n if (!host) {\n throw ConfigurationError.missingEnvVar(\"DATABRICKS_HOST\");\n }\n\n const serviceCtx = ServiceContext.get();\n\n // Create user client with the OAuth token from Databricks Apps\n // Note: We use authType: \"pat\" because the token is passed as a Bearer token\n // just like a PAT, even though it's technically an OAuth token\n const userClient = createWorkspaceClient({\n token,\n host,\n authType: \"pat\",\n clientOptions: getClientOptions(),\n });\n\n const tokenFingerprint = createHash(\"sha256\")\n .update(token)\n .digest(\"hex\")\n .slice(0, 16);\n\n return snapshotCallerContext({\n client: userClient,\n principal: { type: \"user\", userId, userName, userEmail },\n tokenFingerprint,\n workspaceId: serviceCtx.workspaceId,\n });\n }\n\n /** @deprecated Use ServiceContext.createCallerContext. */\n static createUserContext(\n token: string,\n userId: string,\n userName?: string,\n userEmail?: string,\n ): CallerContext & UserContext {\n warnContextDeprecation(\n \"ServiceContext.createUserContext\",\n \"ServiceContext.createCallerContext\",\n );\n const caller = ServiceContext.createCallerContext(\n token,\n userId,\n userName,\n userEmail,\n );\n const warehouseId = WarehouseResource.get()?.warehouseId;\n return legacyUserContext(caller, () => warehouseId);\n }\n\n /**\n * Get the client options for WorkspaceClient.\n * Exposed for testing purposes.\n */\n static getClientOptions(): ClientOptions {\n return getClientOptions();\n }\n\n private static async createContext(\n options?: { warehouseId?: boolean | string },\n client?: WorkspaceClient,\n ): Promise<ServiceContextState> {\n try {\n const wsClient =\n client ?? createWorkspaceClient({ clientOptions: getClientOptions() });\n\n const [resolvedWorkspaceId, currentUser, resolvedResources] =\n await Promise.all([\n ServiceContext.getWorkspaceId(wsClient),\n wsClient.currentUser.me(),\n WarehouseResource.resolve(wsClient, options?.warehouseId),\n ]);\n\n if (!currentUser.id) {\n throw ConfigurationError.resourceNotFound(\"Service user ID\");\n }\n\n const resources = WarehouseResource.bind(resolvedResources);\n\n return Object.freeze({\n client: wsClient,\n serviceUserId: currentUser.id,\n get warehouseId() {\n warnContextDeprecation(\n \"ServiceContextState.warehouseId\",\n \"getWarehouseId() from @databricks/appkit\",\n );\n return resources.warehouseId;\n },\n workspaceId: Promise.resolve(resolvedWorkspaceId),\n });\n } catch (e) {\n if (e instanceof ConfigError) {\n throw ConfigurationError.databricksAuthenticationSetupFailed(\n e.baseMessage,\n { cause: e },\n );\n }\n throw e;\n }\n }\n\n private static async getWorkspaceId(\n client: WorkspaceClient,\n ): Promise<string> {\n if (process.env.DATABRICKS_WORKSPACE_ID) {\n return process.env.DATABRICKS_WORKSPACE_ID;\n }\n\n const response = (await client.apiClient.request({\n path: \"/api/2.0/preview/scim/v2/Me\",\n method: \"GET\",\n headers: new Headers(),\n raw: false,\n query: {},\n responseHeaders: [\"x-databricks-org-id\"],\n })) as { \"x-databricks-org-id\": string };\n\n if (!response[\"x-databricks-org-id\"]) {\n throw ConfigurationError.resourceNotFound(\"Workspace ID\");\n }\n\n return response[\"x-databricks-org-id\"];\n }\n\n /**\n * Reset the service context. Only for testing purposes.\n */\n static reset(): void {\n ServiceContext.instance = null;\n ServiceContext.initPromise = null;\n WarehouseResource.reset();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA4CA,IAAa,iBAAb,MAAa,eAAe;CAC1B,OAAe,WAAuC;CACtD,OAAe,cAAmD;;;;;;;;;CAUlE,aAAa,WACX,SACA,QAC8B;AAC9B,MAAI,eAAe,SACjB,QAAO,eAAe;AAGxB,MAAI,eAAe,YACjB,QAAO,eAAe;AAGxB,iBAAe,cAAc,eAAe,cAAc,SAAS,OAAO;AAC1E,iBAAe,WAAW,MAAM,eAAe;AAC/C,SAAO,eAAe;;;;;;CAOxB,OAAO,MAA2B;AAChC,MAAI,CAAC,eAAe,SAClB,OAAM,oBAAoB,eACxB,kBACA,yCACD;AAEH,SAAO,eAAe;;;;;CAMxB,OAAO,gBAAyB;AAC9B,SAAO,eAAe,aAAa;;;;;;;;;;;CAYrC,OAAO,oBACL,OACA,QACA,UACA,WACe;AACf,MAAI,CAAC,MACH,OAAM,oBAAoB,aAAa,aAAa;EAGtD,MAAM,OAAO,QAAQ,IAAI;AACzB,MAAI,CAAC,KACH,OAAM,mBAAmB,cAAc,kBAAkB;EAG3D,MAAM,aAAa,eAAe,KAAK;EAKvC,MAAM,aAAa,sBAAsB;GACvC;GACA;GACA,UAAU;GACV,eAAe,kBAAkB;GAClC,CAAC;EAEF,MAAM,mBAAmB,WAAW,SAAS,CAC1C,OAAO,MAAM,CACb,OAAO,MAAM,CACb,MAAM,GAAG,GAAG;AAEf,SAAO,sBAAsB;GAC3B,QAAQ;GACR,WAAW;IAAE,MAAM;IAAQ;IAAQ;IAAU;IAAW;GACxD;GACA,aAAa,WAAW;GACzB,CAAC;;;CAIJ,OAAO,kBACL,OACA,QACA,UACA,WAC6B;AAC7B,yBACE,oCACA,qCACD;EACD,MAAM,SAAS,eAAe,oBAC5B,OACA,QACA,UACA,UACD;EACD,MAAM,cAAc,kBAAkB,KAAK,EAAE;AAC7C,SAAO,kBAAkB,cAAc,YAAY;;;;;;CAOrD,OAAO,mBAAkC;AACvC,SAAO,kBAAkB;;CAG3B,aAAqB,cACnB,SACA,QAC8B;AAC9B,MAAI;GACF,MAAM,WACJ,UAAU,sBAAsB,EAAE,eAAe,kBAAkB,EAAE,CAAC;GAExE,MAAM,CAAC,qBAAqB,aAAa,qBACvC,MAAM,QAAQ,IAAI;IAChB,eAAe,eAAe,SAAS;IACvC,SAAS,YAAY,IAAI;IACzB,kBAAkB,QAAQ,UAAU,SAAS,YAAY;IAC1D,CAAC;AAEJ,OAAI,CAAC,YAAY,GACf,OAAM,mBAAmB,iBAAiB,kBAAkB;GAG9D,MAAM,YAAY,kBAAkB,KAAK,kBAAkB;AAE3D,UAAO,OAAO,OAAO;IACnB,QAAQ;IACR,eAAe,YAAY;IAC3B,IAAI,cAAc;AAChB,4BACE,mCACA,2CACD;AACD,YAAO,UAAU;;IAEnB,aAAa,QAAQ,QAAQ,oBAAoB;IAClD,CAAC;WACK,GAAG;AACV,OAAI,aAAa,YACf,OAAM,mBAAmB,oCACvB,EAAE,aACF,EAAE,OAAO,GAAG,CACb;AAEH,SAAM;;;CAIV,aAAqB,eACnB,QACiB;AACjB,MAAI,QAAQ,IAAI,wBACd,QAAO,QAAQ,IAAI;EAGrB,MAAM,WAAY,MAAM,OAAO,UAAU,QAAQ;GAC/C,MAAM;GACN,QAAQ;GACR,SAAS,IAAI,SAAS;GACtB,KAAK;GACL,OAAO,EAAE;GACT,iBAAiB,CAAC,sBAAsB;GACzC,CAAC;AAEF,MAAI,CAAC,SAAS,uBACZ,OAAM,mBAAmB,iBAAiB,eAAe;AAG3D,SAAO,SAAS;;;;;CAMlB,OAAO,QAAc;AACnB,iBAAe,WAAW;AAC1B,iBAAe,cAAc;AAC7B,oBAAkB,OAAO"}
|
|
1
|
+
{"version":3,"file":"service-context.js","names":[],"sources":["../../src/context/service-context.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport {\n AuthenticationError,\n ConfigurationError,\n InitializationError,\n} from \"../errors\";\nimport { WarehouseResource } from \"../resources/warehouse\";\nimport {\n type ClientOptions,\n ConfigError,\n createWorkspaceClient,\n type WorkspaceClient,\n} from \"../workspace-client\";\nimport { type CallerContext, snapshotCallerContext } from \"./caller-context\";\nimport { getClientOptions } from \"./client-options\";\nimport { warnContextDeprecation } from \"./deprecation\";\nimport { legacyUserContext, type UserContext } from \"./user-context\";\n\n/**\n * Service context holds the service principal identity and workspace client.\n * This is initialized once at app startup and shared across all requests.\n */\nexport interface ServiceContextState {\n /** WorkspaceClient authenticated as the service principal */\n readonly client: WorkspaceClient;\n /** The service principal's user ID */\n readonly serviceUserId: string;\n /**\n * @deprecated Use getWarehouseId() from @databricks/appkit.\n * Retained for backward compatibility.\n */\n readonly warehouseId?: Promise<string>;\n /** Promise that resolves to the workspace ID */\n readonly workspaceId: Promise<string>;\n}\n\n/**\n * ServiceContext is a singleton that manages the service principal's\n * WorkspaceClient and workspace ID. WarehouseResource owns warehouse bindings.\n *\n * It's initialized once at app startup and provides the foundation\n * for both service principal and user context execution.\n */\nexport class ServiceContext {\n private static instance: ServiceContextState | null = null;\n private static initPromise: Promise<ServiceContextState> | null = null;\n\n /**\n * Initialize the service context. Should be called once at app startup.\n * Safe to call multiple times - will return the same instance.\n *\n * @param options - A resolved warehouse ID, or a boolean enabling discovery.\n * @param client - Optional pre-configured WorkspaceClient to use instead\n * of creating one from environment credentials.\n */\n static async initialize(\n options?: { warehouseId?: boolean | string },\n client?: WorkspaceClient,\n ): Promise<ServiceContextState> {\n if (ServiceContext.instance) {\n return ServiceContext.instance;\n }\n\n if (ServiceContext.initPromise) {\n return ServiceContext.initPromise;\n }\n\n ServiceContext.initPromise = ServiceContext.createContext(options, client);\n ServiceContext.instance = await ServiceContext.initPromise;\n return ServiceContext.instance;\n }\n\n /**\n * Get the initialized service context.\n * @throws Error if not initialized\n */\n static get(): ServiceContextState {\n if (!ServiceContext.instance) {\n throw InitializationError.notInitialized(\n \"ServiceContext\",\n \"Call ServiceContext.initialize() first\",\n );\n }\n return ServiceContext.instance;\n }\n\n /**\n * Check if the service context has been initialized.\n */\n static isInitialized(): boolean {\n return ServiceContext.instance !== null;\n }\n\n /**\n * Create an immutable caller context from the existing user request headers.\n *\n * @param token - The user's access token from x-forwarded-access-token header\n * @param userId - The user's ID from x-forwarded-user header\n * @param userName - Optional user name\n * @param userEmail - Optional email from x-forwarded-email\n * @throws Error if token is not provided\n */\n static createCallerContext(\n token: string,\n userId: string,\n userName?: string,\n userEmail?: string,\n ): CallerContext {\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n // Local templates can configure only a profile, whose host the SDK resolved.\n const host =\n process.env.DATABRICKS_HOST ||\n (process.env.NODE_ENV === \"development\" && ServiceContext.isInitialized()\n ? ServiceContext.get().client.config?.host\n : undefined);\n if (!host) {\n throw ConfigurationError.missingEnvVar(\"DATABRICKS_HOST\");\n }\n\n const serviceCtx = ServiceContext.get();\n\n // Create user client with the OAuth token from Databricks Apps\n // Note: We use authType: \"pat\" because the token is passed as a Bearer token\n // just like a PAT, even though it's technically an OAuth token\n const userClient = createWorkspaceClient({\n token,\n host,\n authType: \"pat\",\n clientOptions: getClientOptions(),\n });\n\n const tokenFingerprint = createHash(\"sha256\")\n .update(token)\n .digest(\"hex\")\n .slice(0, 16);\n\n return snapshotCallerContext({\n client: userClient,\n principal: { type: \"user\", userId, userName, userEmail },\n tokenFingerprint,\n workspaceId: serviceCtx.workspaceId,\n });\n }\n\n /** @deprecated Use ServiceContext.createCallerContext. */\n static createUserContext(\n token: string,\n userId: string,\n userName?: string,\n userEmail?: string,\n ): CallerContext & UserContext {\n warnContextDeprecation(\n \"ServiceContext.createUserContext\",\n \"ServiceContext.createCallerContext\",\n );\n const caller = ServiceContext.createCallerContext(\n token,\n userId,\n userName,\n userEmail,\n );\n const warehouseId = WarehouseResource.get()?.warehouseId;\n return legacyUserContext(caller, () => warehouseId);\n }\n\n /**\n * Get the client options for WorkspaceClient.\n * Exposed for testing purposes.\n */\n static getClientOptions(): ClientOptions {\n return getClientOptions();\n }\n\n private static async createContext(\n options?: { warehouseId?: boolean | string },\n client?: WorkspaceClient,\n ): Promise<ServiceContextState> {\n try {\n const wsClient =\n client ?? createWorkspaceClient({ clientOptions: getClientOptions() });\n\n const [resolvedWorkspaceId, currentUser, resolvedResources] =\n await Promise.all([\n ServiceContext.getWorkspaceId(wsClient),\n wsClient.currentUser.me(),\n WarehouseResource.resolve(wsClient, options?.warehouseId),\n ]);\n\n if (!currentUser.id) {\n throw ConfigurationError.resourceNotFound(\"Service user ID\");\n }\n\n const resources = WarehouseResource.bind(resolvedResources);\n\n return Object.freeze({\n client: wsClient,\n serviceUserId: currentUser.id,\n get warehouseId() {\n warnContextDeprecation(\n \"ServiceContextState.warehouseId\",\n \"getWarehouseId() from @databricks/appkit\",\n );\n return resources.warehouseId;\n },\n workspaceId: Promise.resolve(resolvedWorkspaceId),\n });\n } catch (e) {\n if (e instanceof ConfigError) {\n throw ConfigurationError.databricksAuthenticationSetupFailed(\n e.baseMessage,\n { cause: e },\n );\n }\n throw e;\n }\n }\n\n private static async getWorkspaceId(\n client: WorkspaceClient,\n ): Promise<string> {\n if (process.env.DATABRICKS_WORKSPACE_ID) {\n return process.env.DATABRICKS_WORKSPACE_ID;\n }\n\n const response = (await client.apiClient.request({\n path: \"/api/2.0/preview/scim/v2/Me\",\n method: \"GET\",\n headers: new Headers(),\n raw: false,\n query: {},\n responseHeaders: [\"x-databricks-org-id\"],\n })) as { \"x-databricks-org-id\": string };\n\n if (!response[\"x-databricks-org-id\"]) {\n throw ConfigurationError.resourceNotFound(\"Workspace ID\");\n }\n\n return response[\"x-databricks-org-id\"];\n }\n\n /**\n * Reset the service context. Only for testing purposes.\n */\n static reset(): void {\n ServiceContext.instance = null;\n ServiceContext.initPromise = null;\n WarehouseResource.reset();\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
import { ServiceContextState } from "./service-context.js";
|
|
2
|
-
import { ExecutionContext } from "./caller-context.js";
|
|
2
|
+
import { CallerContext, ExecutionContext } from "./caller-context.js";
|
|
3
3
|
|
|
4
4
|
//#region src/context/user-context.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* @deprecated Use CallerContext and its principal field. Kept for callers
|
|
7
7
|
* that construct the legacy shape or read its flat identity fields.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
/** WorkspaceClient authenticated as the user */
|
|
11
|
-
|
|
12
|
-
/** The user's
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
userEmail?: string;
|
|
18
|
-
/** Truncated SHA-256 hash of the user's OBO token, used to detect token rotation */
|
|
19
|
-
tokenFingerprint?: string;
|
|
20
|
-
/** @deprecated Use getWarehouseId() from @databricks/appkit. */
|
|
21
|
-
warehouseId?: Promise<string>;
|
|
22
|
-
/** Promise that resolves to the workspace ID (inherited from service context) */
|
|
23
|
-
workspaceId: Promise<string>;
|
|
24
|
-
/** Flag indicating this is a user context */
|
|
9
|
+
type UserContext = {
|
|
10
|
+
/** WorkspaceClient authenticated as the user */client: ServiceContextState["client"]; /** The user's ID (from request headers) */
|
|
11
|
+
userId: string; /** The user's name (from request headers) */
|
|
12
|
+
userName?: string; /** The user's email (from `x-forwarded-email` header) */
|
|
13
|
+
userEmail?: string; /** Truncated SHA-256 hash of the user's OBO token, used to detect token rotation */
|
|
14
|
+
tokenFingerprint?: string; /** @deprecated Use getWarehouseId() from @databricks/appkit. */
|
|
15
|
+
warehouseId?: Promise<string>; /** Promise that resolves to the workspace ID (inherited from service context) */
|
|
16
|
+
workspaceId: Promise<string>; /** Flag indicating this is a user context */
|
|
25
17
|
isUserContext: true;
|
|
26
|
-
}
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated Use isCallerContext. Active caller contexts retain the legacy
|
|
21
|
+
* identity accessors for callers narrowed by this guard.
|
|
22
|
+
*/
|
|
23
|
+
declare function isUserContext(ctx: ExecutionContext | UserContext): ctx is UserContext & Partial<CallerContext>;
|
|
27
24
|
//#endregion
|
|
28
|
-
export { UserContext };
|
|
25
|
+
export { UserContext, isUserContext };
|
|
29
26
|
//# sourceMappingURL=user-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-context.d.ts","names":[],"sources":["../../src/context/user-context.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"user-context.d.ts","names":[],"sources":["../../src/context/user-context.ts"],"mappings":";;;;;AAiBA;;;KAAY,WAAA;EAYI,gDAVd,MAAA,EAAQ,mBAAA,YAYY;EAVpB,MAAA,UAFA;EAIA,QAAA,WAFA;EAIA,SAAA;EAEA,gBAAA,WAEA;EAAA,WAAA,GAAc,OAAA,UAEd;EAAA,WAAA,EAAa,OAAA,UAEb;EAAA,aAAA;AAAA;;;;;iBAkGc,aAAA,CACd,GAAA,EAAK,gBAAA,GAAmB,WAAA,GACvB,GAAA,IAAO,WAAA,GAAc,OAAA,CAAQ,aAAA"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { snapshotCallerContext } from "./caller-context.js";
|
|
1
|
+
import { isCallerContext, snapshotCallerContext } from "./caller-context.js";
|
|
2
2
|
import { warnContextDeprecation } from "./deprecation.js";
|
|
3
3
|
|
|
4
4
|
//#region src/context/user-context.ts
|
|
5
|
+
const immutableCallers = /* @__PURE__ */ new WeakSet();
|
|
5
6
|
function legacyIdentityContext(ctx) {
|
|
7
|
+
if (immutableCallers.has(ctx)) return ctx;
|
|
6
8
|
const caller = snapshotCallerContext(ctx);
|
|
7
9
|
const { principal } = caller;
|
|
8
|
-
|
|
10
|
+
const snapshot = Object.freeze({
|
|
9
11
|
...caller,
|
|
10
12
|
get userId() {
|
|
11
13
|
warnContextDeprecation("UserContext.userId", "CallerContext.principal.userId");
|
|
@@ -24,6 +26,8 @@ function legacyIdentityContext(ctx) {
|
|
|
24
26
|
return true;
|
|
25
27
|
}
|
|
26
28
|
});
|
|
29
|
+
immutableCallers.add(snapshot);
|
|
30
|
+
return snapshot;
|
|
27
31
|
}
|
|
28
32
|
/** Expose the old resource field only through the deprecated context APIs. */
|
|
29
33
|
function legacyUserContext(ctx, resolveWarehouseId) {
|
|
@@ -32,7 +36,30 @@ function legacyUserContext(ctx, resolveWarehouseId) {
|
|
|
32
36
|
return resolveWarehouseId();
|
|
33
37
|
} }, Object.getOwnPropertyDescriptors(legacyIdentityContext(ctx))));
|
|
34
38
|
}
|
|
39
|
+
/** Normalize legacy inputs before opening a caller scope. */
|
|
40
|
+
function toCallerContext(ctx) {
|
|
41
|
+
if ("principal" in ctx) return ctx;
|
|
42
|
+
return {
|
|
43
|
+
client: ctx.client,
|
|
44
|
+
principal: {
|
|
45
|
+
type: "user",
|
|
46
|
+
userId: ctx.userId,
|
|
47
|
+
userName: ctx.userName,
|
|
48
|
+
userEmail: ctx.userEmail
|
|
49
|
+
},
|
|
50
|
+
tokenFingerprint: ctx.tokenFingerprint,
|
|
51
|
+
workspaceId: ctx.workspaceId
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @deprecated Use isCallerContext. Active caller contexts retain the legacy
|
|
56
|
+
* identity accessors for callers narrowed by this guard.
|
|
57
|
+
*/
|
|
58
|
+
function isUserContext(ctx) {
|
|
59
|
+
warnContextDeprecation("isUserContext", "isCallerContext");
|
|
60
|
+
return "principal" in ctx ? isCallerContext(ctx) : "isUserContext" in ctx && ctx.isUserContext === true;
|
|
61
|
+
}
|
|
35
62
|
|
|
36
63
|
//#endregion
|
|
37
|
-
export { legacyUserContext };
|
|
64
|
+
export { isUserContext, legacyUserContext, toCallerContext };
|
|
38
65
|
//# sourceMappingURL=user-context.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-context.js","names":[],"sources":["../../src/context/user-context.ts"],"sourcesContent":["import {\n type CallerContext,\n type ExecutionContext,\n isCallerContext,\n snapshotCallerContext,\n} from \"./caller-context\";\nimport { warnContextDeprecation } from \"./deprecation\";\nimport type { ServiceContextState } from \"./service-context\";\n\nexport type { ExecutionContext } from \"./caller-context\";\n\n/**\n * @deprecated Use CallerContext and its principal field. Kept for callers\n * that construct the legacy shape or read its flat identity fields.\n */\nexport
|
|
1
|
+
{"version":3,"file":"user-context.js","names":[],"sources":["../../src/context/user-context.ts"],"sourcesContent":["import {\n type CallerContext,\n type ExecutionContext,\n isCallerContext,\n snapshotCallerContext,\n} from \"./caller-context\";\nimport { warnContextDeprecation } from \"./deprecation\";\nimport type { ServiceContextState } from \"./service-context\";\n\nexport type { ExecutionContext } from \"./caller-context\";\n\nconst immutableCallers = new WeakSet<object>();\n\n/**\n * @deprecated Use CallerContext and its principal field. Kept for callers\n * that construct the legacy shape or read its flat identity fields.\n */\nexport type UserContext = {\n /** WorkspaceClient authenticated as the user */\n client: ServiceContextState[\"client\"];\n /** The user's ID (from request headers) */\n userId: string;\n /** The user's name (from request headers) */\n userName?: string;\n /** The user's email (from `x-forwarded-email` header) */\n userEmail?: string;\n /** Truncated SHA-256 hash of the user's OBO token, used to detect token rotation */\n tokenFingerprint?: string;\n /** @deprecated Use getWarehouseId() from @databricks/appkit. */\n warehouseId?: Promise<string>;\n /** Promise that resolves to the workspace ID (inherited from service context) */\n workspaceId: Promise<string>;\n /** Flag indicating this is a user context */\n isUserContext: true;\n};\n\n/**\n * @deprecated Use snapshotCallerContext for identity-only snapshots.\n * Retains the legacy fields for existing callers.\n */\nexport function immutableCallerContext(\n ctx: CallerContext,\n): CallerContext & UserContext {\n warnContextDeprecation(\"immutableCallerContext\", \"snapshotCallerContext\");\n return legacyIdentityContext(ctx);\n}\n\nfunction legacyIdentityContext(\n ctx: CallerContext,\n): CallerContext & UserContext {\n if (immutableCallers.has(ctx)) return ctx as CallerContext & UserContext;\n const caller = snapshotCallerContext(ctx);\n const { principal } = caller;\n const snapshot = Object.freeze({\n ...caller,\n get userId() {\n warnContextDeprecation(\n \"UserContext.userId\",\n \"CallerContext.principal.userId\",\n );\n return principal.userId;\n },\n get userName() {\n warnContextDeprecation(\n \"UserContext.userName\",\n \"CallerContext.principal.userName\",\n );\n return principal.userName;\n },\n get userEmail() {\n warnContextDeprecation(\n \"UserContext.userEmail\",\n \"CallerContext.principal.userEmail\",\n );\n return principal.userEmail;\n },\n get isUserContext(): true {\n warnContextDeprecation(\n \"UserContext.isUserContext\",\n \"CallerContext.principal.type\",\n );\n return true;\n },\n });\n immutableCallers.add(snapshot);\n return snapshot;\n}\n\n/** Expose the old resource field only through the deprecated context APIs. */\nexport function legacyUserContext(\n ctx: CallerContext,\n resolveWarehouseId: () => Promise<string> | undefined,\n): CallerContext & UserContext {\n return Object.freeze(\n Object.defineProperties(\n {\n get warehouseId() {\n warnContextDeprecation(\n \"UserContext.warehouseId\",\n \"getWarehouseId() from @databricks/appkit\",\n );\n return resolveWarehouseId();\n },\n },\n Object.getOwnPropertyDescriptors(legacyIdentityContext(ctx)),\n ),\n ) as CallerContext & UserContext;\n}\n\n/** Normalize legacy inputs before opening a caller scope. */\nexport function toCallerContext(\n ctx: CallerContext | UserContext,\n): CallerContext {\n if (\"principal\" in ctx) return ctx;\n return {\n client: ctx.client,\n principal: {\n type: \"user\",\n userId: ctx.userId,\n userName: ctx.userName,\n userEmail: ctx.userEmail,\n },\n tokenFingerprint: ctx.tokenFingerprint,\n workspaceId: ctx.workspaceId,\n };\n}\n\n/**\n * @deprecated Use isCallerContext. Active caller contexts retain the legacy\n * identity accessors for callers narrowed by this guard.\n */\nexport function isUserContext(\n ctx: ExecutionContext | UserContext,\n): ctx is UserContext & Partial<CallerContext> {\n warnContextDeprecation(\"isUserContext\", \"isCallerContext\");\n return \"principal\" in ctx\n ? isCallerContext(ctx)\n : \"isUserContext\" in ctx && ctx.isUserContext === true;\n}\n"],"mappings":";;;;AAWA,MAAM,mCAAmB,IAAI,SAAiB;AAoC9C,SAAS,sBACP,KAC6B;AAC7B,KAAI,iBAAiB,IAAI,IAAI,CAAE,QAAO;CACtC,MAAM,SAAS,sBAAsB,IAAI;CACzC,MAAM,EAAE,cAAc;CACtB,MAAM,WAAW,OAAO,OAAO;EAC7B,GAAG;EACH,IAAI,SAAS;AACX,0BACE,sBACA,iCACD;AACD,UAAO,UAAU;;EAEnB,IAAI,WAAW;AACb,0BACE,wBACA,mCACD;AACD,UAAO,UAAU;;EAEnB,IAAI,YAAY;AACd,0BACE,yBACA,oCACD;AACD,UAAO,UAAU;;EAEnB,IAAI,gBAAsB;AACxB,0BACE,6BACA,+BACD;AACD,UAAO;;EAEV,CAAC;AACF,kBAAiB,IAAI,SAAS;AAC9B,QAAO;;;AAIT,SAAgB,kBACd,KACA,oBAC6B;AAC7B,QAAO,OAAO,OACZ,OAAO,iBACL,EACE,IAAI,cAAc;AAChB,yBACE,2BACA,2CACD;AACD,SAAO,oBAAoB;IAE9B,EACD,OAAO,0BAA0B,sBAAsB,IAAI,CAAC,CAC7D,CACF;;;AAIH,SAAgB,gBACd,KACe;AACf,KAAI,eAAe,IAAK,QAAO;AAC/B,QAAO;EACL,QAAQ,IAAI;EACZ,WAAW;GACT,MAAM;GACN,QAAQ,IAAI;GACZ,UAAU,IAAI;GACd,WAAW,IAAI;GAChB;EACD,kBAAkB,IAAI;EACtB,aAAa,IAAI;EAClB;;;;;;AAOH,SAAgB,cACd,KAC6C;AAC7C,wBAAuB,iBAAiB,kBAAkB;AAC1D,QAAO,eAAe,MAClB,gBAAgB,IAAI,GACpB,mBAAmB,OAAO,IAAI,kBAAkB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-agents.d.ts","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"load-agents.d.ts","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"mappings":";;;;;UAuBU,eAAA;EACR,OAAA,GAAU,IAAA,GAAO,cAAA,KAAmB,MAAA;AAAA;AAAA,UAGrB,WAAA;EAJQ;EAMvB,YAAA,GAAe,YAAA,GAAe,OAAA,CAAQ,YAAA;EALI;EAO1C,cAAA,GAAiB,MAAA,SAAe,SAAA;EAPf;;;;;AAGnB;EAWE,OAAA,GAAU,GAAA,SAAY,eAAA;;;;;;;;EAQtB,UAAA,GAAa,MAAA,SAAe,eAAA;AAAA;AAAA,UAGb,UAAA;EAHI;EAKnB,IAAA,EAAM,MAAA,SAAe,eAAA;EAtBrB;EAwBA,YAAA;AAAA;;;;;;iBAwEc,uBAAA,CAAwB,QAAA;;;;;;;;;iBAiClB,iBAAA,CACpB,QAAA,UACA,GAAA,EAAK,WAAA,GACJ,OAAA,CAAQ,eAAA;;;;;;;;;;;;;;;;;;;iBAgCW,iBAAA,CACpB,GAAA,UACA,GAAA,EAAK,WAAA,GACJ,OAAA,CAAQ,UAAA"}
|
|
@@ -32,7 +32,8 @@ const ALLOWED_KEYS = new Set([
|
|
|
32
32
|
"generationParams",
|
|
33
33
|
"default",
|
|
34
34
|
"baseSystemPrompt",
|
|
35
|
-
"ephemeral"
|
|
35
|
+
"ephemeral",
|
|
36
|
+
"auth"
|
|
36
37
|
]);
|
|
37
38
|
/**
|
|
38
39
|
* Loads a single markdown agent file and resolves its frontmatter against
|
|
@@ -254,6 +255,7 @@ function buildDefinition(name, raw, filePath, ctx) {
|
|
|
254
255
|
const fm = data ?? {};
|
|
255
256
|
const tools = resolveFrontmatterTools(name, fm, filePath, ctx);
|
|
256
257
|
const model = fm.model ?? fm.endpoint ?? ctx.defaultModel;
|
|
258
|
+
if (fm.auth !== void 0 && fm.auth !== "on-behalf-of-user") throw new Error(`Agent '${name}' (${filePath}) has invalid 'auth:' frontmatter: expected "on-behalf-of-user", got ${JSON.stringify(fm.auth)}.`);
|
|
257
259
|
let baseSystemPrompt;
|
|
258
260
|
if (fm.baseSystemPrompt === false) baseSystemPrompt = false;
|
|
259
261
|
else if (typeof fm.baseSystemPrompt === "string") baseSystemPrompt = fm.baseSystemPrompt;
|
|
@@ -267,7 +269,8 @@ function buildDefinition(name, raw, filePath, ctx) {
|
|
|
267
269
|
maxTokens: typeof fm.maxTokens === "number" ? fm.maxTokens : void 0,
|
|
268
270
|
generationParams: parseGenerationParams(fm.generationParams, filePath),
|
|
269
271
|
baseSystemPrompt,
|
|
270
|
-
ephemeral: typeof fm.ephemeral === "boolean" ? fm.ephemeral : void 0
|
|
272
|
+
ephemeral: typeof fm.ephemeral === "boolean" ? fm.ephemeral : void 0,
|
|
273
|
+
auth: fm.auth
|
|
271
274
|
};
|
|
272
275
|
}
|
|
273
276
|
function resolveFrontmatterTools(agentName, fm, filePath, ctx) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-agents.js","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"sourcesContent":["import type { Dirent } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport yaml from \"js-yaml\";\nimport type { AgentAdapter } from \"shared\";\n\nimport type { GenerationParams } from \"../../agents/databricks\";\nimport type {\n AgentDefinition,\n AgentTool,\n BaseSystemPromptOption,\n ToolkitEntry,\n ToolkitOptions,\n} from \"../../core/agent/types\";\nimport { isToolkitEntry } from \"../../core/agent/types\";\nimport { createLogger } from \"../../logging/logger\";\nimport { agentDirNames } from \"./agent-dirs\";\nimport { splitFrontmatter } from \"./frontmatter\";\n\nconst logger = createLogger(\"agents:loader\");\n\ninterface ToolkitProvider {\n toolkit: (opts?: ToolkitOptions) => Record<string, unknown>;\n}\n\nexport interface LoadContext {\n /** Default model when frontmatter has no `endpoint` and the def has no `model`. */\n defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;\n /** Ambient tool library referenced by frontmatter `tools: [key1, key2]`. */\n availableTools?: Record<string, AgentTool>;\n /**\n * Registered plugin toolkits referenced by `plugin:NAME` entries in the\n * unified `tools:` frontmatter list. Keyed by plugin name; each value\n * exposes the same `toolkit(opts?)` surface as the `plugins` argument to\n * `tools(plugins) => Record<...>` in the code form.\n */\n plugins?: Map<string, ToolkitProvider>;\n /**\n * Code-defined agents contributed by `agents({ agents: { ... } })`. The\n * directory loader resolves `agents:` frontmatter references against\n * these alongside sibling markdown files, so a markdown parent can\n * delegate to a code-defined child. Code-defined names win on collision\n * with markdown names, matching the plugin's top-level merge precedence.\n */\n codeAgents?: Record<string, AgentDefinition>;\n}\n\nexport interface LoadResult {\n /** Agent definitions keyed by agent id (directory name under `dir`). */\n defs: Record<string, AgentDefinition>;\n /** First agent with `default: true` frontmatter (sorted id order), or `null`. */\n defaultAgent: string | null;\n}\n\ninterface Frontmatter {\n endpoint?: string;\n model?: string;\n /**\n * Unified tool list. Each entry is one of:\n *\n * - **`plugin:<name>`** (string) — pull every tool from the named plugin.\n * - **`plugin:<name>: [tool1, tool2]`** — pull only the listed tools\n * (shorthand for `{ only: [...] }`).\n * - **`plugin:<name>: { ...ToolkitOptions }`** — pass full\n * `prefix` / `only` / `except` / `rename` options.\n * - **`<key>`** (string, no `plugin:` prefix) — ambient tool name\n * resolved against the `agents({ tools: { ... } })` config.\n *\n * Mirrors the TS function form `tools(plugins) { ... }` where plugin\n * tools and inline tools live in the same record.\n */\n tools?: FrontmatterToolEntry[];\n /**\n * Other agent ids to expose as sub-agents. Each becomes an `agent-<id>`\n * tool at runtime. Resolution happens at directory-load time in\n * {@link loadAgentsFromDir}; the single-file {@link loadAgentFromFile} path\n * rejects non-empty values since there are no siblings to resolve against.\n */\n agents?: string[];\n /**\n * Names of global skills (from the shared `skills/` pool or a catalog\n * volume) to make visible to this agent. Per-agent skills under\n * `<id>/skills/` are always visible and need not be listed here. Ignored\n * when the plugin's `autoInheritSkills` makes every global skill visible.\n */\n skills?: string[];\n maxSteps?: number;\n maxTokens?: number;\n /**\n * Optional OpenAI-compatible generation params forwarded to the serving\n * request body (`temperature`, `top_p`, `stop`, `frequency_penalty`,\n * `presence_penalty`). Parsed defensively in {@link buildDefinition}.\n */\n generationParams?: Record<string, unknown>;\n default?: boolean;\n baseSystemPrompt?: false | string;\n ephemeral?: boolean;\n}\n\n/**\n * Each item in {@link Frontmatter.tools}. Strings are either ambient tool\n * names (no prefix) or bare plugin references (`plugin:NAME`). Objects are\n * single-key mappings whose key is `plugin:NAME` and whose value is either\n * an array of local tool names (sugar for `{ only: [...] }`) or a full\n * `ToolkitOptions` record.\n *\n * Named `FrontmatterToolEntry` to avoid colliding with the exported\n * `ToolEntry` from `tools/define-tool.ts` — that is the plugin-author API\n * surface (`defineTool({ ... }) : ToolEntry`); this is the frontmatter\n * parse type. They are unrelated and live in different layers.\n */\ntype FrontmatterToolEntry =\n | string\n | { [key: string]: ToolkitOptions | string[] };\n\nconst PLUGIN_PREFIX = \"plugin:\";\n\n/**\n * Derives the logical agent id from a markdown path. When the file is named\n * `agent.md`, the id is the parent directory name (folder-based layout);\n * otherwise the id is the file stem (e.g. legacy single-file paths).\n */\nexport function agentIdFromMarkdownPath(filePath: string): string {\n const normalized = path.normalize(filePath);\n const base = path.basename(normalized);\n const parent = path.basename(path.dirname(normalized));\n if (base === \"agent.md\" && parent && parent !== \".\" && parent !== \"..\") {\n return parent;\n }\n return path.basename(normalized, \".md\");\n}\n\nconst ALLOWED_KEYS = new Set([\n \"endpoint\",\n \"model\",\n \"tools\",\n \"agents\",\n \"skills\",\n \"maxSteps\",\n \"maxTokens\",\n \"generationParams\",\n \"default\",\n \"baseSystemPrompt\",\n \"ephemeral\",\n]);\n\n/**\n * Loads a single markdown agent file and resolves its frontmatter against\n * registered plugin toolkits + ambient tool library.\n *\n * Rejects non-empty `agents:` frontmatter because single-file loads have\n * no siblings to resolve sub-agent references against — callers must use\n * {@link loadAgentsFromDir} when markdown agents delegate to one another.\n */\nexport async function loadAgentFromFile(\n filePath: string,\n ctx: LoadContext,\n): Promise<AgentDefinition> {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const name = agentIdFromMarkdownPath(filePath);\n const { data } = parseFrontmatter(raw, filePath);\n if (Array.isArray(data?.agents) && data.agents.length > 0) {\n throw new Error(\n `Agent '${name}' (${filePath}) declares 'agents:' in frontmatter, ` +\n `which requires loadAgentsFromDir to resolve sibling references. ` +\n `Use loadAgentsFromDir, or wire sub-agents in code via createAgent({ agents: { ... } }).`,\n );\n }\n return buildDefinition(name, raw, filePath, ctx);\n}\n\n/**\n * Scans a directory for one subdirectory per agent, each containing\n * `agent.md` (frontmatter + body). Produces an `AgentDefinition` record keyed\n * by agent id (folder name). Throws on frontmatter errors or unresolved\n * references. Returns an empty map if the directory does not exist.\n *\n * Legacy top-level `*.md` files are rejected with an error — migrate each to\n * `<id>/agent.md` under a sibling folder named for the agent id.\n *\n * Runs in two passes so sub-agent references in frontmatter (`agents: [...]`)\n * can be resolved regardless of directory iteration order:\n *\n * 1. Build every agent's definition from its own `agent.md`.\n * 2. Walk `agents:` references and wire `def.agents = { child: childDef }`\n * by looking them up in the complete map. Dangling names and\n * self-references fail loudly; mutual delegation is allowed and bounded\n * at runtime by `limits.maxSubAgentDepth`.\n */\nexport async function loadAgentsFromDir(\n dir: string,\n ctx: LoadContext,\n): Promise<LoadResult> {\n let entries: Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { defs: {}, defaultAgent: null };\n }\n throw err;\n }\n const orphanMd = entries\n .filter((e) => e.isFile() && e.name.endsWith(\".md\"))\n .map((e) => e.name)\n .sort();\n\n if (orphanMd.length > 0) {\n const hint = orphanMd\n .map((f) => `${path.basename(f, \".md\")}/agent.md`)\n .join(\", \");\n throw new Error(\n `Agents directory contains unsupported top-level markdown file(s): ${orphanMd.join(\", \")}. ` +\n `Use one folder per agent with a fixed entry file, e.g. ${hint}.`,\n );\n }\n\n // A symlink to a file is filtered out below when reading agent.md (ENOTDIR).\n const agentIds = agentDirNames(entries);\n\n const defs: Record<string, AgentDefinition> = {};\n const subAgentRefs: Record<string, string[]> = {};\n let defaultAgent: string | null = null;\n\n // Pass 1: build every agent's definition; collect sub-agent refs.\n for (const id of agentIds) {\n const agentPath = path.join(dir, id, \"agent.md\");\n let raw: string;\n try {\n raw = await fs.readFile(agentPath, \"utf-8\");\n } catch (err) {\n // No agent.md → a code-agent folder (agent.ts) or an asset dir (skills/);\n // ENOTDIR → the entry is a symlink to a file, not an agent folder.\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\" || code === \"ENOTDIR\") continue;\n throw err;\n }\n defs[id] = buildDefinition(id, raw, agentPath, ctx);\n const { data } = parseFrontmatter(raw, agentPath);\n if (data?.agents !== undefined) {\n subAgentRefs[id] = normalizeAgentsFrontmatter(data.agents, id, agentPath);\n }\n if (data?.default === true && !defaultAgent) {\n defaultAgent = id;\n }\n }\n\n // Pass 2: resolve sub-agent references against the complete defs map.\n // Code-defined agents (ctx.codeAgents) take precedence over markdown ones\n // with the same name, matching the plugin's top-level merge behaviour.\n for (const [name, refs] of Object.entries(subAgentRefs)) {\n if (refs.length === 0) continue;\n const children: Record<string, AgentDefinition> = {};\n const missing: string[] = [];\n for (const ref of refs) {\n if (ref === name) {\n throw new Error(\n `Agent '${name}' (${path.join(dir, name, \"agent.md\")}) cannot reference itself in 'agents:'.`,\n );\n }\n const sibling = ctx.codeAgents?.[ref] ?? defs[ref];\n if (!sibling) {\n missing.push(ref);\n continue;\n }\n children[ref] = sibling;\n }\n if (missing.length > 0) {\n const available =\n [...Object.keys(ctx.codeAgents ?? {}), ...Object.keys(defs)]\n .sort()\n .join(\", \") || \"<none>\";\n throw new Error(\n `Agent '${name}' references sub-agent(s) '${missing.join(\", \")}' in 'agents:', ` +\n `but no markdown or code agent(s) with those names exist. ` +\n `Available: ${available}.`,\n );\n }\n defs[name].agents = children;\n }\n\n return { defs, defaultAgent };\n}\n\n/**\n * Validates that `agents:` frontmatter is an array of non-empty strings and\n * returns it with duplicates removed. Throws with a clear per-file message\n * on malformed input rather than silently ignoring.\n */\nfunction normalizeAgentsFrontmatter(\n value: unknown,\n agentName: string,\n filePath: string,\n): string[] {\n if (!Array.isArray(value)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' frontmatter: ` +\n `expected an array of sibling agent ids, got ${typeof value}.`,\n );\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const item of value) {\n if (typeof item !== \"string\" || item.trim() === \"\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' entry: ` +\n `expected non-empty string, got ${JSON.stringify(item)}.`,\n );\n }\n if (seen.has(item)) continue;\n seen.add(item);\n out.push(item);\n }\n return out;\n}\n\n/** Exposed for tests. Parses `--- yaml ---\\nbody` and validates frontmatter keys. */\nexport function parseFrontmatter(\n raw: string,\n sourcePath?: string,\n): { data: Frontmatter | null; content: string } {\n const { yaml: yamlBlock, body } = splitFrontmatter(raw);\n if (yamlBlock === null) {\n return { data: null, content: body };\n }\n let parsed: unknown;\n try {\n parsed = yaml.load(yamlBlock);\n } catch (err) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(\n `Invalid YAML frontmatter${src}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n if (parsed === null || parsed === undefined) {\n return { data: {}, content: body };\n }\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(`Frontmatter must be a YAML object${src}`);\n }\n const data = parsed as Record<string, unknown>;\n for (const key of Object.keys(data)) {\n if (!ALLOWED_KEYS.has(key)) {\n logger.warn(\n \"Ignoring unknown frontmatter key '%s' in %s\",\n key,\n sourcePath ?? \"<inline>\",\n );\n }\n }\n return { data: data as Frontmatter, content: body };\n}\n\nconst isNumber = (v: unknown): v is number => typeof v === \"number\";\n\n/**\n * Per-key validators for {@link GenerationParams} frontmatter. Keyed by wire\n * name and typed as `Record<keyof GenerationParams, ...>`, so a new param on\n * the interface is a compile error until it gets an entry here — parsing,\n * unknown-key detection, and warnings all derive from this one table.\n */\nconst GENERATION_PARAM_SPECS: Record<\n keyof GenerationParams,\n { label: string; valid: (v: unknown) => boolean }\n> = {\n temperature: { label: \"number\", valid: isNumber },\n top_p: { label: \"number\", valid: isNumber },\n frequency_penalty: { label: \"number\", valid: isNumber },\n presence_penalty: { label: \"number\", valid: isNumber },\n stop: {\n label: \"string or string[]\",\n valid: (v) =>\n typeof v === \"string\" ||\n (Array.isArray(v) && v.every((s) => typeof s === \"string\")),\n },\n};\n\n/**\n * Defensively maps a frontmatter `generationParams` map to {@link GenerationParams}.\n * Picks only known keys with the expected wire types. A key present with the\n * wrong type (e.g. `temperature: \"0.5\"`) or an unknown key (e.g. `top-p`) is\n * dropped and logged at warn level, so a silently-ignored param is visible\n * rather than mistaken for \"applied\". Returns `undefined` when no valid key is\n * present.\n */\nfunction parseGenerationParams(\n value: unknown,\n sourcePath?: string,\n): GenerationParams | undefined {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return undefined;\n }\n const raw = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n const where = sourcePath ?? \"<inline>\";\n\n for (const [key, v] of Object.entries(raw)) {\n const spec = GENERATION_PARAM_SPECS[key as keyof GenerationParams];\n if (!spec) {\n logger.warn(\n \"Ignoring unknown generationParams key '%s' in %s\",\n key,\n where,\n );\n } else if (spec.valid(v)) {\n out[key] = v;\n } else {\n logger.warn(\n \"Ignoring generationParams.%s in %s: expected %s, got %s\",\n key,\n where,\n spec.label,\n typeof v,\n );\n }\n }\n\n return Object.keys(out).length > 0 ? (out as GenerationParams) : undefined;\n}\n\n/**\n * Defensively parses a frontmatter `skills:` list into deduped skill names.\n * Non-array values and non-string/empty entries are dropped with a warning,\n * so a malformed list is visible rather than silently applied. Returns\n * `undefined` when nothing valid is present.\n */\nfunction parseSkillsFrontmatter(\n value: unknown,\n sourcePath?: string,\n): string[] | undefined {\n if (value === undefined) return undefined;\n const where = sourcePath ?? \"<inline>\";\n if (!Array.isArray(value)) {\n logger.warn(\n \"Ignoring 'skills' in %s: expected an array of skill names\",\n where,\n );\n return undefined;\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const item of value) {\n if (typeof item !== \"string\" || item.trim() === \"\") {\n logger.warn(\n \"Ignoring invalid 'skills' entry in %s: %s\",\n where,\n JSON.stringify(item),\n );\n continue;\n }\n const name = item.trim();\n if (seen.has(name)) continue;\n seen.add(name);\n out.push(name);\n }\n return out.length > 0 ? out : undefined;\n}\n\nfunction buildDefinition(\n name: string,\n raw: string,\n filePath: string,\n ctx: LoadContext,\n): AgentDefinition {\n const { data, content } = parseFrontmatter(raw, filePath);\n const fm: Frontmatter = data ?? {};\n\n const tools = resolveFrontmatterTools(name, fm, filePath, ctx);\n const model = fm.model ?? fm.endpoint ?? ctx.defaultModel;\n\n let baseSystemPrompt: BaseSystemPromptOption | undefined;\n if (fm.baseSystemPrompt === false) baseSystemPrompt = false;\n else if (typeof fm.baseSystemPrompt === \"string\")\n baseSystemPrompt = fm.baseSystemPrompt;\n\n return {\n name,\n instructions: content,\n model,\n tools: Object.keys(tools).length > 0 ? tools : undefined,\n skills: parseSkillsFrontmatter(fm.skills, filePath),\n maxSteps: typeof fm.maxSteps === \"number\" ? fm.maxSteps : undefined,\n maxTokens: typeof fm.maxTokens === \"number\" ? fm.maxTokens : undefined,\n generationParams: parseGenerationParams(fm.generationParams, filePath),\n baseSystemPrompt,\n ephemeral: typeof fm.ephemeral === \"boolean\" ? fm.ephemeral : undefined,\n };\n}\n\nfunction resolveFrontmatterTools(\n agentName: string,\n fm: Frontmatter,\n filePath: string,\n ctx: LoadContext,\n): Record<string, AgentTool> {\n const out: Record<string, AgentTool> = {};\n const pluginIdx = ctx.plugins ?? new Map<string, ToolkitProvider>();\n\n for (const entry of fm.tools ?? []) {\n const parsed = parseToolEntry(entry, filePath, agentName);\n if (parsed.kind === \"plugin\") {\n const provider = pluginIdx.get(parsed.pluginName);\n if (!provider) {\n const available =\n pluginIdx.size > 0\n ? Array.from(pluginIdx.keys()).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references 'plugin:${parsed.pluginName}', but plugin '${parsed.pluginName}' is not registered. Available: ${available}`,\n );\n }\n const entries = provider.toolkit(parsed.opts) as Record<string, unknown>;\n for (const [key, value] of Object.entries(entries)) {\n if (!isToolkitEntry(value)) {\n throw new Error(\n `Plugin '${parsed.pluginName}'.toolkit() returned a value at key '${key}' that is not a ToolkitEntry`,\n );\n }\n out[key] = value as ToolkitEntry;\n }\n } else {\n const tool = ctx.availableTools?.[parsed.toolName];\n if (!tool) {\n const available = ctx.availableTools\n ? Object.keys(ctx.availableTools).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references ambient tool '${parsed.toolName}', which is not in the agents() plugin's tools field. Available: ${available}. ` +\n \"If you meant to reference a plugin, use the 'plugin:NAME' prefix.\",\n );\n }\n out[parsed.toolName] = tool;\n }\n }\n\n return out;\n}\n\ntype ParsedToolEntry =\n | { kind: \"plugin\"; pluginName: string; opts: ToolkitOptions | undefined }\n | { kind: \"ambient\"; toolName: string };\n\n/**\n * Classify one item in the `tools:` frontmatter list into either a plugin\n * reference (with optional ToolkitOptions) or an ambient tool lookup.\n *\n * Strings starting with `plugin:` are bare plugin references. Strings\n * without the prefix are ambient tool names. Object entries are\n * single-key mappings keyed by `plugin:NAME`; the value is either an\n * array (sugar for `{ only: [...] }`) or a full `ToolkitOptions` record.\n */\nfunction parseToolEntry(\n entry: FrontmatterToolEntry,\n filePath: string,\n agentName: string,\n): ParsedToolEntry {\n if (typeof entry === \"string\") {\n if (entry.startsWith(PLUGIN_PREFIX)) {\n const pluginName = entry.slice(PLUGIN_PREFIX.length);\n if (pluginName.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n return { kind: \"plugin\", pluginName, opts: undefined };\n }\n if (entry.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty string in 'tools:'.`,\n );\n }\n return { kind: \"ambient\", toolName: entry };\n }\n if (typeof entry !== \"object\" || entry === null) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'tools:' entry: ${JSON.stringify(entry)}`,\n );\n }\n const keys = Object.keys(entry);\n if (keys.length !== 1) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'tools:' object entry must have exactly one key, got: ${keys.join(\", \")}`,\n );\n }\n const key = keys[0];\n // Bare `- plugin:` (no name after the colon) parses as a mapping with the\n // key `\"plugin\"`. Catch that as a friendly error rather than dumping it\n // through the generic \"expected key 'plugin:NAME'\" branch.\n if (key === \"plugin\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n if (!key.startsWith(PLUGIN_PREFIX)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'tools:' object entries are reserved for plugin references; expected key 'plugin:NAME', got '${key}'. ` +\n \"Use a bare string for ambient tools (e.g. `- get_weather`).\",\n );\n }\n const pluginName = key.slice(PLUGIN_PREFIX.length);\n if (pluginName.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n const value = entry[key];\n if (Array.isArray(value)) {\n return { kind: \"plugin\", pluginName, opts: { only: value } };\n }\n if (typeof value === \"object\" && value !== null) {\n return {\n kind: \"plugin\",\n pluginName,\n opts: value as ToolkitOptions,\n };\n }\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'plugin:${pluginName}' options must be an array of tool names or a ToolkitOptions object.`,\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"load-agents.js","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"sourcesContent":["import type { Dirent } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport yaml from \"js-yaml\";\nimport type { AgentAdapter } from \"shared\";\n\nimport type { GenerationParams } from \"../../agents/databricks\";\nimport type {\n AgentAuth,\n AgentDefinition,\n AgentTool,\n BaseSystemPromptOption,\n ToolkitEntry,\n ToolkitOptions,\n} from \"../../core/agent/types\";\nimport { isToolkitEntry } from \"../../core/agent/types\";\nimport { createLogger } from \"../../logging/logger\";\nimport { agentDirNames } from \"./agent-dirs\";\nimport { splitFrontmatter } from \"./frontmatter\";\n\nconst logger = createLogger(\"agents:loader\");\n\ninterface ToolkitProvider {\n toolkit: (opts?: ToolkitOptions) => Record<string, unknown>;\n}\n\nexport interface LoadContext {\n /** Default model when frontmatter has no `endpoint` and the def has no `model`. */\n defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;\n /** Ambient tool library referenced by frontmatter `tools: [key1, key2]`. */\n availableTools?: Record<string, AgentTool>;\n /**\n * Registered plugin toolkits referenced by `plugin:NAME` entries in the\n * unified `tools:` frontmatter list. Keyed by plugin name; each value\n * exposes the same `toolkit(opts?)` surface as the `plugins` argument to\n * `tools(plugins) => Record<...>` in the code form.\n */\n plugins?: Map<string, ToolkitProvider>;\n /**\n * Code-defined agents contributed by `agents({ agents: { ... } })`. The\n * directory loader resolves `agents:` frontmatter references against\n * these alongside sibling markdown files, so a markdown parent can\n * delegate to a code-defined child. Code-defined names win on collision\n * with markdown names, matching the plugin's top-level merge precedence.\n */\n codeAgents?: Record<string, AgentDefinition>;\n}\n\nexport interface LoadResult {\n /** Agent definitions keyed by agent id (directory name under `dir`). */\n defs: Record<string, AgentDefinition>;\n /** First agent with `default: true` frontmatter (sorted id order), or `null`. */\n defaultAgent: string | null;\n}\n\ninterface Frontmatter {\n endpoint?: string;\n model?: string;\n /**\n * Unified tool list. Each entry is one of:\n *\n * - **`plugin:<name>`** (string) — pull every tool from the named plugin.\n * - **`plugin:<name>: [tool1, tool2]`** — pull only the listed tools\n * (shorthand for `{ only: [...] }`).\n * - **`plugin:<name>: { ...ToolkitOptions }`** — pass full\n * `prefix` / `only` / `except` / `rename` options.\n * - **`<key>`** (string, no `plugin:` prefix) — ambient tool name\n * resolved against the `agents({ tools: { ... } })` config.\n *\n * Mirrors the TS function form `tools(plugins) { ... }` where plugin\n * tools and inline tools live in the same record.\n */\n tools?: FrontmatterToolEntry[];\n /**\n * Other agent ids to expose as sub-agents. Each becomes an `agent-<id>`\n * tool at runtime. Resolution happens at directory-load time in\n * {@link loadAgentsFromDir}; the single-file {@link loadAgentFromFile} path\n * rejects non-empty values since there are no siblings to resolve against.\n */\n agents?: string[];\n /**\n * Names of global skills (from the shared `skills/` pool or a catalog\n * volume) to make visible to this agent. Per-agent skills under\n * `<id>/skills/` are always visible and need not be listed here. Ignored\n * when the plugin's `autoInheritSkills` makes every global skill visible.\n */\n skills?: string[];\n maxSteps?: number;\n maxTokens?: number;\n /**\n * Optional OpenAI-compatible generation params forwarded to the serving\n * request body (`temperature`, `top_p`, `stop`, `frequency_penalty`,\n * `presence_penalty`). Parsed defensively in {@link buildDefinition}.\n */\n generationParams?: Record<string, unknown>;\n default?: boolean;\n baseSystemPrompt?: false | string;\n ephemeral?: boolean;\n auth?: AgentAuth;\n}\n\n/**\n * Each item in {@link Frontmatter.tools}. Strings are either ambient tool\n * names (no prefix) or bare plugin references (`plugin:NAME`). Objects are\n * single-key mappings whose key is `plugin:NAME` and whose value is either\n * an array of local tool names (sugar for `{ only: [...] }`) or a full\n * `ToolkitOptions` record.\n *\n * Named `FrontmatterToolEntry` to avoid colliding with the exported\n * `ToolEntry` from `tools/define-tool.ts` — that is the plugin-author API\n * surface (`defineTool({ ... }) : ToolEntry`); this is the frontmatter\n * parse type. They are unrelated and live in different layers.\n */\ntype FrontmatterToolEntry =\n | string\n | { [key: string]: ToolkitOptions | string[] };\n\nconst PLUGIN_PREFIX = \"plugin:\";\n\n/**\n * Derives the logical agent id from a markdown path. When the file is named\n * `agent.md`, the id is the parent directory name (folder-based layout);\n * otherwise the id is the file stem (e.g. legacy single-file paths).\n */\nexport function agentIdFromMarkdownPath(filePath: string): string {\n const normalized = path.normalize(filePath);\n const base = path.basename(normalized);\n const parent = path.basename(path.dirname(normalized));\n if (base === \"agent.md\" && parent && parent !== \".\" && parent !== \"..\") {\n return parent;\n }\n return path.basename(normalized, \".md\");\n}\n\nconst ALLOWED_KEYS = new Set([\n \"endpoint\",\n \"model\",\n \"tools\",\n \"agents\",\n \"skills\",\n \"maxSteps\",\n \"maxTokens\",\n \"generationParams\",\n \"default\",\n \"baseSystemPrompt\",\n \"ephemeral\",\n \"auth\",\n]);\n\n/**\n * Loads a single markdown agent file and resolves its frontmatter against\n * registered plugin toolkits + ambient tool library.\n *\n * Rejects non-empty `agents:` frontmatter because single-file loads have\n * no siblings to resolve sub-agent references against — callers must use\n * {@link loadAgentsFromDir} when markdown agents delegate to one another.\n */\nexport async function loadAgentFromFile(\n filePath: string,\n ctx: LoadContext,\n): Promise<AgentDefinition> {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const name = agentIdFromMarkdownPath(filePath);\n const { data } = parseFrontmatter(raw, filePath);\n if (Array.isArray(data?.agents) && data.agents.length > 0) {\n throw new Error(\n `Agent '${name}' (${filePath}) declares 'agents:' in frontmatter, ` +\n `which requires loadAgentsFromDir to resolve sibling references. ` +\n `Use loadAgentsFromDir, or wire sub-agents in code via createAgent({ agents: { ... } }).`,\n );\n }\n return buildDefinition(name, raw, filePath, ctx);\n}\n\n/**\n * Scans a directory for one subdirectory per agent, each containing\n * `agent.md` (frontmatter + body). Produces an `AgentDefinition` record keyed\n * by agent id (folder name). Throws on frontmatter errors or unresolved\n * references. Returns an empty map if the directory does not exist.\n *\n * Legacy top-level `*.md` files are rejected with an error — migrate each to\n * `<id>/agent.md` under a sibling folder named for the agent id.\n *\n * Runs in two passes so sub-agent references in frontmatter (`agents: [...]`)\n * can be resolved regardless of directory iteration order:\n *\n * 1. Build every agent's definition from its own `agent.md`.\n * 2. Walk `agents:` references and wire `def.agents = { child: childDef }`\n * by looking them up in the complete map. Dangling names and\n * self-references fail loudly; mutual delegation is allowed and bounded\n * at runtime by `limits.maxSubAgentDepth`.\n */\nexport async function loadAgentsFromDir(\n dir: string,\n ctx: LoadContext,\n): Promise<LoadResult> {\n let entries: Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { defs: {}, defaultAgent: null };\n }\n throw err;\n }\n const orphanMd = entries\n .filter((e) => e.isFile() && e.name.endsWith(\".md\"))\n .map((e) => e.name)\n .sort();\n\n if (orphanMd.length > 0) {\n const hint = orphanMd\n .map((f) => `${path.basename(f, \".md\")}/agent.md`)\n .join(\", \");\n throw new Error(\n `Agents directory contains unsupported top-level markdown file(s): ${orphanMd.join(\", \")}. ` +\n `Use one folder per agent with a fixed entry file, e.g. ${hint}.`,\n );\n }\n\n // A symlink to a file is filtered out below when reading agent.md (ENOTDIR).\n const agentIds = agentDirNames(entries);\n\n const defs: Record<string, AgentDefinition> = {};\n const subAgentRefs: Record<string, string[]> = {};\n let defaultAgent: string | null = null;\n\n // Pass 1: build every agent's definition; collect sub-agent refs.\n for (const id of agentIds) {\n const agentPath = path.join(dir, id, \"agent.md\");\n let raw: string;\n try {\n raw = await fs.readFile(agentPath, \"utf-8\");\n } catch (err) {\n // No agent.md → a code-agent folder (agent.ts) or an asset dir (skills/);\n // ENOTDIR → the entry is a symlink to a file, not an agent folder.\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\" || code === \"ENOTDIR\") continue;\n throw err;\n }\n defs[id] = buildDefinition(id, raw, agentPath, ctx);\n const { data } = parseFrontmatter(raw, agentPath);\n if (data?.agents !== undefined) {\n subAgentRefs[id] = normalizeAgentsFrontmatter(data.agents, id, agentPath);\n }\n if (data?.default === true && !defaultAgent) {\n defaultAgent = id;\n }\n }\n\n // Pass 2: resolve sub-agent references against the complete defs map.\n // Code-defined agents (ctx.codeAgents) take precedence over markdown ones\n // with the same name, matching the plugin's top-level merge behaviour.\n for (const [name, refs] of Object.entries(subAgentRefs)) {\n if (refs.length === 0) continue;\n const children: Record<string, AgentDefinition> = {};\n const missing: string[] = [];\n for (const ref of refs) {\n if (ref === name) {\n throw new Error(\n `Agent '${name}' (${path.join(dir, name, \"agent.md\")}) cannot reference itself in 'agents:'.`,\n );\n }\n const sibling = ctx.codeAgents?.[ref] ?? defs[ref];\n if (!sibling) {\n missing.push(ref);\n continue;\n }\n children[ref] = sibling;\n }\n if (missing.length > 0) {\n const available =\n [...Object.keys(ctx.codeAgents ?? {}), ...Object.keys(defs)]\n .sort()\n .join(\", \") || \"<none>\";\n throw new Error(\n `Agent '${name}' references sub-agent(s) '${missing.join(\", \")}' in 'agents:', ` +\n `but no markdown or code agent(s) with those names exist. ` +\n `Available: ${available}.`,\n );\n }\n defs[name].agents = children;\n }\n\n return { defs, defaultAgent };\n}\n\n/**\n * Validates that `agents:` frontmatter is an array of non-empty strings and\n * returns it with duplicates removed. Throws with a clear per-file message\n * on malformed input rather than silently ignoring.\n */\nfunction normalizeAgentsFrontmatter(\n value: unknown,\n agentName: string,\n filePath: string,\n): string[] {\n if (!Array.isArray(value)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' frontmatter: ` +\n `expected an array of sibling agent ids, got ${typeof value}.`,\n );\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const item of value) {\n if (typeof item !== \"string\" || item.trim() === \"\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' entry: ` +\n `expected non-empty string, got ${JSON.stringify(item)}.`,\n );\n }\n if (seen.has(item)) continue;\n seen.add(item);\n out.push(item);\n }\n return out;\n}\n\n/** Exposed for tests. Parses `--- yaml ---\\nbody` and validates frontmatter keys. */\nexport function parseFrontmatter(\n raw: string,\n sourcePath?: string,\n): { data: Frontmatter | null; content: string } {\n const { yaml: yamlBlock, body } = splitFrontmatter(raw);\n if (yamlBlock === null) {\n return { data: null, content: body };\n }\n let parsed: unknown;\n try {\n parsed = yaml.load(yamlBlock);\n } catch (err) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(\n `Invalid YAML frontmatter${src}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n if (parsed === null || parsed === undefined) {\n return { data: {}, content: body };\n }\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(`Frontmatter must be a YAML object${src}`);\n }\n const data = parsed as Record<string, unknown>;\n for (const key of Object.keys(data)) {\n if (!ALLOWED_KEYS.has(key)) {\n logger.warn(\n \"Ignoring unknown frontmatter key '%s' in %s\",\n key,\n sourcePath ?? \"<inline>\",\n );\n }\n }\n return { data: data as Frontmatter, content: body };\n}\n\nconst isNumber = (v: unknown): v is number => typeof v === \"number\";\n\n/**\n * Per-key validators for {@link GenerationParams} frontmatter. Keyed by wire\n * name and typed as `Record<keyof GenerationParams, ...>`, so a new param on\n * the interface is a compile error until it gets an entry here — parsing,\n * unknown-key detection, and warnings all derive from this one table.\n */\nconst GENERATION_PARAM_SPECS: Record<\n keyof GenerationParams,\n { label: string; valid: (v: unknown) => boolean }\n> = {\n temperature: { label: \"number\", valid: isNumber },\n top_p: { label: \"number\", valid: isNumber },\n frequency_penalty: { label: \"number\", valid: isNumber },\n presence_penalty: { label: \"number\", valid: isNumber },\n stop: {\n label: \"string or string[]\",\n valid: (v) =>\n typeof v === \"string\" ||\n (Array.isArray(v) && v.every((s) => typeof s === \"string\")),\n },\n};\n\n/**\n * Defensively maps a frontmatter `generationParams` map to {@link GenerationParams}.\n * Picks only known keys with the expected wire types. A key present with the\n * wrong type (e.g. `temperature: \"0.5\"`) or an unknown key (e.g. `top-p`) is\n * dropped and logged at warn level, so a silently-ignored param is visible\n * rather than mistaken for \"applied\". Returns `undefined` when no valid key is\n * present.\n */\nfunction parseGenerationParams(\n value: unknown,\n sourcePath?: string,\n): GenerationParams | undefined {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return undefined;\n }\n const raw = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n const where = sourcePath ?? \"<inline>\";\n\n for (const [key, v] of Object.entries(raw)) {\n const spec = GENERATION_PARAM_SPECS[key as keyof GenerationParams];\n if (!spec) {\n logger.warn(\n \"Ignoring unknown generationParams key '%s' in %s\",\n key,\n where,\n );\n } else if (spec.valid(v)) {\n out[key] = v;\n } else {\n logger.warn(\n \"Ignoring generationParams.%s in %s: expected %s, got %s\",\n key,\n where,\n spec.label,\n typeof v,\n );\n }\n }\n\n return Object.keys(out).length > 0 ? (out as GenerationParams) : undefined;\n}\n\n/**\n * Defensively parses a frontmatter `skills:` list into deduped skill names.\n * Non-array values and non-string/empty entries are dropped with a warning,\n * so a malformed list is visible rather than silently applied. Returns\n * `undefined` when nothing valid is present.\n */\nfunction parseSkillsFrontmatter(\n value: unknown,\n sourcePath?: string,\n): string[] | undefined {\n if (value === undefined) return undefined;\n const where = sourcePath ?? \"<inline>\";\n if (!Array.isArray(value)) {\n logger.warn(\n \"Ignoring 'skills' in %s: expected an array of skill names\",\n where,\n );\n return undefined;\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const item of value) {\n if (typeof item !== \"string\" || item.trim() === \"\") {\n logger.warn(\n \"Ignoring invalid 'skills' entry in %s: %s\",\n where,\n JSON.stringify(item),\n );\n continue;\n }\n const name = item.trim();\n if (seen.has(name)) continue;\n seen.add(name);\n out.push(name);\n }\n return out.length > 0 ? out : undefined;\n}\n\nfunction buildDefinition(\n name: string,\n raw: string,\n filePath: string,\n ctx: LoadContext,\n): AgentDefinition {\n const { data, content } = parseFrontmatter(raw, filePath);\n const fm: Frontmatter = data ?? {};\n\n const tools = resolveFrontmatterTools(name, fm, filePath, ctx);\n const model = fm.model ?? fm.endpoint ?? ctx.defaultModel;\n if (fm.auth !== undefined && fm.auth !== \"on-behalf-of-user\") {\n throw new Error(\n `Agent '${name}' (${filePath}) has invalid 'auth:' frontmatter: ` +\n `expected \"on-behalf-of-user\", got ${JSON.stringify(fm.auth)}.`,\n );\n }\n\n let baseSystemPrompt: BaseSystemPromptOption | undefined;\n if (fm.baseSystemPrompt === false) baseSystemPrompt = false;\n else if (typeof fm.baseSystemPrompt === \"string\")\n baseSystemPrompt = fm.baseSystemPrompt;\n\n return {\n name,\n instructions: content,\n model,\n tools: Object.keys(tools).length > 0 ? tools : undefined,\n skills: parseSkillsFrontmatter(fm.skills, filePath),\n maxSteps: typeof fm.maxSteps === \"number\" ? fm.maxSteps : undefined,\n maxTokens: typeof fm.maxTokens === \"number\" ? fm.maxTokens : undefined,\n generationParams: parseGenerationParams(fm.generationParams, filePath),\n baseSystemPrompt,\n ephemeral: typeof fm.ephemeral === \"boolean\" ? fm.ephemeral : undefined,\n auth: fm.auth,\n };\n}\n\nfunction resolveFrontmatterTools(\n agentName: string,\n fm: Frontmatter,\n filePath: string,\n ctx: LoadContext,\n): Record<string, AgentTool> {\n const out: Record<string, AgentTool> = {};\n const pluginIdx = ctx.plugins ?? new Map<string, ToolkitProvider>();\n\n for (const entry of fm.tools ?? []) {\n const parsed = parseToolEntry(entry, filePath, agentName);\n if (parsed.kind === \"plugin\") {\n const provider = pluginIdx.get(parsed.pluginName);\n if (!provider) {\n const available =\n pluginIdx.size > 0\n ? Array.from(pluginIdx.keys()).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references 'plugin:${parsed.pluginName}', but plugin '${parsed.pluginName}' is not registered. Available: ${available}`,\n );\n }\n const entries = provider.toolkit(parsed.opts) as Record<string, unknown>;\n for (const [key, value] of Object.entries(entries)) {\n if (!isToolkitEntry(value)) {\n throw new Error(\n `Plugin '${parsed.pluginName}'.toolkit() returned a value at key '${key}' that is not a ToolkitEntry`,\n );\n }\n out[key] = value as ToolkitEntry;\n }\n } else {\n const tool = ctx.availableTools?.[parsed.toolName];\n if (!tool) {\n const available = ctx.availableTools\n ? Object.keys(ctx.availableTools).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references ambient tool '${parsed.toolName}', which is not in the agents() plugin's tools field. Available: ${available}. ` +\n \"If you meant to reference a plugin, use the 'plugin:NAME' prefix.\",\n );\n }\n out[parsed.toolName] = tool;\n }\n }\n\n return out;\n}\n\ntype ParsedToolEntry =\n | { kind: \"plugin\"; pluginName: string; opts: ToolkitOptions | undefined }\n | { kind: \"ambient\"; toolName: string };\n\n/**\n * Classify one item in the `tools:` frontmatter list into either a plugin\n * reference (with optional ToolkitOptions) or an ambient tool lookup.\n *\n * Strings starting with `plugin:` are bare plugin references. Strings\n * without the prefix are ambient tool names. Object entries are\n * single-key mappings keyed by `plugin:NAME`; the value is either an\n * array (sugar for `{ only: [...] }`) or a full `ToolkitOptions` record.\n */\nfunction parseToolEntry(\n entry: FrontmatterToolEntry,\n filePath: string,\n agentName: string,\n): ParsedToolEntry {\n if (typeof entry === \"string\") {\n if (entry.startsWith(PLUGIN_PREFIX)) {\n const pluginName = entry.slice(PLUGIN_PREFIX.length);\n if (pluginName.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n return { kind: \"plugin\", pluginName, opts: undefined };\n }\n if (entry.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty string in 'tools:'.`,\n );\n }\n return { kind: \"ambient\", toolName: entry };\n }\n if (typeof entry !== \"object\" || entry === null) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'tools:' entry: ${JSON.stringify(entry)}`,\n );\n }\n const keys = Object.keys(entry);\n if (keys.length !== 1) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'tools:' object entry must have exactly one key, got: ${keys.join(\", \")}`,\n );\n }\n const key = keys[0];\n // Bare `- plugin:` (no name after the colon) parses as a mapping with the\n // key `\"plugin\"`. Catch that as a friendly error rather than dumping it\n // through the generic \"expected key 'plugin:NAME'\" branch.\n if (key === \"plugin\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n if (!key.startsWith(PLUGIN_PREFIX)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'tools:' object entries are reserved for plugin references; expected key 'plugin:NAME', got '${key}'. ` +\n \"Use a bare string for ambient tools (e.g. `- get_weather`).\",\n );\n }\n const pluginName = key.slice(PLUGIN_PREFIX.length);\n if (pluginName.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n const value = entry[key];\n if (Array.isArray(value)) {\n return { kind: \"plugin\", pluginName, opts: { only: value } };\n }\n if (typeof value === \"object\" && value !== null) {\n return {\n kind: \"plugin\",\n pluginName,\n opts: value as ToolkitOptions,\n };\n }\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'plugin:${pluginName}' options must be an array of tool names or a ToolkitOptions object.`,\n );\n}\n"],"mappings":";;;;;;;;;AAqBA,MAAM,SAAS,aAAa,gBAAgB;AAiG5C,MAAM,gBAAgB;;;;;;AAOtB,SAAgB,wBAAwB,UAA0B;CAChE,MAAM,aAAa,KAAK,UAAU,SAAS;CAC3C,MAAM,OAAO,KAAK,SAAS,WAAW;CACtC,MAAM,SAAS,KAAK,SAAS,KAAK,QAAQ,WAAW,CAAC;AACtD,KAAI,SAAS,cAAc,UAAU,WAAW,OAAO,WAAW,KAChE,QAAO;AAET,QAAO,KAAK,SAAS,YAAY,MAAM;;AAGzC,MAAM,eAAe,IAAI,IAAI;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;AAUF,eAAsB,kBACpB,UACA,KAC0B;CAC1B,MAAM,MAAM,MAAM,GAAG,SAAS,UAAU,QAAQ;CAChD,MAAM,OAAO,wBAAwB,SAAS;CAC9C,MAAM,EAAE,SAAS,iBAAiB,KAAK,SAAS;AAChD,KAAI,MAAM,QAAQ,MAAM,OAAO,IAAI,KAAK,OAAO,SAAS,EACtD,OAAM,IAAI,MACR,UAAU,KAAK,KAAK,SAAS,8LAG9B;AAEH,QAAO,gBAAgB,MAAM,KAAK,UAAU,IAAI;;;;;;;;;;;;;;;;;;;;AAqBlD,eAAsB,kBACpB,KACA,KACqB;CACrB,IAAI;AACJ,KAAI;AACF,YAAU,MAAM,GAAG,QAAQ,KAAK,EAAE,eAAe,MAAM,CAAC;UACjD,KAAK;AACZ,MAAK,IAA8B,SAAS,SAC1C,QAAO;GAAE,MAAM,EAAE;GAAE,cAAc;GAAM;AAEzC,QAAM;;CAER,MAAM,WAAW,QACd,QAAQ,MAAM,EAAE,QAAQ,IAAI,EAAE,KAAK,SAAS,MAAM,CAAC,CACnD,KAAK,MAAM,EAAE,KAAK,CAClB,MAAM;AAET,KAAI,SAAS,SAAS,GAAG;EACvB,MAAM,OAAO,SACV,KAAK,MAAM,GAAG,KAAK,SAAS,GAAG,MAAM,CAAC,WAAW,CACjD,KAAK,KAAK;AACb,QAAM,IAAI,MACR,qEAAqE,SAAS,KAAK,KAAK,CAAC,2DAC7B,KAAK,GAClE;;CAIH,MAAM,WAAW,cAAc,QAAQ;CAEvC,MAAM,OAAwC,EAAE;CAChD,MAAM,eAAyC,EAAE;CACjD,IAAI,eAA8B;AAGlC,MAAK,MAAM,MAAM,UAAU;EACzB,MAAM,YAAY,KAAK,KAAK,KAAK,IAAI,WAAW;EAChD,IAAI;AACJ,MAAI;AACF,SAAM,MAAM,GAAG,SAAS,WAAW,QAAQ;WACpC,KAAK;GAGZ,MAAM,OAAQ,IAA8B;AAC5C,OAAI,SAAS,YAAY,SAAS,UAAW;AAC7C,SAAM;;AAER,OAAK,MAAM,gBAAgB,IAAI,KAAK,WAAW,IAAI;EACnD,MAAM,EAAE,SAAS,iBAAiB,KAAK,UAAU;AACjD,MAAI,MAAM,WAAW,OACnB,cAAa,MAAM,2BAA2B,KAAK,QAAQ,IAAI,UAAU;AAE3E,MAAI,MAAM,YAAY,QAAQ,CAAC,aAC7B,gBAAe;;AAOnB,MAAK,MAAM,CAAC,MAAM,SAAS,OAAO,QAAQ,aAAa,EAAE;AACvD,MAAI,KAAK,WAAW,EAAG;EACvB,MAAM,WAA4C,EAAE;EACpD,MAAM,UAAoB,EAAE;AAC5B,OAAK,MAAM,OAAO,MAAM;AACtB,OAAI,QAAQ,KACV,OAAM,IAAI,MACR,UAAU,KAAK,KAAK,KAAK,KAAK,KAAK,MAAM,WAAW,CAAC,yCACtD;GAEH,MAAM,UAAU,IAAI,aAAa,QAAQ,KAAK;AAC9C,OAAI,CAAC,SAAS;AACZ,YAAQ,KAAK,IAAI;AACjB;;AAEF,YAAS,OAAO;;AAElB,MAAI,QAAQ,SAAS,GAAG;GACtB,MAAM,YACJ,CAAC,GAAG,OAAO,KAAK,IAAI,cAAc,EAAE,CAAC,EAAE,GAAG,OAAO,KAAK,KAAK,CAAC,CACzD,MAAM,CACN,KAAK,KAAK,IAAI;AACnB,SAAM,IAAI,MACR,UAAU,KAAK,6BAA6B,QAAQ,KAAK,KAAK,CAAC,sFAE/C,UAAU,GAC3B;;AAEH,OAAK,MAAM,SAAS;;AAGtB,QAAO;EAAE;EAAM;EAAc;;;;;;;AAQ/B,SAAS,2BACP,OACA,WACA,UACU;AACV,KAAI,CAAC,MAAM,QAAQ,MAAM,CACvB,OAAM,IAAI,MACR,UAAU,UAAU,KAAK,SAAS,mFACe,OAAO,MAAM,GAC/D;CAEH,MAAM,MAAgB,EAAE;CACxB,MAAM,uBAAO,IAAI,KAAa;AAC9B,MAAK,MAAM,QAAQ,OAAO;AACxB,MAAI,OAAO,SAAS,YAAY,KAAK,MAAM,KAAK,GAC9C,OAAM,IAAI,MACR,UAAU,UAAU,KAAK,SAAS,gEACE,KAAK,UAAU,KAAK,CAAC,GAC1D;AAEH,MAAI,KAAK,IAAI,KAAK,CAAE;AACpB,OAAK,IAAI,KAAK;AACd,MAAI,KAAK,KAAK;;AAEhB,QAAO;;;AAIT,SAAgB,iBACd,KACA,YAC+C;CAC/C,MAAM,EAAE,MAAM,WAAW,SAAS,iBAAiB,IAAI;AACvD,KAAI,cAAc,KAChB,QAAO;EAAE,MAAM;EAAM,SAAS;EAAM;CAEtC,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,KAAK,UAAU;UACtB,KAAK;EACZ,MAAM,MAAM,aAAa,KAAK,WAAW,KAAK;AAC9C,QAAM,IAAI,MACR,2BAA2B,IAAI,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GACpF;;AAEH,KAAI,WAAW,QAAQ,WAAW,OAChC,QAAO;EAAE,MAAM,EAAE;EAAE,SAAS;EAAM;AAEpC,KAAI,OAAO,WAAW,YAAY,MAAM,QAAQ,OAAO,EAAE;EACvD,MAAM,MAAM,aAAa,KAAK,WAAW,KAAK;AAC9C,QAAM,IAAI,MAAM,oCAAoC,MAAM;;CAE5D,MAAM,OAAO;AACb,MAAK,MAAM,OAAO,OAAO,KAAK,KAAK,CACjC,KAAI,CAAC,aAAa,IAAI,IAAI,CACxB,QAAO,KACL,+CACA,KACA,cAAc,WACf;AAGL,QAAO;EAAQ;EAAqB,SAAS;EAAM;;AAGrD,MAAM,YAAY,MAA4B,OAAO,MAAM;;;;;;;AAQ3D,MAAM,yBAGF;CACF,aAAa;EAAE,OAAO;EAAU,OAAO;EAAU;CACjD,OAAO;EAAE,OAAO;EAAU,OAAO;EAAU;CAC3C,mBAAmB;EAAE,OAAO;EAAU,OAAO;EAAU;CACvD,kBAAkB;EAAE,OAAO;EAAU,OAAO;EAAU;CACtD,MAAM;EACJ,OAAO;EACP,QAAQ,MACN,OAAO,MAAM,YACZ,MAAM,QAAQ,EAAE,IAAI,EAAE,OAAO,MAAM,OAAO,MAAM,SAAS;EAC7D;CACF;;;;;;;;;AAUD,SAAS,sBACP,OACA,YAC8B;AAC9B,KAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,MAAM,CACrE;CAEF,MAAM,MAAM;CACZ,MAAM,MAA+B,EAAE;CACvC,MAAM,QAAQ,cAAc;AAE5B,MAAK,MAAM,CAAC,KAAK,MAAM,OAAO,QAAQ,IAAI,EAAE;EAC1C,MAAM,OAAO,uBAAuB;AACpC,MAAI,CAAC,KACH,QAAO,KACL,oDACA,KACA,MACD;WACQ,KAAK,MAAM,EAAE,CACtB,KAAI,OAAO;MAEX,QAAO,KACL,2DACA,KACA,OACA,KAAK,OACL,OAAO,EACR;;AAIL,QAAO,OAAO,KAAK,IAAI,CAAC,SAAS,IAAK,MAA2B;;;;;;;;AASnE,SAAS,uBACP,OACA,YACsB;AACtB,KAAI,UAAU,OAAW,QAAO;CAChC,MAAM,QAAQ,cAAc;AAC5B,KAAI,CAAC,MAAM,QAAQ,MAAM,EAAE;AACzB,SAAO,KACL,6DACA,MACD;AACD;;CAEF,MAAM,MAAgB,EAAE;CACxB,MAAM,uBAAO,IAAI,KAAa;AAC9B,MAAK,MAAM,QAAQ,OAAO;AACxB,MAAI,OAAO,SAAS,YAAY,KAAK,MAAM,KAAK,IAAI;AAClD,UAAO,KACL,6CACA,OACA,KAAK,UAAU,KAAK,CACrB;AACD;;EAEF,MAAM,OAAO,KAAK,MAAM;AACxB,MAAI,KAAK,IAAI,KAAK,CAAE;AACpB,OAAK,IAAI,KAAK;AACd,MAAI,KAAK,KAAK;;AAEhB,QAAO,IAAI,SAAS,IAAI,MAAM;;AAGhC,SAAS,gBACP,MACA,KACA,UACA,KACiB;CACjB,MAAM,EAAE,MAAM,YAAY,iBAAiB,KAAK,SAAS;CACzD,MAAM,KAAkB,QAAQ,EAAE;CAElC,MAAM,QAAQ,wBAAwB,MAAM,IAAI,UAAU,IAAI;CAC9D,MAAM,QAAQ,GAAG,SAAS,GAAG,YAAY,IAAI;AAC7C,KAAI,GAAG,SAAS,UAAa,GAAG,SAAS,oBACvC,OAAM,IAAI,MACR,UAAU,KAAK,KAAK,SAAS,uEACU,KAAK,UAAU,GAAG,KAAK,CAAC,GAChE;CAGH,IAAI;AACJ,KAAI,GAAG,qBAAqB,MAAO,oBAAmB;UAC7C,OAAO,GAAG,qBAAqB,SACtC,oBAAmB,GAAG;AAExB,QAAO;EACL;EACA,cAAc;EACd;EACA,OAAO,OAAO,KAAK,MAAM,CAAC,SAAS,IAAI,QAAQ;EAC/C,QAAQ,uBAAuB,GAAG,QAAQ,SAAS;EACnD,UAAU,OAAO,GAAG,aAAa,WAAW,GAAG,WAAW;EAC1D,WAAW,OAAO,GAAG,cAAc,WAAW,GAAG,YAAY;EAC7D,kBAAkB,sBAAsB,GAAG,kBAAkB,SAAS;EACtE;EACA,WAAW,OAAO,GAAG,cAAc,YAAY,GAAG,YAAY;EAC9D,MAAM,GAAG;EACV;;AAGH,SAAS,wBACP,WACA,IACA,UACA,KAC2B;CAC3B,MAAM,MAAiC,EAAE;CACzC,MAAM,YAAY,IAAI,2BAAW,IAAI,KAA8B;AAEnE,MAAK,MAAM,SAAS,GAAG,SAAS,EAAE,EAAE;EAClC,MAAM,SAAS,eAAe,OAAO,UAAU,UAAU;AACzD,MAAI,OAAO,SAAS,UAAU;GAC5B,MAAM,WAAW,UAAU,IAAI,OAAO,WAAW;AACjD,OAAI,CAAC,UAAU;IACb,MAAM,YACJ,UAAU,OAAO,IACb,MAAM,KAAK,UAAU,MAAM,CAAC,CAAC,KAAK,KAAK,GACvC;AACN,UAAM,IAAI,MACR,UAAU,UAAU,KAAK,SAAS,uBAAuB,OAAO,WAAW,iBAAiB,OAAO,WAAW,kCAAkC,YACjJ;;GAEH,MAAM,UAAU,SAAS,QAAQ,OAAO,KAAK;AAC7C,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,EAAE;AAClD,QAAI,CAAC,eAAe,MAAM,CACxB,OAAM,IAAI,MACR,WAAW,OAAO,WAAW,uCAAuC,IAAI,8BACzE;AAEH,QAAI,OAAO;;SAER;GACL,MAAM,OAAO,IAAI,iBAAiB,OAAO;AACzC,OAAI,CAAC,MAAM;IACT,MAAM,YAAY,IAAI,iBAClB,OAAO,KAAK,IAAI,eAAe,CAAC,KAAK,KAAK,GAC1C;AACJ,UAAM,IAAI,MACR,UAAU,UAAU,KAAK,SAAS,6BAA6B,OAAO,SAAS,mEAAmE,UAAU,qEAE7J;;AAEH,OAAI,OAAO,YAAY;;;AAI3B,QAAO;;;;;;;;;;;AAgBT,SAAS,eACP,OACA,UACA,WACiB;AACjB,KAAI,OAAO,UAAU,UAAU;AAC7B,MAAI,MAAM,WAAW,cAAc,EAAE;GACnC,MAAM,aAAa,MAAM,MAAM,EAAqB;AACpD,OAAI,WAAW,WAAW,EACxB,OAAM,IAAI,MACR,UAAU,UAAU,KAAK,SAAS,0CACnC;AAEH,UAAO;IAAE,MAAM;IAAU;IAAY,MAAM;IAAW;;AAExD,MAAI,MAAM,WAAW,EACnB,OAAM,IAAI,MACR,UAAU,UAAU,KAAK,SAAS,oCACnC;AAEH,SAAO;GAAE,MAAM;GAAW,UAAU;GAAO;;AAE7C,KAAI,OAAO,UAAU,YAAY,UAAU,KACzC,OAAM,IAAI,MACR,UAAU,UAAU,KAAK,SAAS,gCAAgC,KAAK,UAAU,MAAM,GACxF;CAEH,MAAM,OAAO,OAAO,KAAK,MAAM;AAC/B,KAAI,KAAK,WAAW,EAClB,OAAM,IAAI,MACR,UAAU,UAAU,KAAK,SAAS,0DAA0D,KAAK,KAAK,KAAK,GAC5G;CAEH,MAAM,MAAM,KAAK;AAIjB,KAAI,QAAQ,SACV,OAAM,IAAI,MACR,UAAU,UAAU,KAAK,SAAS,0CACnC;AAEH,KAAI,CAAC,IAAI,WAAW,cAAc,CAChC,OAAM,IAAI,MACR,UAAU,UAAU,KAAK,SAAS,iGAAiG,IAAI,kEAExI;CAEH,MAAM,aAAa,IAAI,MAAM,EAAqB;AAClD,KAAI,WAAW,WAAW,EACxB,OAAM,IAAI,MACR,UAAU,UAAU,KAAK,SAAS,0CACnC;CAEH,MAAM,QAAQ,MAAM;AACpB,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO;EAAE,MAAM;EAAU;EAAY,MAAM,EAAE,MAAM,OAAO;EAAE;AAE9D,KAAI,OAAO,UAAU,YAAY,UAAU,KACzC,QAAO;EACL,MAAM;EACN;EACA,MAAM;EACP;AAEH,OAAM,IAAI,MACR,UAAU,UAAU,KAAK,SAAS,YAAY,WAAW,sEAC1D"}
|