eve 0.15.1 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/src/execution/deliver-payloads.d.ts +3 -0
- package/dist/src/execution/deliver-payloads.js +1 -0
- package/dist/src/execution/dispatch-runtime-actions-step.d.ts +2 -0
- package/dist/src/execution/dispatch-runtime-actions-step.js +1 -1
- package/dist/src/execution/dispatch-workflow-runtime-actions-step.d.ts +1 -0
- package/dist/src/execution/dispatch-workflow-runtime-actions-step.js +1 -1
- package/dist/src/execution/durable-session-migrations/turn-workflow.d.ts +7 -0
- package/dist/src/execution/durable-session-migrations/turn-workflow.js +1 -1
- package/dist/src/execution/forward-turn-delivery-step.d.ts +6 -0
- package/dist/src/execution/forward-turn-delivery-step.js +1 -0
- package/dist/src/execution/hook-ownership.d.ts +6 -0
- package/dist/src/execution/hook-ownership.js +1 -1
- package/dist/src/execution/remote-agent-dispatch.d.ts +1 -0
- package/dist/src/execution/remote-agent-dispatch.js +1 -1
- package/dist/src/execution/route-child-delivery.d.ts +18 -0
- package/dist/src/execution/route-child-delivery.js +1 -0
- package/dist/src/execution/session-delivery-hook.d.ts +20 -0
- package/dist/src/execution/session-delivery-hook.js +1 -0
- package/dist/src/execution/subagent-tool.d.ts +2 -0
- package/dist/src/execution/subagent-tool.js +1 -1
- package/dist/src/execution/turn-control-protocol.d.ts +36 -0
- package/dist/src/execution/turn-control-protocol.js +1 -0
- package/dist/src/execution/turn-control-receiver.d.ts +35 -0
- package/dist/src/execution/turn-control-receiver.js +1 -0
- package/dist/src/execution/turn-dispatch.d.ts +17 -0
- package/dist/src/execution/turn-dispatch.js +1 -0
- package/dist/src/execution/turn-execution-cursor.d.ts +48 -0
- package/dist/src/execution/turn-execution-cursor.js +1 -0
- package/dist/src/execution/turn-workflow.d.ts +1 -27
- package/dist/src/execution/turn-workflow.js +1 -1
- package/dist/src/execution/workflow-entry.d.ts +6 -4
- package/dist/src/execution/workflow-entry.js +1 -1
- package/dist/src/execution/workflow-steps.js +1 -1
- package/dist/src/harness/runtime-actions.d.ts +4 -22
- package/dist/src/harness/runtime-actions.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/workflow-bundle/workflow-builders.js +1 -1
- package/dist/src/setup/scaffold/create/project.js +1 -1
- package/docs/connections/overview.mdx +11 -11
- package/docs/meta.json +1 -0
- package/docs/patterns/dynamic-scheduling.md +257 -0
- package/docs/patterns/meta.json +9 -0
- package/docs/patterns/multi-tenant-approvals.md +191 -0
- package/docs/patterns/multi-tenant-auth.md +177 -0
- package/docs/patterns/multi-tenant-memory.md +188 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createActionResultEvent}from"#protocol/message.js";import{parseJsonObject}from"#shared/json.js";import{clearProxyInputRequestsForChild}from"#harness/proxy-input-requests.js";import{getRuntimeActionRequestKey,getRuntimeActionResultKey}from"#runtime/actions/keys.js";const PENDING_RUNTIME_ACTION_BATCH_KEY=`eve.runtime.pendingActionBatch`;function getPendingRuntimeActionBatch(e){let t=e?.[PENDING_RUNTIME_ACTION_BATCH_KEY];if(typeof t!=`object`||!t)return;let n=t;if(!(!Array.isArray(n.actions)||!Array.isArray(n.responseMessages)||typeof n.event!=`object`||n.event===null))return n}function hasPendingRuntimeActionBatch(e){return getPendingRuntimeActionBatch(e)!==void 0}function clearPendingRuntimeActionBatch(e){if(e.state?.[PENDING_RUNTIME_ACTION_BATCH_KEY]===void 0)return e;let t={...e.state};return delete t[PENDING_RUNTIME_ACTION_BATCH_KEY],{...e,state:Object.keys(t).length>0?t:void 0}}function setPendingRuntimeActionBatch(e){let t={...e.session.state};return t[PENDING_RUNTIME_ACTION_BATCH_KEY]={actions:[...e.actions],event:e.event,responseMessages:[...e.responseMessages]},{...e.session,state:t}}function recordPendingSubagentChildToken(e){let t=getPendingRuntimeActionBatch(e.session.state);if(t===void 0)return e.session;let n={...e.session.state};return n[PENDING_RUNTIME_ACTION_BATCH_KEY]={...t,childContinuationTokens:{...t.childContinuationTokens,[e.callId]:e.childContinuationToken}},{...e.session,state:n}}
|
|
1
|
+
import{createActionResultEvent}from"#protocol/message.js";import{parseJsonObject}from"#shared/json.js";import{clearProxyInputRequestsForChild}from"#harness/proxy-input-requests.js";import{getRuntimeActionRequestKey,getRuntimeActionResultKey}from"#runtime/actions/keys.js";const PENDING_RUNTIME_ACTION_BATCH_KEY=`eve.runtime.pendingActionBatch`;function getPendingRuntimeActionBatch(e){let t=e?.[PENDING_RUNTIME_ACTION_BATCH_KEY];if(typeof t!=`object`||!t)return;let n=t;if(!(!Array.isArray(n.actions)||!Array.isArray(n.responseMessages)||typeof n.event!=`object`||n.event===null))return n}function hasPendingRuntimeActionBatch(e){return getPendingRuntimeActionBatch(e)!==void 0}function clearPendingRuntimeActionBatch(e){if(e.state?.[PENDING_RUNTIME_ACTION_BATCH_KEY]===void 0)return e;let t={...e.state};return delete t[PENDING_RUNTIME_ACTION_BATCH_KEY],{...e,state:Object.keys(t).length>0?t:void 0}}function setPendingRuntimeActionBatch(e){let t={...e.session.state};return t[PENDING_RUNTIME_ACTION_BATCH_KEY]={actions:[...e.actions],event:e.event,responseMessages:[...e.responseMessages]},{...e.session,state:t}}function recordPendingSubagentChildToken(e){let t=getPendingRuntimeActionBatch(e.session.state);if(t===void 0)return e.session;let n={...e.session.state};return n[PENDING_RUNTIME_ACTION_BATCH_KEY]={...t,childContinuationTokens:{...t.childContinuationTokens,[e.callId]:e.childContinuationToken}},{...e.session,state:n}}function resolveReadyRuntimeActionResults(e){let t=getPendingRuntimeActionBatch(e.session.state);if(t!==void 0)return resolveRuntimeActionResultsForBatch({batch:t,results:e.results})}function resolveRuntimeActionResultsForBatch(e){return resolveRuntimeActionResultsForKeys({pendingKeys:e.batch.actions.map(e=>getRuntimeActionRequestKey(e)),results:e.results})}function resolveRuntimeActionResultsForKeys(e){let t=new Set(e.pendingKeys),n=new Map;for(let r of e.results){let e=getRuntimeActionResultKey(r);t.has(e)&&n.set(e,r)}let r=[];for(let t of e.pendingKeys){let e=n.get(t);if(e===void 0)return;r.push(e)}return r}async function resolvePendingRuntimeActions(t){let r=getPendingRuntimeActionBatch(t.session.state);if(r===void 0)return{messages:[...t.session.history],outcome:`continue`,session:t.session};let i=resolveReadyRuntimeActionResults({results:t.stepInput?.runtimeActionResults??[],session:t.session});if(i===void 0)return{messages:[...t.session.history],outcome:`unresolved`,session:t.session};if(t.emit!==void 0)for(let n of i)n.kind===`subagent-result`&&n.isError!==!0&&await t.emit({data:{callId:n.callId,output:typeof n.output==`string`?n.output:JSON.stringify(n.output),subagentName:n.subagentName},type:`subagent.completed`}),await t.emit(createActionResultEvent({result:n,sequence:r.event.sequence,stepIndex:r.event.stepIndex,turnId:r.event.turnId}));let a={...t.session.state};delete a[PENDING_RUNTIME_ACTION_BATCH_KEY];let o={...t.session,state:Object.keys(a).length>0?a:void 0},s=r.childContinuationTokens;if(s!==void 0)for(let e of i){if(e.kind!==`subagent-result`)continue;let t=s[e.callId];t!==void 0&&(o=clearProxyInputRequestsForChild(o,t))}let c=i.map(e=>{switch(e.kind){case`load-skill-result`:return{output:toToolResultOutput(e),toolCallId:e.callId,toolName:`load_skill`,type:`tool-result`};case`subagent-result`:return{output:toToolResultOutput(e),toolCallId:e.callId,toolName:e.subagentName,type:`tool-result`};case`tool-result`:return{output:toToolResultOutput(e),toolCallId:e.callId,toolName:e.toolName,type:`tool-result`}}throw Error(`Unsupported runtime action result kind "${String(e)}".`)}),l=[...o.history,...r.responseMessages];return c.length>0&&l.push({content:c,role:`tool`}),{messages:l,outcome:`resolved`,session:o}}function createRuntimeActionRequestFromToolCall(e){let t=e.tools.get(e.toolCall.toolName);return t?.runtimeAction?.kind===`subagent-call`?{callId:e.toolCall.toolCallId,description:t.description,input:resolveToolCallInputObject(e.toolCall.input,{callId:e.toolCall.toolCallId,toolName:e.toolCall.toolName}),kind:`subagent-call`,name:t.name,nodeId:t.runtimeAction.nodeId,subagentName:t.runtimeAction.subagentName}:t?.runtimeAction?.kind===`remote-agent-call`?{callId:e.toolCall.toolCallId,description:t.description,input:resolveToolCallInputObject(e.toolCall.input,{callId:e.toolCall.toolCallId,toolName:e.toolCall.toolName}),kind:`remote-agent-call`,name:t.name,nodeId:t.runtimeAction.nodeId,remoteAgentName:t.runtimeAction.remoteAgentName??t.name}:{callId:e.toolCall.toolCallId,input:resolveToolCallInputObject(e.toolCall.input,{callId:e.toolCall.toolCallId,toolName:e.toolCall.toolName}),kind:`tool-call`,toolName:e.toolCall.toolName}}function resolveToolCallInputObject(e,n){if(e==null)return{};try{return parseJsonObject(e)}catch(e){let t=e instanceof Error?e.message:String(e);throw TypeError(`Failed to parse tool-call arguments for "${n.toolName}" (${n.callId}): ${t}`,{cause:e})}}function toToolResultOutput(e){return typeof e.output==`string`?e.isError===!0?{type:`error-text`,value:e.output}:{type:`text`,value:e.output}:e.isError===!0?{type:`error-json`,value:toMutableJsonValue(e.output)}:{type:`json`,value:toMutableJsonValue(e.output)}}function toMutableJsonValue(e){if(e===null||typeof e==`string`||typeof e==`number`||typeof e==`boolean`)return e;if(Array.isArray(e))return e.map(e=>toMutableJsonValue(e));let t={};for(let[n,r]of Object.entries(e))t[n]=toMutableJsonValue(r);return t}export{clearPendingRuntimeActionBatch,createRuntimeActionRequestFromToolCall,getPendingRuntimeActionBatch,hasPendingRuntimeActionBatch,recordPendingSubagentChildToken,resolvePendingRuntimeActions,resolveRuntimeActionResultsForKeys,resolveToolCallInputObject,setPendingRuntimeActionBatch};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createRequire}from"node:module";import{existsSync,readFileSync,realpathSync}from"node:fs";import{basename,dirname,join}from"node:path";import{EVE_PACKAGE_NAME}from"#internal/package-name.js";import{fileURLToPath}from"node:url";let cachedPackageInfo;const BUNDLED_FALLBACK_PACKAGE_VERSION=`0.15.
|
|
1
|
+
import{createRequire}from"node:module";import{existsSync,readFileSync,realpathSync}from"node:fs";import{basename,dirname,join}from"node:path";import{EVE_PACKAGE_NAME}from"#internal/package-name.js";import{fileURLToPath}from"node:url";let cachedPackageInfo;const BUNDLED_FALLBACK_PACKAGE_VERSION=`0.15.2`,WORKFLOW_MODULE_ALIASES={"workflow/errors":`src/compiled/@workflow/errors/index.js`,"workflow/internal/private":`src/compiled/@workflow/core/private.js`};function resolveFallbackPackageVersion(){return BUNDLED_FALLBACK_PACKAGE_VERSION.startsWith(`__`)?`0.0.0`:BUNDLED_FALLBACK_PACKAGE_VERSION}const FALLBACK_PACKAGE_INFO={name:EVE_PACKAGE_NAME,version:resolveFallbackPackageVersion()};function resolveCurrentModulePath(){return typeof __filename==`string`?__filename:resolveCurrentModulePathFromStack()}function resolveCurrentModulePathFromStack(){let e=Error.prepareStackTrace;try{Error.prepareStackTrace=(e,t)=>t;let e=Error().stack?.[0]?.getFileName();if(typeof e!=`string`||e.length===0)throw Error(`Failed to resolve the current module path from the stack trace.`);return e.startsWith(`file:`)?fileURLToPath(e):e}finally{Error.prepareStackTrace=e}}const require=createRequire(resolveCurrentModulePath());function isBuildOutputPackageRoot(e){return basename(e)===`dist`&&existsSync(join(dirname(e),`package.json`))}function resolvePackageBuildRoot(){let e=dirname(realpathSync(resolveCurrentModulePath()));for(;;){if(isBuildOutputPackageRoot(e))return e;let t=dirname(e);if(t===e)return null;e=t}}function findNearestPackageRoot(e){let n=e;for(;;){if(existsSync(join(n,`package.json`))&&!isBuildOutputPackageRoot(n))return n;let r=dirname(n);if(r===n)throw Error(`Failed to resolve package root from "${e}".`);n=r}}function resolvePackageRoot(){return findNearestPackageRoot(dirname(realpathSync(resolveCurrentModulePath())))}function tryResolvePackageRoot(){try{return resolvePackageRoot()}catch{return}}function rewriteSourceFilePathForBuild(e){return e.replace(/\.[cm]?tsx?$/,`.js`)}function resolvePackageSourceFilePath(e){let t=resolvePackageBuildRoot();return t===null?join(resolvePackageRoot(),e):join(t,rewriteSourceFilePathForBuild(e))}function resolvePackageSourceDirectoryPath(e){let t=resolvePackageBuildRoot();return join(t===null?resolvePackageRoot():t,e)}function resolvePackageDependencyPath(e){return require.resolve(e)}function resolvePackageCompiledFilePath(e){let t=resolvePackageBuildRoot();return t===null?join(resolvePackageRoot(),`.generated`,`compiled`,e.replace(/^src\/compiled\//,``)):join(t,e)}function normalizeInstalledPackageInfo(e){let t=e;if(!(typeof t.name!=`string`||typeof t.version!=`string`))return{name:t.name,version:t.version}}function tryReadInstalledPackageInfo(e,t){let r=normalizeInstalledPackageInfo(JSON.parse(readFileSync(e,`utf8`)));if(r?.name===t)return r}function resolveInstalledPackageInfo(){if(cachedPackageInfo)return cachedPackageInfo;let e=tryResolvePackageRoot(),t=e===void 0?void 0:tryReadInstalledPackageInfo(join(e,`package.json`),EVE_PACKAGE_NAME);if(t)return cachedPackageInfo=t,cachedPackageInfo;try{let e=tryReadInstalledPackageInfo(require.resolve(`${EVE_PACKAGE_NAME}/package.json`),EVE_PACKAGE_NAME);if(e)return cachedPackageInfo=e,cachedPackageInfo}catch{}return cachedPackageInfo={...FALLBACK_PACKAGE_INFO},cachedPackageInfo}function readWorkflowVersionFromManifest(e){let t=e;for(let e of[t.devDependencies,t.dependencies,t.peerDependencies]){let t=e?.[`@workflow/core`];if(typeof t==`string`&&t.trim().length>0)return t}}function resolveExpectedWorkflowVersion(){let e=tryResolvePackageRoot();if(e!==void 0)try{return readWorkflowVersionFromManifest(JSON.parse(readFileSync(join(e,`package.json`),`utf8`)))}catch{}try{return readWorkflowVersionFromManifest(JSON.parse(readFileSync(require.resolve(`${EVE_PACKAGE_NAME}/package.json`),`utf8`)))}catch{return}}function resolveWorkflowModulePath(e){if(e===`workflow`)return resolvePackageSourceFilePath(`src/internal/workflow/index.ts`);if(e===`workflow/api`||e===`workflow/runtime`)return resolvePackageSourceFilePath(`src/internal/workflow/runtime.ts`);if(e===`workflow/internal/builtins`)return resolvePackageSourceFilePath(`src/internal/workflow/builtins.ts`);let t=WORKFLOW_MODULE_ALIASES[e];return t===void 0?require.resolve(e):resolvePackageCompiledFilePath(t)}export{resolveExpectedWorkflowVersion,resolveInstalledPackageInfo,resolvePackageDependencyPath,resolvePackageRoot,resolvePackageSourceDirectoryPath,resolvePackageSourceFilePath,resolveWorkflowModulePath};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{transformWorkflowDirectives}from"./workflow-transformer.js";import{existsSync,readFileSync}from"node:fs";import{dirname,isAbsolute,join,relative,resolve,sep}from"node:path";import{readFile}from"node:fs/promises";import{STABLE_WORKFLOW_NAMES}from"#execution/workflow-runtime.js";import{EVE_WORKFLOW_QUEUE_TOPIC}from"#internal/workflow/queue-namespace.js";const WORKFLOW_QUEUE_TRIGGER={type:`queue/v2beta`,topic:EVE_WORKFLOW_QUEUE_TOPIC,consumer:`default`,retryAfterSeconds:5,initialDelaySeconds:0},packageJsonCache=new Map,projectDepsCache=new Map;async function applyWorkflowTransform(t,n,r,o,s,c=STABLE_WORKFLOW_NAMES){let l=s??process.cwd(),{moduleSpecifier:u,stableModuleSpecifier:d}=resolveModuleSpecifier(o===void 0?isAbsolute(t)?t:join(l,t):o,l);return transformWorkflowDirectives({filename:t,mode:r,moduleSpecifier:u,source:n,stableModuleSpecifier:d,stableWorkflowNames:c})}function detectWorkflowPatterns(e){return{hasSerde:e.includes(`workflow.serde`)||e.includes(`@serde`)||e.includes(`workflowSerde`)||e.includes(`__workflow_serde`),hasUseStep:/["']use step["']/.test(e),hasUseWorkflow:/["']use workflow["']/.test(e)}}function getImportPath(e,t){let n=isInNodeModules(e),r=!n&&isWorkspacePackage(e,t);if(n||r){let n=findPackageJson(e);if(n!==null){let i=getProjectDependencies(t).has(n.name);if(!(r||i))return{importPath:toRelativeImportPath(e,t),isPackage:!1};let a=resolveExportSubpath(e,n);return a?{importPath:`${n.name}${a}`,isPackage:!0}:isRootEntrypointFile(e,n)?{importPath:n.name,isPackage:!0}:{importPath:toRelativeImportPath(e,t),isPackage:!1}}}return{importPath:toRelativeImportPath(e,t),isPackage:!1}}function resolveModuleSpecifier(e,t){let n=isInNodeModules(e),r=!n&&isWorkspacePackage(e,t);if(!n&&!r)return{moduleSpecifier:void 0,stableModuleSpecifier:void 0};
|
|
1
|
+
import{transformWorkflowDirectives}from"./workflow-transformer.js";import{existsSync,readFileSync}from"node:fs";import{dirname,isAbsolute,join,relative,resolve,sep}from"node:path";import{readFile}from"node:fs/promises";import{STABLE_WORKFLOW_NAMES}from"#execution/workflow-runtime.js";import{EVE_PACKAGE_NAME}from"#internal/package-name.js";import{EVE_WORKFLOW_QUEUE_TOPIC}from"#internal/workflow/queue-namespace.js";const WORKFLOW_QUEUE_TRIGGER={type:`queue/v2beta`,topic:EVE_WORKFLOW_QUEUE_TOPIC,consumer:`default`,retryAfterSeconds:5,initialDelaySeconds:0},packageJsonCache=new Map,projectDepsCache=new Map;async function applyWorkflowTransform(t,n,r,o,s,c=STABLE_WORKFLOW_NAMES){let l=s??process.cwd(),{moduleSpecifier:u,stableModuleSpecifier:d}=resolveModuleSpecifier(o===void 0?isAbsolute(t)?t:join(l,t):o,l);return transformWorkflowDirectives({filename:t,mode:r,moduleSpecifier:u,source:n,stableModuleSpecifier:d,stableWorkflowNames:c})}function detectWorkflowPatterns(e){return{hasSerde:e.includes(`workflow.serde`)||e.includes(`@serde`)||e.includes(`workflowSerde`)||e.includes(`__workflow_serde`),hasUseStep:/["']use step["']/.test(e),hasUseWorkflow:/["']use workflow["']/.test(e)}}function getImportPath(e,t){let n=isInNodeModules(e),r=!n&&isWorkspacePackage(e,t);if(n||r){let n=findPackageJson(e);if(n!==null){let i=getProjectDependencies(t).has(n.name);if(!(r||i))return{importPath:toRelativeImportPath(e,t),isPackage:!1};let a=resolveExportSubpath(e,n);return a?{importPath:`${n.name}${a}`,isPackage:!0}:isRootEntrypointFile(e,n)?{importPath:n.name,isPackage:!0}:{importPath:toRelativeImportPath(e,t),isPackage:!1}}}return{importPath:toRelativeImportPath(e,t),isPackage:!1}}function resolveModuleSpecifier(e,t){let n=isInNodeModules(e),r=!n&&isWorkspacePackage(e,t),i=findPackageJson(e);if(!n&&!r)return{moduleSpecifier:void 0,stableModuleSpecifier:i?.name===EVE_PACKAGE_NAME?EVE_PACKAGE_NAME:void 0};if(i===null)return{moduleSpecifier:void 0,stableModuleSpecifier:void 0};let a=resolveExportSubpath(e,i),o=a?`${i.name}${a}`:i.name;return{moduleSpecifier:`${o}@${i.version}`,stableModuleSpecifier:o}}function findPackageJson(e){let i=dirname(e),o=[];for(;i!==dirname(i);){let e=packageJsonCache.get(i);if(e!==void 0){for(let t of o)packageJsonCache.set(t,e);return e}o.push(i);let s=join(i,`package.json`);if(existsSync(s))try{let e=JSON.parse(readFileSync(s,`utf8`));if(typeof e.name==`string`&&typeof e.version==`string`){let t={dir:i,exports:e.exports,main:e.main,module:e.module,name:e.name,version:e.version};packageJsonCache.set(i,t);for(let e of o)packageJsonCache.set(e,t);return t}}catch{}i=dirname(i)}for(let e of o)packageJsonCache.set(e,null);return null}function resolveExportSubpath(e,t){if(t.exports===null||typeof t.exports!=`object`||Array.isArray(t.exports))return``;let n=e.replace(/\\/g,`/`),r=t.dir.replace(/\\/g,`/`),i=n.startsWith(`${r}/`)?`./${n.substring(r.length+1)}`:null;if(i===null)return``;for(let[e,n]of Object.entries(t.exports)){let t=resolveExportTarget(n);if(t!==null&&normalizeExportPath(t)===i)return e===`.`?``:e.substring(1)}return``}function resolveExportTarget(e){if(typeof e==`string`)return e;if(Array.isArray(e)){for(let t of e){let e=resolveExportTarget(t);if(e!==null)return e}return null}if(typeof e==`object`&&e)for(let t of[`workflow`,`default`,`require`,`import`,`node`]){let n=e[t],r=resolveExportTarget(n);if(r!==null)return r}return null}function normalizeExportPath(e){return e.startsWith(`./`)?e:`./${e}`}function isInNodeModules(e){return e.split(sep).join(`/`).includes(`/node_modules/`)}function getProjectDependencies(e){let r=projectDepsCache.get(e);if(r!==void 0)return r;let i=new Set,o=join(e,`package.json`);if(existsSync(o))try{let e=JSON.parse(readFileSync(o,`utf8`));for(let t of[`dependencies`,`devDependencies`,`peerDependencies`,`optionalDependencies`]){let n=e[t];if(typeof n==`object`&&n&&!Array.isArray(n))for(let e of Object.keys(n))i.add(e)}}catch{}return projectDepsCache.set(e,i),i}function isWorkspacePackage(e,t){if(isInNodeModules(e))return!1;let n=findPackageJson(e);return n===null||resolve(n.dir)===resolve(t)?!1:getProjectDependencies(t).has(n.name)}function toRelativeImportPath(e,t){let n=t.replace(/\\/g,`/`),r=e.replace(/\\/g,`/`),i=r.startsWith(`${n}/`)?r.substring(n.length+1):relative(t,e).replace(/\\/g,`/`);return i.startsWith(`.`)||(i=`./${i}`),i}function hasRootExport(e){if(typeof e==`string`||Array.isArray(e))return!0;if(typeof e!=`object`||!e)return!1;let t=Object.keys(e);return t.length>0&&t.every(e=>!e.startsWith(`.`))?!0:`.`in e}function normalizePackageTargetPath(e){let t=e.replace(/\\/g,`/`);return t.startsWith(`./`)?t.substring(2):t.startsWith(`/`)?t.substring(1):t}function isRootEntrypointFile(e,t){let n=e.replace(/\\/g,`/`),r=t.dir.replace(/\\/g,`/`);if(!n.startsWith(`${r}/`))return!1;let i=n.substring(r.length+1);if(t.exports!==void 0){let e;if(t.exports!==null&&typeof t.exports==`object`&&`.`in t.exports)e=t.exports[`.`];else if(hasRootExport(t.exports))e=t.exports;else return!1;let n=resolveExportTarget(e);return n!==null&&normalizePackageTargetPath(n)===i}return[t.module,t.main,`index.js`,`index.mjs`,`index.cjs`,`index.ts`,`index.mts`,`index.cts`].flatMap(e=>typeof e==`string`?[normalizePackageTargetPath(e)]:[]).includes(i)}async function readSourceFile(e){return await readFile(e,`utf8`)}export{WORKFLOW_QUEUE_TRIGGER,applyWorkflowTransform,detectWorkflowPatterns,getImportPath,readSourceFile};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{pinnedNodeEngineMajor}from"../../node-engine.js";import{pathExists,writeTextFile}from"../files.js";import{resolveVersionToken}from"../version-tokens.js";import{applyPackageManagerWorkspaceConfiguration,isPackageManagerWorkspaceMember,patchWorkspaceRootPackageJson}from"../workspace-root.js";import{SUPPORTED_AUTHORED_MODULE_FILE_EXTENSIONS}from"../update/module-files.js";import{WEB_APP_TEMPLATE_FILES}from"./web-template.js";import{basename,join,resolve}from"node:path";import{mkdir,readdir,stat}from"node:fs/promises";const CURRENT_DIRECTORY_PROJECT_NAME=`.`,ALLOWED_CREATE_IN_PLACE_ENTRIES=new Set([`.DS_Store`,`.git`,`.gitkeep`,`.hg`]),DEFAULT_AI_PACKAGE_VERSION=`^7.0.0`,DEFAULT_CONNECT_PACKAGE_VERSION=`0.2.2`,DEFAULT_ZOD_PACKAGE_VERSION=`4.4.3`,DEFAULT_EVE_PACKAGE_CONTRACT={version:`0.15.
|
|
1
|
+
import{pinnedNodeEngineMajor}from"../../node-engine.js";import{pathExists,writeTextFile}from"../files.js";import{resolveVersionToken}from"../version-tokens.js";import{applyPackageManagerWorkspaceConfiguration,isPackageManagerWorkspaceMember,patchWorkspaceRootPackageJson}from"../workspace-root.js";import{SUPPORTED_AUTHORED_MODULE_FILE_EXTENSIONS}from"../update/module-files.js";import{WEB_APP_TEMPLATE_FILES}from"./web-template.js";import{basename,join,resolve}from"node:path";import{mkdir,readdir,stat}from"node:fs/promises";const CURRENT_DIRECTORY_PROJECT_NAME=`.`,ALLOWED_CREATE_IN_PLACE_ENTRIES=new Set([`.DS_Store`,`.git`,`.gitkeep`,`.hg`]),DEFAULT_AI_PACKAGE_VERSION=`^7.0.0`,DEFAULT_CONNECT_PACKAGE_VERSION=`0.2.2`,DEFAULT_ZOD_PACKAGE_VERSION=`4.4.3`,DEFAULT_EVE_PACKAGE_CONTRACT={version:`0.15.2`,nodeEngine:`>=24`};function resolveEvePackageContract(e=DEFAULT_EVE_PACKAGE_CONTRACT){return{version:resolveVersionToken(`evePackage.version`,e.version),nodeEngine:resolveVersionToken(`evePackage.nodeEngine`,e.nodeEngine)}}function modelProviderSlug(e){let t=(e.split(`/`)[0]??``).replaceAll(/[^A-Za-z0-9._-]/gu,``);return t.length>0?t:`anthropic`}function byokProviderEnvVar(e){let t=modelProviderSlug(e).toUpperCase().replaceAll(/[^A-Z0-9]/gu,`_`);return`${/^[0-9]/.test(t)?`_`:``}${t}_API_KEY`}function agentTemplateFiles(e){return{"agent/agent.ts":BASE_AGENT_TEMPLATE.replaceAll(`__EVE_INIT_MODEL__`,e),"agent/channels/eve.ts":WEB_APP_TEMPLATE_FILES[`agent/channels/eve.ts`],"agent/instructions.md":AGENT_INSTRUCTIONS_TEMPLATE}}function renderTemplate(e,t){return e.replaceAll(`__EVE_INIT_APP_NAME__`,t.appName).replaceAll(`__EVE_INIT_MODEL__`,t.model).replaceAll(`__EVE_INIT_BYOK_PROVIDER__`,modelProviderSlug(t.model)).replaceAll(`__EVE_INIT_BYOK_ENV_VAR__`,byokProviderEnvVar(t.model)).replaceAll(`__EVE_INIT_PACKAGE_VERSION__`,formatEveDependencySpecifier(t.eveVersion)).replaceAll(`__EVE_INIT_AI_SDK_VERSION__`,t.aiPackageVersion).replaceAll(`__EVE_INIT_CONNECT_VERSION__`,t.connectPackageVersion).replaceAll(`__EVE_INIT_ZOD_VERSION__`,t.zodPackageVersion).replaceAll(`__EVE_INIT_TYPESCRIPT_VERSION__`,t.typescriptPackageVersion).replaceAll(`__EVE_INIT_TYPES_NODE_VERSION__`,t.nodeTypesVersion).replaceAll(`__EVE_INIT_NODE_ENGINE__`,t.nodeEngine)}function formatEveDependencySpecifier(e){return/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z-.]+)?$/.test(e)?`^${e}`:e}const BASE_AGENT_TEMPLATE=`import { defineAgent } from "eve";
|
|
2
2
|
|
|
3
3
|
export default defineAgent({
|
|
4
4
|
model: "__EVE_INIT_MODEL__",
|
|
@@ -32,11 +32,11 @@ eve resolves and caches connection tokens per step; they never land in conversat
|
|
|
32
32
|
|
|
33
33
|
A connection credential can belong to the agent or to the person using it. This choice is separate from route auth, but user-scoped connection auth depends on route auth: eve can only resolve a user token when the active session has `ctx.session.auth.current?.principalType === "user"`.
|
|
34
34
|
|
|
35
|
-
| Credential owner | Use when
|
|
36
|
-
| ---------------- |
|
|
37
|
-
| App | The agent should use one shared service, bot, installation, or app credential.
|
|
38
|
-
| User | Each end-user should authorize and use their own third-party account.
|
|
39
|
-
| User from a job | Background work should use the same user's OAuth grant that started the work.
|
|
35
|
+
| Credential owner | Use when | Auth shape |
|
|
36
|
+
| ---------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
37
|
+
| App | The agent should use one shared service, bot, installation, or app credential. | `auth: { getToken }` defaults to `principalType: "app"`, or use `connect({ connector: "linear/myagent", principalType: "app" })` with Vercel Connect. |
|
|
38
|
+
| User | Each end-user should authorize and use their own third-party account. | `connect("linear/myagent")`, `connect({ connector: "linear/myagent", principalType: "user" })`, or `auth: { principalType: "user", getToken }`. |
|
|
39
|
+
| User from a job | Background work should use the same user's OAuth grant that started the work. | Start or resume the session through a channel whose route auth resolved that user, or pass an explicit user auth context when dispatching through a channel. |
|
|
40
40
|
|
|
41
41
|
`principalType: "user"` does not mean "ask any human later." It means "key this credential to the authenticated user already attached to the eve session." If the run was started by a schedule, a same-project runtime token, `localDev()`, or another internal runtime path without an end-user principal, a user-scoped connection fails with `reason: "principal_required"` instead of starting OAuth. In that case, either authenticate the inbound channel as a user or configure the connection as app-scoped.
|
|
42
42
|
|
|
@@ -157,12 +157,12 @@ App-scoped Connect auth is non-interactive. eve asks Vercel Connect for an app t
|
|
|
157
157
|
|
|
158
158
|
### Troubleshooting Vercel Connect auth
|
|
159
159
|
|
|
160
|
-
| Symptom | What it means
|
|
161
|
-
| -------------------------------------------- |
|
|
162
|
-
| `reason: "principal_required"` | A user-scoped connection ran without an authenticated user on the active session. | Return `principalType: "user"` from the channel's route auth, or change the connection to `principalType: "app"` if it should be shared.
|
|
163
|
-
| `authorization.required` appears but no UI | eve parked the turn for OAuth, but the channel or frontend is not rendering the challenge. | Render the challenge from the stream event and continue the same session after the callback.
|
|
164
|
-
| OAuth works locally but fails after deploy | The project may not be linked to the Connect client, or the deployed runtime may not have the expected Vercel OIDC/project scope. | Run Connect setup from the consuming project directory, link the project, deploy again, and verify the connector UID in `connect("...")`.
|
|
165
|
-
| A scheduled or internal run needs user OAuth | Schedules and runtime callers do not automatically carry an end-user principal. | Dispatch through a user-authenticated channel when work is user-owned, or use app-scoped auth for agent-owned background work.
|
|
160
|
+
| Symptom | What it means | Fix |
|
|
161
|
+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
162
|
+
| `reason: "principal_required"` | A user-scoped connection ran without an authenticated user on the active session. | Return `principalType: "user"` from the channel's route auth, or change the connection to `principalType: "app"` if it should be shared. |
|
|
163
|
+
| `authorization.required` appears but no UI | eve parked the turn for OAuth, but the channel or frontend is not rendering the challenge. | Render the challenge from the stream event and continue the same session after the callback. |
|
|
164
|
+
| OAuth works locally but fails after deploy | The project may not be linked to the Connect client, or the deployed runtime may not have the expected Vercel OIDC/project scope. | Run Connect setup from the consuming project directory, link the project, deploy again, and verify the connector UID in `connect("...")`. |
|
|
165
|
+
| A scheduled or internal run needs user OAuth | Schedules and runtime callers do not automatically carry an end-user principal. | Dispatch through a user-authenticated channel when work is user-owned, or use app-scoped auth for agent-owned background work. |
|
|
166
166
|
|
|
167
167
|
## Self-hosted interactive OAuth
|
|
168
168
|
|
package/docs/meta.json
CHANGED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Dynamic scheduling"
|
|
3
|
+
description: "Compose one minute-level eve schedule, proactive channel handoff, and CRUD tools into application-managed schedules."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Authored eve schedules are static files discovered at build time. You can build dynamic scheduling today by putting schedule rows in your application store and using one authored schedule as a dispatcher:
|
|
7
|
+
|
|
8
|
+
1. CRUD tools let the agent create and manage rows for the current tenant;
|
|
9
|
+
2. `defineSchedule({ cron: "* * * * *" })` wakes once a minute;
|
|
10
|
+
3. the handler atomically claims due rows;
|
|
11
|
+
4. `receive(...)` starts a normal durable agent session for each row.
|
|
12
|
+
|
|
13
|
+
PostgreSQL or a durable KV store can back the adapter. The important storage capability is an atomic lease, not a particular schema.
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
agent/
|
|
17
|
+
channels/slack.ts
|
|
18
|
+
lib/schedule-store.ts # your storage adapter
|
|
19
|
+
lib/tenant.ts
|
|
20
|
+
schedules/dynamic.ts
|
|
21
|
+
tools/create_schedule.ts
|
|
22
|
+
tools/delete_schedule.ts
|
|
23
|
+
tools/list_schedules.ts
|
|
24
|
+
tools/update_schedule.ts
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Dispatch due schedules every minute
|
|
28
|
+
|
|
29
|
+
This is the only authored schedule. It looks up due application-managed rows and hands each one to Slack as a proactive session:
|
|
30
|
+
|
|
31
|
+
```ts title="agent/schedules/dynamic.ts"
|
|
32
|
+
import { defineSchedule } from "eve/schedules";
|
|
33
|
+
import slack from "../channels/slack.js";
|
|
34
|
+
import { scheduleStore } from "../lib/schedule-store.js";
|
|
35
|
+
|
|
36
|
+
export default defineSchedule({
|
|
37
|
+
cron: "* * * * *",
|
|
38
|
+
run({ receive, waitUntil }) {
|
|
39
|
+
waitUntil(
|
|
40
|
+
(async () => {
|
|
41
|
+
const jobs = await scheduleStore.claimDue({
|
|
42
|
+
now: new Date(),
|
|
43
|
+
limit: 25,
|
|
44
|
+
leaseForMs: 5 * 60_000,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await Promise.all(
|
|
48
|
+
jobs.map(async (job) => {
|
|
49
|
+
try {
|
|
50
|
+
await receive(slack, {
|
|
51
|
+
message: [
|
|
52
|
+
`Run dynamic schedule ${job.id}.`,
|
|
53
|
+
"Complete this tenant-owned task:",
|
|
54
|
+
job.prompt,
|
|
55
|
+
].join("\n\n"),
|
|
56
|
+
target: { channelId: job.channelId },
|
|
57
|
+
auth: {
|
|
58
|
+
attributes: {
|
|
59
|
+
tenantId: job.tenantId,
|
|
60
|
+
role: job.ownerRole,
|
|
61
|
+
scheduleId: job.id,
|
|
62
|
+
},
|
|
63
|
+
authenticator: job.authenticator,
|
|
64
|
+
...(job.issuer ? { issuer: job.issuer } : {}),
|
|
65
|
+
principalId: job.ownerId,
|
|
66
|
+
principalType: "user",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
await scheduleStore.complete(job);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
await scheduleStore.release(job, { error, retryAt: new Date(Date.now() + 300_000) });
|
|
72
|
+
}
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
})(),
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`waitUntil` keeps the cron invocation alive until claiming and handoff settle. `receive` starts the same durable runtime used by inbound channel messages.
|
|
82
|
+
|
|
83
|
+
This example uses Slack because it has a proactive target of `{ channelId }`. Any channel that implements `receive` can replace it.
|
|
84
|
+
|
|
85
|
+
Configure Slack normally:
|
|
86
|
+
|
|
87
|
+
```ts title="agent/channels/slack.ts"
|
|
88
|
+
import { connectSlackCredentials } from "@vercel/connect/eve";
|
|
89
|
+
import { slackChannel } from "eve/channels/slack";
|
|
90
|
+
|
|
91
|
+
export default slackChannel({
|
|
92
|
+
credentials: connectSlackCredentials("slack/my-agent"),
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Give the agent CRUD tools
|
|
97
|
+
|
|
98
|
+
Tenant and owner identity come from `ctx.session`, never the model:
|
|
99
|
+
|
|
100
|
+
```ts title="agent/lib/tenant.ts"
|
|
101
|
+
import type { SessionAuthContext, SessionContext } from "eve/context";
|
|
102
|
+
|
|
103
|
+
export function requireScheduleOwner(ctx: SessionContext): {
|
|
104
|
+
tenantId: string;
|
|
105
|
+
userId: string;
|
|
106
|
+
auth: SessionAuthContext;
|
|
107
|
+
} {
|
|
108
|
+
const auth = ctx.session.auth.current;
|
|
109
|
+
const tenantId = auth?.attributes.tenantId;
|
|
110
|
+
if (auth?.principalType !== "user" || typeof tenantId !== "string") {
|
|
111
|
+
throw new Error("An authenticated tenant user is required.");
|
|
112
|
+
}
|
|
113
|
+
return { tenantId, userId: auth.principalId, auth };
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Create a one-time schedule with `everyMinutes: null`, or a recurring one with an interval:
|
|
118
|
+
|
|
119
|
+
```ts title="agent/tools/create_schedule.ts"
|
|
120
|
+
import { defineTool } from "eve/tools";
|
|
121
|
+
import { z } from "zod";
|
|
122
|
+
import { scheduleStore } from "../lib/schedule-store.js";
|
|
123
|
+
import { requireScheduleOwner } from "../lib/tenant.js";
|
|
124
|
+
|
|
125
|
+
export default defineTool({
|
|
126
|
+
description: "Create a one-time or repeating scheduled agent run for this tenant.",
|
|
127
|
+
inputSchema: z.object({
|
|
128
|
+
prompt: z.string().min(1).max(8000),
|
|
129
|
+
channelId: z.string().min(1),
|
|
130
|
+
firstRunAt: z.string().datetime({ offset: true }),
|
|
131
|
+
everyMinutes: z.number().int().min(1).max(525600).nullable().default(null),
|
|
132
|
+
}),
|
|
133
|
+
async execute(input, ctx) {
|
|
134
|
+
return await scheduleStore.create(requireScheduleOwner(ctx), {
|
|
135
|
+
...input,
|
|
136
|
+
firstRunAt: new Date(input.firstRunAt),
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```ts title="agent/tools/list_schedules.ts"
|
|
143
|
+
import { defineTool } from "eve/tools";
|
|
144
|
+
import { z } from "zod";
|
|
145
|
+
import { scheduleStore } from "../lib/schedule-store.js";
|
|
146
|
+
import { requireScheduleOwner } from "../lib/tenant.js";
|
|
147
|
+
|
|
148
|
+
export default defineTool({
|
|
149
|
+
description: "List this tenant's dynamic schedules and their latest status.",
|
|
150
|
+
inputSchema: z.object({}),
|
|
151
|
+
async execute(_input, ctx) {
|
|
152
|
+
return await scheduleStore.list(requireScheduleOwner(ctx));
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```ts title="agent/tools/update_schedule.ts"
|
|
158
|
+
import { defineTool } from "eve/tools";
|
|
159
|
+
import { z } from "zod";
|
|
160
|
+
import { scheduleStore } from "../lib/schedule-store.js";
|
|
161
|
+
import { requireScheduleOwner } from "../lib/tenant.js";
|
|
162
|
+
|
|
163
|
+
export default defineTool({
|
|
164
|
+
description: "Change, pause, or resume one of this tenant's schedules.",
|
|
165
|
+
inputSchema: z.object({
|
|
166
|
+
id: z.string().uuid(),
|
|
167
|
+
prompt: z.string().min(1).max(8000).optional(),
|
|
168
|
+
channelId: z.string().min(1).optional(),
|
|
169
|
+
nextRunAt: z.string().datetime({ offset: true }).optional(),
|
|
170
|
+
everyMinutes: z.number().int().min(1).max(525600).nullable().optional(),
|
|
171
|
+
enabled: z.boolean().optional(),
|
|
172
|
+
}),
|
|
173
|
+
async execute({ id, nextRunAt, ...patch }, ctx) {
|
|
174
|
+
return await scheduleStore.update(requireScheduleOwner(ctx), id, {
|
|
175
|
+
...patch,
|
|
176
|
+
...(nextRunAt ? { nextRunAt: new Date(nextRunAt) } : {}),
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```ts title="agent/tools/delete_schedule.ts"
|
|
183
|
+
import { defineTool } from "eve/tools";
|
|
184
|
+
import { always } from "eve/tools/approval";
|
|
185
|
+
import { z } from "zod";
|
|
186
|
+
import { scheduleStore } from "../lib/schedule-store.js";
|
|
187
|
+
import { requireScheduleOwner } from "../lib/tenant.js";
|
|
188
|
+
|
|
189
|
+
export default defineTool({
|
|
190
|
+
description: "Permanently delete one of this tenant's schedules.",
|
|
191
|
+
inputSchema: z.object({ id: z.string().uuid() }),
|
|
192
|
+
approval: always(),
|
|
193
|
+
async execute({ id }, ctx) {
|
|
194
|
+
return { deleted: await scheduleStore.delete(requireScheduleOwner(ctx), id) };
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Supply the schedule adapter
|
|
200
|
+
|
|
201
|
+
The eve-facing implementation depends on this shape, not a database schema:
|
|
202
|
+
|
|
203
|
+
```ts title="agent/lib/schedule-store.ts"
|
|
204
|
+
import type { SessionAuthContext } from "eve/context";
|
|
205
|
+
|
|
206
|
+
export interface ScheduleOwner {
|
|
207
|
+
tenantId: string;
|
|
208
|
+
userId: string;
|
|
209
|
+
auth: SessionAuthContext;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface ClaimedSchedule {
|
|
213
|
+
id: string;
|
|
214
|
+
leaseToken: string;
|
|
215
|
+
tenantId: string;
|
|
216
|
+
ownerId: string;
|
|
217
|
+
ownerRole: string;
|
|
218
|
+
authenticator: string;
|
|
219
|
+
issuer?: string;
|
|
220
|
+
prompt: string;
|
|
221
|
+
channelId: string;
|
|
222
|
+
everyMinutes: number | null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface ScheduleStore {
|
|
226
|
+
create(owner: ScheduleOwner, input: unknown): Promise<unknown>;
|
|
227
|
+
list(owner: ScheduleOwner): Promise<unknown[]>;
|
|
228
|
+
update(owner: ScheduleOwner, id: string, patch: unknown): Promise<unknown>;
|
|
229
|
+
delete(owner: ScheduleOwner, id: string): Promise<boolean>;
|
|
230
|
+
claimDue(options: { now: Date; limit: number; leaseForMs: number }): Promise<ClaimedSchedule[]>;
|
|
231
|
+
complete(job: ClaimedSchedule): Promise<void>;
|
|
232
|
+
release(job: ClaimedSchedule, failure: { error: unknown; retryAt: Date }): Promise<void>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export { scheduleStore } from "../../lib/schedule-store.js";
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Implement that adapter with whichever durable store already belongs to your application. It must preserve a few semantics:
|
|
239
|
+
|
|
240
|
+
- user-facing CRUD is always tenant-scoped;
|
|
241
|
+
- `claimDue` atomically leases rows so overlapping minute ticks do not claim the same work;
|
|
242
|
+
- dispatch revalidates the owner and destination before returning a job;
|
|
243
|
+
- `complete` disables one-time rows or computes the next recurring run;
|
|
244
|
+
- expired leases are recoverable.
|
|
245
|
+
|
|
246
|
+
Delivery is at least once. A crash after `receive` succeeds but before `complete` can dispatch again, so side-effecting tasks need application-level idempotency.
|
|
247
|
+
|
|
248
|
+
## Scheduling instructions
|
|
249
|
+
|
|
250
|
+
```md title="agent/instructions.md"
|
|
251
|
+
Before creating a schedule, confirm the user's time zone and destination.
|
|
252
|
+
Convert the first run to ISO 8601 with an explicit offset. Use everyMinutes only
|
|
253
|
+
for repeating work and null for a one-time run. List schedules before changing
|
|
254
|
+
an ambiguous one.
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The eve-specific core is small: four tools, one one-minute `defineSchedule`, and proactive `receive`. Storage and recurrence policy stay behind the application's adapter.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Multi-tenant approvals"
|
|
3
|
+
description: "Resolve tenant policy asynchronously for authored tools, OpenAPI operations, and MCP tools."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
eve's `approval` field is an async policy hook. It receives the active session, qualified tool name, tool input, and previously approved tools. That is enough to ask your application whether this tenant should allow, deny, or require human confirmation for any authored or connection tool.
|
|
7
|
+
|
|
8
|
+
The pattern has two pieces:
|
|
9
|
+
|
|
10
|
+
1. one adapter translates eve's approval context into an application policy request;
|
|
11
|
+
2. tools, OpenAPI connections, and MCP connections reuse that adapter.
|
|
12
|
+
|
|
13
|
+
Tenant policy storage remains yours. It might be a few columns in PostgreSQL, a policy service, an authorization engine, or configuration in a durable KV store.
|
|
14
|
+
|
|
15
|
+
## Adapt tenant policy to eve approval
|
|
16
|
+
|
|
17
|
+
The current caller and initiating caller are both available on the session. This example requires them to belong to the same tenant before consulting policy:
|
|
18
|
+
|
|
19
|
+
```ts title="agent/lib/tenant-approval.ts"
|
|
20
|
+
import type { ApprovalContext, ApprovalStatus } from "eve/tools";
|
|
21
|
+
import { approvalPolicies } from "./approval-policies.js";
|
|
22
|
+
|
|
23
|
+
type Surface = "connection" | "tool";
|
|
24
|
+
|
|
25
|
+
function tenantIdOf(auth: ApprovalContext["session"]["auth"]["current"]): string | null {
|
|
26
|
+
const tenantId = auth?.attributes.tenantId;
|
|
27
|
+
return typeof tenantId === "string" ? tenantId : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function decideTenantApproval(
|
|
31
|
+
surface: Surface,
|
|
32
|
+
ctx: ApprovalContext,
|
|
33
|
+
): Promise<ApprovalStatus> {
|
|
34
|
+
const current = ctx.session.auth.current;
|
|
35
|
+
const tenantId = tenantIdOf(current);
|
|
36
|
+
const initiatorTenantId = tenantIdOf(ctx.session.auth.initiator);
|
|
37
|
+
|
|
38
|
+
if (current?.principalType !== "user" || !tenantId || tenantId !== initiatorTenantId) {
|
|
39
|
+
return { type: "denied", reason: "The session is not pinned to one tenant user." };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const input = ctx.toolInput as Record<string, unknown> | undefined;
|
|
43
|
+
if (typeof input?.tenantId === "string" && input.tenantId !== tenantId) {
|
|
44
|
+
return { type: "denied", reason: "Tool input cannot select another tenant." };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const policy = await approvalPolicies.decide({
|
|
48
|
+
tenantId,
|
|
49
|
+
userId: current.principalId,
|
|
50
|
+
resource: `${surface}:${ctx.toolName}`,
|
|
51
|
+
input,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
switch (policy.decision) {
|
|
55
|
+
case "allow":
|
|
56
|
+
return { type: "approved", reason: policy.reason };
|
|
57
|
+
case "require-approval":
|
|
58
|
+
return "user-approval";
|
|
59
|
+
case "deny":
|
|
60
|
+
return { type: "denied", reason: policy.reason };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For authored tools, `ctx.toolName` is the path-derived name such as `transfer_funds`. For connection tools, it is qualified, such as `billing__updateSubscription` or `support__add_internal_note`. Your policy service can match exact names, connection-wide patterns, roles, amounts, environments, or any other tenant-owned rule.
|
|
66
|
+
|
|
67
|
+
The callback deliberately does not treat `approvedTools` as a session-wide grant. Every call is evaluated. If your policy supports approve-once behavior, consult `ctx.approvedTools` explicitly after pinning the session tenant.
|
|
68
|
+
|
|
69
|
+
## Apply it to an authored tool
|
|
70
|
+
|
|
71
|
+
Approval runs before `execute`. The executor must still derive and enforce tenancy again because approval is a gate, not authorization:
|
|
72
|
+
|
|
73
|
+
```ts title="agent/tools/transfer_funds.ts"
|
|
74
|
+
import { defineTool } from "eve/tools";
|
|
75
|
+
import { z } from "zod";
|
|
76
|
+
import { transferFunds } from "../../lib/payments.js";
|
|
77
|
+
import { decideTenantApproval } from "../lib/tenant-approval.js";
|
|
78
|
+
|
|
79
|
+
export default defineTool({
|
|
80
|
+
description: "Transfer funds from the current tenant's account.",
|
|
81
|
+
inputSchema: z.object({
|
|
82
|
+
destinationAccountId: z.string().min(1),
|
|
83
|
+
amount: z.number().positive(),
|
|
84
|
+
currency: z.string().length(3),
|
|
85
|
+
}),
|
|
86
|
+
approval: (ctx) => decideTenantApproval("tool", ctx),
|
|
87
|
+
async execute(input, ctx) {
|
|
88
|
+
const tenantId = ctx.session.auth.current?.attributes.tenantId;
|
|
89
|
+
if (typeof tenantId !== "string") {
|
|
90
|
+
throw new Error("An authenticated tenant is required.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return await transferFunds({
|
|
94
|
+
...input,
|
|
95
|
+
tenantId,
|
|
96
|
+
idempotencyKey: `${ctx.session.id}:${ctx.session.turn.id}`,
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use an application idempotency key for side effects. Human approval and replay safety solve different problems.
|
|
103
|
+
|
|
104
|
+
## Apply it to an OpenAPI connection
|
|
105
|
+
|
|
106
|
+
The same callback gates every generated operation. The qualified operation name lets tenant policy distinguish reads from writes:
|
|
107
|
+
|
|
108
|
+
```ts title="agent/connections/billing.ts"
|
|
109
|
+
import { defineOpenAPIConnection } from "eve/connections";
|
|
110
|
+
import { decideTenantApproval } from "../lib/tenant-approval.js";
|
|
111
|
+
|
|
112
|
+
export default defineOpenAPIConnection({
|
|
113
|
+
spec: "https://billing.example.com/openapi.json",
|
|
114
|
+
description: "Billing operations for the authenticated tenant.",
|
|
115
|
+
operations: { allow: ["listInvoices", "updateSubscription"] },
|
|
116
|
+
headers: async (ctx) => {
|
|
117
|
+
const tenantId = ctx.session.auth.current?.attributes.tenantId;
|
|
118
|
+
if (typeof tenantId !== "string") throw new Error("Tenant is required.");
|
|
119
|
+
return {
|
|
120
|
+
"X-Service-Token": process.env.BILLING_SERVICE_TOKEN!,
|
|
121
|
+
"X-Tenant-Id": tenantId,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
approval: (ctx) => decideTenantApproval("connection", ctx),
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The allow-list limits what the model can discover. Approval independently decides whether a discovered operation may run.
|
|
129
|
+
|
|
130
|
+
## Apply it to an MCP connection
|
|
131
|
+
|
|
132
|
+
```ts title="agent/connections/support.ts"
|
|
133
|
+
import { defineMcpClientConnection } from "eve/connections";
|
|
134
|
+
import { decideTenantApproval } from "../lib/tenant-approval.js";
|
|
135
|
+
|
|
136
|
+
export default defineMcpClientConnection({
|
|
137
|
+
url: "https://support.example.com/mcp",
|
|
138
|
+
description: "Support tickets for the authenticated tenant.",
|
|
139
|
+
tools: { allow: ["search_tickets", "add_internal_note"] },
|
|
140
|
+
headers: async (ctx) => {
|
|
141
|
+
const tenantId = ctx.session.auth.current?.attributes.tenantId;
|
|
142
|
+
if (typeof tenantId !== "string") throw new Error("Tenant is required.");
|
|
143
|
+
return {
|
|
144
|
+
"X-Service-Token": process.env.SUPPORT_SERVICE_TOKEN!,
|
|
145
|
+
"X-Tenant-Id": tenantId,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
approval: (ctx) => decideTenantApproval("connection", ctx),
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The policy receives `connection:support__search_tickets` or `connection:support__add_internal_note` as its resource.
|
|
153
|
+
|
|
154
|
+
## Supply the policy adapter
|
|
155
|
+
|
|
156
|
+
The eve code needs only this interface:
|
|
157
|
+
|
|
158
|
+
```ts title="agent/lib/approval-policies.ts"
|
|
159
|
+
export interface ApprovalPolicyRequest {
|
|
160
|
+
tenantId: string;
|
|
161
|
+
userId: string;
|
|
162
|
+
resource: string;
|
|
163
|
+
input?: Record<string, unknown>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface ApprovalPolicyDecision {
|
|
167
|
+
decision: "allow" | "deny" | "require-approval";
|
|
168
|
+
reason?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface ApprovalPolicyProvider {
|
|
172
|
+
decide(request: ApprovalPolicyRequest): Promise<ApprovalPolicyDecision>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { approvalPolicies } from "../../lib/approval-policies.js";
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Your provider decides the policy model. A common implementation checks active tenant membership, finds an exact resource rule before a connection-wide fallback, evaluates role and input thresholds, and defaults to deny. Keep those choices in application code rather than encoding a database design into the agent.
|
|
179
|
+
|
|
180
|
+
Policy lookup failures should throw or deny, never silently allow. Recheck authorization inside side-effecting executors because membership or policy can change while a run is parked.
|
|
181
|
+
|
|
182
|
+
## Protect the approval response
|
|
183
|
+
|
|
184
|
+
An approval durably pauses the session and a later request resumes it. Your HTTP boundary must ensure a caller cannot continue or stream a session owned by another tenant. Persist session ownership in your application and check it before proxying:
|
|
185
|
+
|
|
186
|
+
- `POST /eve/v1/session/:sessionId`, including `inputResponses`;
|
|
187
|
+
- `GET /eve/v1/session/:sessionId/stream`.
|
|
188
|
+
|
|
189
|
+
Built-in approval confirms that a human with access to the session approved the call. It is not a four-eyes workflow that proves a different person or role approved it. For that requirement, create an application-owned approval request, notify eligible approvers through a channel, and have policy return allow only after that request records an authorized decision.
|
|
190
|
+
|
|
191
|
+
The complete eve integration is one async adapter reused by tools and both connection protocols. The tenant's rule storage and governance model remain application concerns.
|