experimental-ash 0.8.0 → 0.8.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 +28 -0
- package/dist/src/chunks/{dev-authored-source-watcher-BFC_yNcP.js → dev-authored-source-watcher-CDT0dQQf.js} +1 -1
- package/dist/src/chunks/{host-DMccRKcz.js → host-M56X595D.js} +2 -2
- package/dist/src/chunks/{paths-B-aiDznc.js → paths-B-Onq-sx.js} +25 -25
- package/dist/src/chunks/{prewarm-CCbReSNm.js → prewarm-DQzlGOTi.js} +1 -1
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/authored-definition/connection.js +10 -0
- package/dist/src/public/channels/ash.d.ts +8 -1
- package/dist/src/public/channels/slack/index.d.ts +1 -1
- package/dist/src/public/channels/slack/slack.js +8 -8
- package/dist/src/public/channels/slack/slackChannel.d.ts +14 -3
- package/dist/src/public/channels/slack/slackChannel.js +39 -48
- package/dist/src/runtime/connections/types.d.ts +21 -0
- package/dist/src/runtime/connections/validate-authorization.js +27 -2
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{E as e,S as t,T as n,_ as r,a as i,c as a,d as o,g as s,h as c,l,m as u,p as ee,s as te,u as d,x as f,y as p}from"./paths-B-
|
|
1
|
+
import{E as e,S as t,T as n,_ as r,a as i,c as a,d as o,g as s,h as c,l,m as u,p as ee,s as te,u as d,x as f,y as p}from"./paths-B-Onq-sx.js";import{t as m}from"./authored-module-loader-Pt_g8xX2.js";import{t as h}from"./errors-DsO9xmQL.js";import{i as g,t as _}from"./package-DmsQgn4v.js";import{join as v,posix as y}from"node:path";import{mkdir as ne,readFile as re,readdir as ie,realpath as b,writeFile as x}from"node:fs/promises";import{createHash as S}from"node:crypto";import{existsSync as C}from"node:fs";function w(e){return e.dev?{appRoot:e.appRoot,dev:e.dev,moduleMapLoaderPath:g(`src/internal/authored-module-map-loader.ts`)}:{appRoot:e.appRoot,dev:e.dev}}const T=`#ash-channel/`;function E(e){let t=e.compileResult.manifest.channels,n=new Set,r=[],i=new Set,o=a();for(let e of t){if(e.kind===`disabled`){if(!o.has(e.name))throw Error(`agent/channels/${e.name}.ts exports disableRoute() but "${e.name}" is not a framework channel. Rename the file to one of: ${[...o].sort().join(`, `)}.`);i.add(e.name);continue}n.add(e.name),r.push({method:e.method,route:e.urlPath})}let s=l().filter(e=>!n.has(e.name)&&!i.has(e.name)).map(e=>({method:e.method,route:e.urlPath})),c=new Set,u=[];for(let e of[...s,...r]){let t=k(e);c.has(t)||(c.add(t),u.push(e))}return u}function D(e,t){for(let n of t.registrations)A(e,{artifactsConfig:t.artifactsConfig,method:n.method,route:n.route})}function O(e,t){return N(t.previous,t.next)?!1:(j(e),D(e,{artifactsConfig:t.artifactsConfig,registrations:t.next}),e.routing.sync(),!0)}function k(e){return`${e.method.toUpperCase()} ${e.route}`}function A(e,t){let r=k(t),i=`${T}${r}`,a=n(g(`src/internal/nitro/routes/channel-dispatch.ts`));e.options.handlers.push({handler:i,method:t.method,route:t.route}),e.options.virtual[i]=[`import { dispatchChannelRequest } from ${a};`,`const config = ${JSON.stringify(t.artifactsConfig)};`,`export default (event) => dispatchChannelRequest(event, ${JSON.stringify(r)}, config);`].join(`
|
|
2
2
|
`)}function j(e){for(let t=e.options.handlers.length-1;t>=0;--t){let n=e.options.handlers[t];n!==void 0&&M(n)&&e.options.handlers.splice(t,1)}for(let t of Object.keys(e.options.virtual))t.startsWith(T)&&delete e.options.virtual[t]}function M(e){return e.handler.startsWith(T)}function N(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1){let r=e[n],i=t[n];if(r===void 0||i===void 0||r.method!==i.method||r.route!==i.route)return!1}return!0}const P=`ash.schedule.`;var F=class extends Error{scheduleId;sourceId;taskName;constructor(e,t={}){super(e),this.name=`ScheduleRegistrationError`,t.scheduleId!==void 0&&(this.scheduleId=t.scheduleId),t.sourceId!==void 0&&(this.sourceId=t.sourceId),t.taskName!==void 0&&(this.taskName=t.taskName)}};function I(e){let t=e.map(e=>({cron:e.cron,description:`Run Ash schedule "${e.name}" from "${e.logicalPath}".`,logicalPath:e.logicalPath,scheduleId:e.name,sourceId:e.sourceId,taskName:R(e.sourceId)})).sort((e,t)=>e.sourceId.localeCompare(t.sourceId));return L(t),t}function L(e){let t=new Map;for(let n of e){let e=t.get(n.scheduleId);if(e===void 0){t.set(n.scheduleId,n);continue}throw new F(`Duplicate authored schedule id "${n.scheduleId}" found in "${e.logicalPath}" and "${n.logicalPath}".`,{scheduleId:n.scheduleId,sourceId:n.sourceId,taskName:n.taskName})}}function R(e){return`${P}${Buffer.from(e,`utf8`).toString(`base64url`)}`}const z=`#ash-schedule-task/`;function B(e,t){if(t.registrations.length!==0){e.options.experimental.tasks=!0;for(let n of t.registrations)U(e,{artifactsConfig:t.artifactsConfig,dispatchModulePath:t.dispatchModulePath,registration:n})}}function V(e,t){let n=!G(t.previous,t.next);return H(e),B(e,{artifactsConfig:t.artifactsConfig,dispatchModulePath:t.dispatchModulePath,registrations:t.next}),n}function H(e){for(let t of Object.keys(e.options.tasks))t.startsWith(`ash.schedule.`)&&delete e.options.tasks[t];for(let t of Object.keys(e.options.virtual))t.startsWith(z)&&delete e.options.virtual[t];for(let[t,n]of Object.entries(e.options.scheduledTasks)){let r=W(n).filter(e=>!e.startsWith(P));if(r.length===0){delete e.options.scheduledTasks[t];continue}if(r.length===1){let[n]=r;n!==void 0&&(e.options.scheduledTasks[t]=n);continue}e.options.scheduledTasks[t]=r}}function U(e,t){let r=`${z}${t.registration.taskName}`,i=n(t.dispatchModulePath);e.options.tasks[t.registration.taskName]={description:t.registration.description,handler:r},e.options.virtual[r]=[`import { dispatchScheduleTask } from ${i};`,`const config = ${JSON.stringify(t.artifactsConfig)};`,`export default {`,` meta: { description: ${JSON.stringify(t.registration.description)} },`,` async run(event) {`,` return { result: await dispatchScheduleTask(event.name, config) };`,` },`,`};`].join(`
|
|
3
3
|
`),ae(e,t.registration.cron,t.registration.taskName)}function ae(e,t,n){let r=e.options.scheduledTasks[t];if(r===void 0){e.options.scheduledTasks[t]=n;return}if(typeof r==`string`){e.options.scheduledTasks[t]=[r,n];return}r.includes(n)||r.push(n)}function W(e){return typeof e==`string`?[e]:[...e]}function G(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1){let r=e[n],i=t[n];if(r===void 0||i===void 0||r.cron!==i.cron||r.description!==i.description||r.logicalPath!==i.logicalPath||r.scheduleId!==i.scheduleId||r.sourceId!==i.sourceId||r.taskName!==i.taskName)return!1}return!0}async function K(e){return[...e.manifest.schedules].map(e=>{let t={cron:e.cron,logicalPath:e.logicalPath,markdown:e.markdown,name:e.name,sourceId:e.sourceId,sourceKind:e.sourceKind};return e.channel===void 0?t:{...t,channel:e.channel}})}async function q(e){return await K({manifest:await o({compiledArtifactsSource:e.compiledArtifactsSource})})}async function J(e){let t=v(e.outDir,`compiled-artifacts-bootstrap.mjs`),n=v(e.outDir,`compiled-artifacts-instrumentation.mjs`),r=se(e.compileResult.manifest.agentRoot);await ne(e.outDir,{recursive:!0}),await x(t,await le({compileResult:e.compileResult,installModulePath:g(`src/runtime/loaders/bundled-artifacts.ts`),moduleMapPath:t,metadata:e.compileResult.metadata})),r!==void 0&&await x(n,ue({agentName:e.compileResult.manifest.config.name,instrumentationPath:r,registerConfigPath:g(`src/harness/instrumentation-config.ts`)}));let i={bootstrapPath:t};return r!==void 0&&(i.instrumentationPluginPath=n,i.instrumentationSourcePath=r),i}const oe=[`.ts`,`.mts`,`.js`,`.mjs`];function se(e){for(let t of oe){let n=v(e,`instrumentation${t}`);if(C(n))return n}}function ce(e){return e.replace(/^export const moduleMap = /m,`const moduleMap = `).replace(/\nexport default moduleMap;\n?$/,`
|
|
4
4
|
`)}async function le(e){let r=ce(t({importSpecifierStyle:`absolute`,manifest:e.compileResult.manifest,moduleMapPath:e.moduleMapPath})).trim();return[`// Generated by Ash. Do not edit by hand.`,`import { installBundledCompiledArtifacts } from ${n(e.installModulePath)};`,``,r,``,`const metadata = ${JSON.stringify(e.metadata,null,2)};`,``,`const manifest = ${JSON.stringify(e.compileResult.manifest,null,2)};`,``,`export function installCompiledArtifactsBootstrap() {`,` installBundledCompiledArtifacts({`,` manifest,`,` metadata,`,` moduleMap,`,` });`,`}`,``,`installCompiledArtifactsBootstrap();`,``,`// Default export satisfies the Nitro plugin contract so this file`,`// can be used directly as a Nitro plugin without a separate wrapper.`,`export default function installCompiledArtifactsPlugin() {`,` // Already installed on import above.`,`}`,``,`export async function __ashInstallCompiledArtifactsStep() {`,` "use step";`,` return null;`,`}`,``].join(`
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{D as e,b as t,t as n,v as r,y as i}from"../../chunks/paths-B-
|
|
1
|
+
import{D as e,b as t,t as n,v as r,y as i}from"../../chunks/paths-B-Onq-sx.js";import{d as a,f as o,h as s}from"../../chunks/types-MZUhN0Zy.js";import{createCliTheme as c,renderCliBanner as l,renderCliSection as u}from"../ui/output.js";async function d(e){let t=await f(e);return{application:n(t?.project.appRoot??e),compiledState:t,messaging:{createSessionRoutePath:o,continueSessionRoutePattern:a,streamRoutePattern:s}}}async function f(n){try{return await i({startPath:n})}catch(n){if(n instanceof r)return n.result;if(n instanceof e||n instanceof t)return null;throw n}}function p(e,t){return`${e} ${t}${e===1?``:`s`}`}function m(e,t){return`${`${e} error${e===1?``:`s`}`}, ${`${t} warning${t===1?``:`s`}`}`}function h(e){switch(e){case`ready`:return`success`;case`failed`:return`danger`;default:return`warning`}}async function g(e,t){let n=await d(t),r=n.compiledState,i=n.application,a=c(),o=[{label:`App Root`,value:i.appRoot}],s=[{label:`Workflow Build`,value:i.workflowBuildDir},{label:`Output`,value:i.outputDir}],f=[];r===null?o.push({label:`Compile`,tone:`warning`,value:`unavailable`}):(o.push({label:`Agent Root`,value:r.project.agentRoot},{label:`Layout`,value:r.project.layout},{label:`Compile`,tone:h(r.metadata.status),value:r.metadata.status},{label:`Diagnostics`,tone:r.metadata.discovery.summary.errors>0?`danger`:r.metadata.discovery.summary.warnings>0?`warning`:`success`,value:m(r.metadata.discovery.summary.errors,r.metadata.discovery.summary.warnings)},{label:`Instructions`,value:r.manifest.instructions?.logicalPath??`none`},{label:`Skills`,value:p(r.manifest.skills.length,`skill`)}),s.unshift({label:`Compiled Manifest`,value:r.paths.compiledManifestPath},{label:`Discovery Manifest`,value:r.paths.discoveryManifestPath},{label:`Diagnostics`,value:r.paths.diagnosticsPath},{label:`Module Map`,value:r.paths.moduleMapPath},{label:`Metadata`,value:r.paths.compileMetadataPath}),f.push(r.manifest.instructions===void 0?{label:`Instructions`,value:`No instructions prompt discovered.`}:{label:`Instructions`,value:r.manifest.instructions.logicalPath})),e.log([l(a,{subtitle:`Resolved application paths and the active message contract.`,title:`Ash Info`}),``,u(a,{rows:o,title:`Application`}),``,u(a,{rows:s,title:`Artifacts`}),...r===null?[]:[``,u(a,{rows:f,title:`Instructions`})],``,u(a,{rows:[{label:`Workflow ID`,value:i.workflowId},{label:`Source Dir`,value:i.workflowSourceDir},{label:`Create`,tone:`info`,value:`POST ${n.messaging.createSessionRoutePath}`},{label:`Continue`,tone:`info`,value:`POST ${n.messaging.continueSessionRoutePattern}`},{label:`Stream`,tone:`info`,value:`GET ${n.messaging.streamRoutePattern}`}],title:`Messaging`})].join(`
|
|
2
2
|
`))}export{g as printApplicationInfo};
|
package/dist/src/cli/run.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{t as e}from"../chunks/package-DmsQgn4v.js";import{createCliTheme as t,renderCliTaggedLine as n}from"./ui/output.js";import{i as r,n as i,r as a,t as o}from"../chunks/url-BVRhVE2O.js";import{resolve as s}from"node:path";async function c(){return(await import(`../chunks/host-
|
|
1
|
+
import{t as e}from"../chunks/package-DmsQgn4v.js";import{createCliTheme as t,renderCliTaggedLine as n}from"./ui/output.js";import{i as r,n as i,r as a,t as o}from"../chunks/url-BVRhVE2O.js";import{resolve as s}from"node:path";async function c(){return(await import(`../chunks/host-M56X595D.js`).then(e=>e.t)).buildHost}async function l(){return(await import(`./commands/info.js`)).printApplicationInfo}async function u(){return(await import(`./dev/repl.js`)).runDevelopmentRepl}async function d(){return(await import(`../evals/cli/eval.js`)).runEvalCommand}async function f(){return(await import(`../chunks/host-M56X595D.js`).then(e=>e.t)).startHost}function p(e=process.cwd()){return s(e)}function m(e){return`Ash (v${e})`}function h(e){return e.name()===`info`||e.name()===`dev`}async function g(e){await new Promise((t,n)=>{let r=!1,i=()=>{process.off(`SIGINT`,a),process.off(`SIGTERM`,a)},a=()=>{r||(r=!0,i(),e.close().then(t,n))};process.once(`SIGINT`,a),process.once(`SIGTERM`,a)})}function _(e){if(!/^-?\d+$/.test(e))throw new r(`Expected a numeric port, received "${e}".`);let t=Number(e);if(!Number.isInteger(t))throw new r(`Expected a numeric port, received "${e}".`);if(t<0||t>65535)throw new r(`Expected a port between 0 and 65535, received "${e}".`);return t}function v(){return!!(process.stdin.isTTY&&process.stdout.isTTY)}function y(e){let t=e[1];return e[0]!==`dev`||e.length!==2||t===void 0||t.startsWith(`-`)?[...e]:[`dev`,`--url`,t]}function b(e){if(e.url){if(e.host!==void 0)throw new r(`The --host option cannot be used with --url.`);if(e.port!==void 0)throw new r(`The --port option cannot be used with --url.`);if(e.repl===!1)throw new r(`The --no-repl option cannot be used with --url.`);return e.url}}function x(r,a){let s=p(),y=e().version,x=new i,S=t();return x.name(`ash`).description(`Build and run an Ash application.`).version(y).showHelpAfterError().exitOverride().hook(`preAction`,(e,t)=>{h(t)&&r.log(m(y))}).configureOutput({writeErr:e=>{r.error(e.trimEnd())},writeOut:e=>{r.log(e.trimEnd())}}),x.command(`build`).description(`Build the current Ash application.`).action(async()=>{let{loadDevelopmentEnvironmentFiles:e}=await import(`./dev/environment.js`);e(s);let t=await(a.buildHost??await c())(s);r.log(n(S,{message:`built output at ${t}`,tag:`build`,tone:`success`}))}),x.command(`dev`).description(`Start the Ash development server or connect the REPL to an existing URL.`).option(`--host <host>`,`Host interface to bind`).option(`--no-repl`,`Start the server without the interactive REPL`).option(`--port <port>`,`Port to listen on (defaults to $PORT, then 3000)`,_).option(`--schedules`,`Run scheduled tasks during development (off by default)`).option(`-u, --url <url>`,`Connect the REPL to an existing server URL`,o).addHelpText(`after`,`
|
|
2
2
|
You can also pass a bare URL as the only argument, for example: ash dev https://example.com
|
|
3
3
|
`).action(async e=>{let t=b(e),{loadDevelopmentEnvironmentFiles:i}=await import(`./dev/environment.js`);if(i(s),t){if(r.log(n(S,{message:`REPL connecting to ${t}`,tag:`dev`,tone:`info`})),!v()){r.log(n(S,{message:`Interactive REPL disabled because the current terminal is not a TTY.`,tag:`dev`,tone:`warning`}));return}r.log(``),await(a.runDevelopmentRepl??await u())({serverUrl:t});return}let o=await(a.startHost??await f())(s,{host:e.host,port:e.port,schedules:e.schedules===!0}),c=!1,l=async()=>{c||(c=!0,await o.close())};try{if(r.log(n(S,{message:`server listening at ${o.url}`,tag:`dev`,tone:`success`})),e.repl===!1)return await g({close:l});if(!v())return r.log(n(S,{message:`Interactive REPL disabled because the current terminal is not a TTY.`,tag:`dev`,tone:`warning`})),await g({close:l});r.log(``),await(a.runDevelopmentRepl??await u())({serverUrl:o.url})}finally{await l()}}),x.command(`info`).description(`Print resolved application information.`).action(async()=>{await(a.printApplicationInfo??await l())(r,s)}),x.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await(a.runEvalCommand??await d())(e,r)}),x}async function S(e=process.argv.slice(2),t=console,n={}){let r=x(t,n),i=e.length===0?[`info`]:y(e);try{await r.parseAsync(i,{from:`user`})}catch(e){if(e instanceof a){if(e.exitCode===0)return;throw Error(e.message)}throw e}}export{S as runCli};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"../../chunks/paths-B-
|
|
1
|
+
import{n as e}from"../../chunks/paths-B-Onq-sx.js";import{loadDevelopmentEnvironmentFiles as t}from"../../cli/dev/environment.js";import{a as n,n as r,t as i}from"../../chunks/client-BeZ_W7vl.js";import{n as a}from"../../chunks/host-M56X595D.js";import{discoverAndImportSuites as o,discoverSuiteFiles as s,importSuiteFile as c}from"../runner/discover.js";import{executeSuite as l}from"../runner/execute-suite.js";import{ConsoleReporter as u}from"../runner/reporters/console.js";var d=n();function f(e,t){e.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await p(e,t)})}async function p(n,r){let i=e();if(t(i),n.listSuites){await y(i,r);return}let s=n.suite,c=await o(i,s);if(c.length===0){s&&s.length>0?r.error(`No suites found matching: ${s.join(`, `)}`):r.error(`No eval suites found. Create suite files under evals/ with the *.eval.ts extension.`),process.exitCode=1;return}let u,d;n.url?d={kind:`remote`,url:n.url}:(u=await a(i,{host:`127.0.0.1`,port:0}),d={kind:`local`,url:u.url});let f=m(d);try{let e=[];for(let t of c){let r=_(t,n),a=v(r,{json:n.json===!0,skipReport:n.skipReport===!0}),o=await l({suite:r,target:d,reporters:a,appRoot:i,client:f});e.push(o)}n.json&&r.log(JSON.stringify(e,null,2)),e.some(e=>e.errored>0)&&(process.exitCode=1)}finally{u&&await u.close()}process.exit(process.exitCode??0)}function m(e){if(e.kind===`local`)return new i({host:e.url});let t={},n=process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();return n&&(t[r]=n),new i({auth:h(),headers:Object.keys(t).length>0?t:void 0,host:e.url})}function h(){let e=process.env.ASH_EVAL_AUTH_TOKEN?.trim();return e?{bearer:e}:{bearer:g}}async function g(){try{let e=(await(0,d.getVercelOidcToken)()).trim();if(e.length>0)return e}catch{}return process.env.VERCEL_OIDC_TOKEN?.trim()??``}function _(e,t){let n=t.maxConcurrency?Number.parseInt(t.maxConcurrency,10):void 0,r=t.timeout?Number.parseInt(t.timeout,10):void 0;if(n===void 0&&r===void 0)return e;let i={...e};return n!==void 0&&(i.maxConcurrency=n),r!==void 0&&(i.timeoutMs=r),i}function v(e,t){let n=t.json?[]:[new u];return!t.skipReport&&e.reporters&&n.push(...e.reporters),n}async function y(e,t){let n=await s(e);if(n.length===0){t.log(`No eval suites found.`);return}t.log(`Found ${n.length} eval suite file(s):\n`);for(let r of n){let n=await c(e,r);t.log(` ${n.id}${n.description?` - ${n.description}`:``}`)}}export{f as registerEvalCommand,p as runEvalCommand};
|
|
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#package-name.js";
|
|
|
6
6
|
let cachedPackageInfo;
|
|
7
7
|
// The package build stamps the published version into `dist` so bundled
|
|
8
8
|
// deployments can still report package metadata without resolving package.json.
|
|
9
|
-
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.8.
|
|
9
|
+
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.8.2";
|
|
10
10
|
const BUNDLED_FALLBACK_PACKAGE_VERSION_PLACEHOLDER = "__ASH_PACKAGE_VERSION_PLACEHOLDER__";
|
|
11
11
|
const WORKFLOW_MODULE_ALIASES = {
|
|
12
12
|
"workflow/api": "src/compiled/@workflow/core/runtime.js",
|
|
@@ -13,6 +13,16 @@ const KNOWN_AUTHORIZATION_KEYS = [
|
|
|
13
13
|
"getToken",
|
|
14
14
|
"principalType",
|
|
15
15
|
"startAuthorization",
|
|
16
|
+
// Optional metadata marker that auth-provider helpers may attach so
|
|
17
|
+
// downstream tooling (eg. the Ash compiler / Vercel dashboard) can
|
|
18
|
+
// detect Vercel Connect-backed connections without opening the
|
|
19
|
+
// closure state of `getToken`. The runtime never reads it; it
|
|
20
|
+
// survives `normalizeAuthorizationSpec` so consumers can pick it
|
|
21
|
+
// off the normalized auth definition. See
|
|
22
|
+
// `runtime/connections/types.ts#AuthorizationDefinitionBase` for
|
|
23
|
+
// the type and `@vercel/connect/ash`'s `connect()` for the
|
|
24
|
+
// canonical producer.
|
|
25
|
+
"vercelConnect",
|
|
16
26
|
];
|
|
17
27
|
/**
|
|
18
28
|
* Validates one authored MCP client connection module export at build time
|
|
@@ -5,4 +5,11 @@ export interface AshChannelInput {
|
|
|
5
5
|
readonly auth: AuthFn<Request>;
|
|
6
6
|
readonly uploadPolicy?: Partial<UploadPolicy>;
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Concrete return type of {@link ashChannel}. Named so consumers can
|
|
10
|
+
* default-export an `ashChannel(...)` call under `declaration: true`
|
|
11
|
+
* without TypeScript falling back to an internal path for `Channel`.
|
|
12
|
+
*/
|
|
13
|
+
export interface AshChannel extends Channel {
|
|
14
|
+
}
|
|
15
|
+
export declare function ashChannel(input: AshChannelInput): AshChannel;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { slack, type SlackOptions } from "#public/channels/slack/slack.js";
|
|
2
|
-
export { slackChannel, type SlackApiHandle, type SlackApiResponse, type SlackChannelConfig, type SlackChannelEvents, type SlackChannelCredentials, type SlackChannelState, type SlackContext, type SlackInteractionAction, type SlackReceiveArgs, } from "#public/channels/slack/slackChannel.js";
|
|
2
|
+
export { slackChannel, type SlackApiHandle, type SlackApiResponse, type SlackChannel, type SlackChannelConfig, type SlackChannelEvents, type SlackChannelCredentials, type SlackChannelState, type SlackContext, type SlackInteractionAction, type SlackReceiveArgs, } from "#public/channels/slack/slackChannel.js";
|
|
3
3
|
export { Actions, Button, Card, CardText, Divider, Fields, Image, LinkButton, Modal, RadioSelect, Section, Select, SelectOption, Table, TextInput, } from "#compiled/chat/index.js";
|
|
4
4
|
export type { AdapterPostableMessage, Attachment, Author, CardElement, FileUpload, Message, PostableMessage, SentMessage, Thread, } from "#compiled/chat/index.js";
|
|
@@ -19,21 +19,21 @@ export function slack(options = {}) {
|
|
|
19
19
|
return { auth: resolveAuth(message) };
|
|
20
20
|
},
|
|
21
21
|
events: {
|
|
22
|
-
"turn.started"(_event, ctx) {
|
|
23
|
-
ctx.thread.startTyping("Thinking...");
|
|
22
|
+
async "turn.started"(_event, ctx) {
|
|
23
|
+
await ctx.thread.startTyping("Thinking...");
|
|
24
24
|
},
|
|
25
|
-
"actions.requested"(event, ctx) {
|
|
25
|
+
async "actions.requested"(event, ctx) {
|
|
26
26
|
const labels = event.actions.map((a) => (a.kind === "tool-call" ? a.toolName : a.kind));
|
|
27
|
-
ctx.thread.startTyping(`Running ${labels.join(", ")}...`);
|
|
27
|
+
await ctx.thread.startTyping(`Running ${labels.join(", ")}...`);
|
|
28
28
|
},
|
|
29
|
-
"message.completed"(event, ctx) {
|
|
29
|
+
async "message.completed"(event, ctx) {
|
|
30
30
|
if (event.finishReason === "tool-calls")
|
|
31
31
|
return;
|
|
32
32
|
if (event.message)
|
|
33
|
-
ctx.thread.post(event.message);
|
|
33
|
+
await ctx.thread.post(event.message);
|
|
34
34
|
},
|
|
35
|
-
"session.failed"(_event, ctx) {
|
|
36
|
-
ctx.thread.post("Something went wrong.");
|
|
35
|
+
async "session.failed"(_event, ctx) {
|
|
36
|
+
await ctx.thread.post("Something went wrong.");
|
|
37
37
|
},
|
|
38
38
|
},
|
|
39
39
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
1
|
+
import { type SlackBotToken } from "#compiled/@chat-adapter/slack/index.js";
|
|
2
|
+
import { type Message, type SerializedThread, type Thread } from "#compiled/chat/index.js";
|
|
3
3
|
import type { HandleMessageStreamEvent } from "#protocol/message.js";
|
|
4
4
|
import { type UploadPolicy } from "#public/channels/upload-policy.js";
|
|
5
5
|
import { type Channel } from "#public/definitions/defineChannel.js";
|
|
@@ -72,6 +72,10 @@ export interface SlackChannelEvents {
|
|
|
72
72
|
export interface SlackChannelConfig {
|
|
73
73
|
readonly credentials?: SlackChannelCredentials;
|
|
74
74
|
readonly botName?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Override the default webhook route path (`/ash/v1/slack`).
|
|
77
|
+
*/
|
|
78
|
+
readonly route?: string;
|
|
75
79
|
/**
|
|
76
80
|
* Inbound upload policy applied to file attachments before they
|
|
77
81
|
* reach the harness. Violating attachments are dropped with a
|
|
@@ -83,5 +87,12 @@ export interface SlackChannelConfig {
|
|
|
83
87
|
onInteraction?(action: SlackInteractionAction, ctx: SlackContext): void | Promise<void>;
|
|
84
88
|
readonly events?: SlackChannelEvents;
|
|
85
89
|
}
|
|
86
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Concrete return type of {@link slackChannel}. Named so consumers can
|
|
92
|
+
* default-export a `slackChannel(...)` call under `declaration: true`
|
|
93
|
+
* without TypeScript falling back to an internal path for `Channel`.
|
|
94
|
+
*/
|
|
95
|
+
export interface SlackChannel extends Channel<SlackChannelState> {
|
|
96
|
+
}
|
|
97
|
+
export declare function slackChannel(config?: SlackChannelConfig): SlackChannel;
|
|
87
98
|
export {};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { createSlackAdapter } from "#compiled/@chat-adapter/slack/index.js";
|
|
2
|
+
import { ThreadImpl, } from "#compiled/chat/index.js";
|
|
1
3
|
import { createLogger } from "#internal/logging.js";
|
|
2
4
|
import { buildSlackTurnMessage, collectSlackFileParts, createSlackFetchFile, } from "#public/channels/slack/attachments.js";
|
|
3
5
|
import { deriveHitlResponse, isHitlAction, renderInputRequestBlocks, } from "#public/channels/slack/hitl.js";
|
|
4
6
|
import { mergeUploadPolicy } from "#public/channels/upload-policy.js";
|
|
5
|
-
import { defineChannel, POST
|
|
7
|
+
import { defineChannel, POST } from "#public/definitions/defineChannel.js";
|
|
6
8
|
const log = createLogger("slack.channel");
|
|
7
9
|
function decodeThreadId(id) {
|
|
8
10
|
const parts = id.replace(/^slack:/u, "").split(":");
|
|
@@ -74,15 +76,18 @@ function extractSelectedOptionValue(action) {
|
|
|
74
76
|
return typeof selected?.value === "string" ? selected.value : undefined;
|
|
75
77
|
}
|
|
76
78
|
function rebuildSlackContext(state, botToken) {
|
|
77
|
-
const
|
|
79
|
+
const adapter = createSlackAdapter({
|
|
80
|
+
botToken: botToken ?? process.env.SLACK_BOT_TOKEN,
|
|
81
|
+
});
|
|
78
82
|
const thread = state.serializedThread
|
|
79
|
-
?
|
|
80
|
-
: new
|
|
83
|
+
? ThreadImpl.fromJSON(state.serializedThread, adapter)
|
|
84
|
+
: new ThreadImpl({
|
|
81
85
|
adapterName: "slack",
|
|
82
86
|
channelId: "",
|
|
83
87
|
id: "slack::",
|
|
84
88
|
isDM: false,
|
|
85
89
|
});
|
|
90
|
+
thread._adapter = adapter;
|
|
86
91
|
return {
|
|
87
92
|
thread,
|
|
88
93
|
slack: buildSlackApiHandle(thread, botToken, state.teamId ?? undefined),
|
|
@@ -112,7 +117,6 @@ export function slackChannel(config = {}) {
|
|
|
112
117
|
const slackFetchFile = createSlackFetchFile({
|
|
113
118
|
botToken: config.credentials?.botToken,
|
|
114
119
|
});
|
|
115
|
-
let activeSend = null;
|
|
116
120
|
let chatPromise = null;
|
|
117
121
|
async function getChat() {
|
|
118
122
|
if (chatPromise)
|
|
@@ -139,39 +143,6 @@ export function slackChannel(config = {}) {
|
|
|
139
143
|
userName: config.botName ?? "ash-agent",
|
|
140
144
|
});
|
|
141
145
|
await chat.initialize();
|
|
142
|
-
chat.onNewMention(async (thread, message) => {
|
|
143
|
-
// Slack sends both `app_mention` and `message.channels` for the same message.
|
|
144
|
-
// The Chat SDK dedup relies on in-memory state that doesn't survive across
|
|
145
|
-
// serverless invocations, so both events reach this handler. Only process
|
|
146
|
-
// `app_mention` to prevent duplicate runs.
|
|
147
|
-
const rawEvent = message.raw;
|
|
148
|
-
if (rawEvent?.type !== "app_mention")
|
|
149
|
-
return;
|
|
150
|
-
const send = activeSend;
|
|
151
|
-
if (!send) {
|
|
152
|
-
throw new Error("slackChannel: mention received but no request context is active.");
|
|
153
|
-
}
|
|
154
|
-
const teamId = rawEvent.team_id ?? rawEvent.team;
|
|
155
|
-
const slackCtx = {
|
|
156
|
-
thread,
|
|
157
|
-
slack: buildSlackApiHandle(thread, config.credentials?.botToken, teamId),
|
|
158
|
-
};
|
|
159
|
-
const runOpts = config.run ? config.run(slackCtx, message) : { auth: null };
|
|
160
|
-
if (runOpts === null)
|
|
161
|
-
return;
|
|
162
|
-
const decoded = decodeThreadId(thread.id ?? "");
|
|
163
|
-
const continuationToken = `slack:${decoded.channelId}:${decoded.threadTs}`;
|
|
164
|
-
const fileParts = collectSlackFileParts(message, uploadPolicy);
|
|
165
|
-
const turnMessage = buildSlackTurnMessage(message.text, fileParts);
|
|
166
|
-
await send(turnMessage, {
|
|
167
|
-
auth: runOpts.auth,
|
|
168
|
-
continuationToken,
|
|
169
|
-
state: {
|
|
170
|
-
serializedThread: thread.toJSON(),
|
|
171
|
-
teamId: teamId ?? null,
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
146
|
return { chat };
|
|
176
147
|
})();
|
|
177
148
|
chatPromise = promise;
|
|
@@ -188,7 +159,7 @@ export function slackChannel(config = {}) {
|
|
|
188
159
|
return rebuildSlackContext(state, config.credentials?.botToken);
|
|
189
160
|
},
|
|
190
161
|
routes: [
|
|
191
|
-
POST("/ash/v1/slack", async (req, { send, waitUntil }) => {
|
|
162
|
+
POST(config.route ?? "/ash/v1/slack", async (req, { send, waitUntil }) => {
|
|
192
163
|
const { chat } = await getChat();
|
|
193
164
|
const contentType = req.headers.get("content-type") ?? "";
|
|
194
165
|
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
@@ -249,16 +220,36 @@ export function slackChannel(config = {}) {
|
|
|
249
220
|
}
|
|
250
221
|
return new Response("ok", { status: 200 });
|
|
251
222
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
223
|
+
chat.onNewMention(async (thread, message) => {
|
|
224
|
+
const rawEvent = message.raw;
|
|
225
|
+
// Slack sends both `app_mention` and `message.channels` for the same
|
|
226
|
+
// utterance. The Chat SDK dedup relies on in-memory state that doesn't
|
|
227
|
+
// survive serverless invocations, so both events reach this handler.
|
|
228
|
+
// Only process `app_mention` to prevent duplicate runs.
|
|
229
|
+
if (rawEvent?.type !== "app_mention")
|
|
230
|
+
return;
|
|
231
|
+
const teamId = rawEvent.team_id ?? rawEvent.team;
|
|
232
|
+
const slackCtx = {
|
|
233
|
+
thread,
|
|
234
|
+
slack: buildSlackApiHandle(thread, config.credentials?.botToken, teamId),
|
|
235
|
+
};
|
|
236
|
+
const runOpts = config.run ? config.run(slackCtx, message) : { auth: null };
|
|
237
|
+
if (runOpts === null)
|
|
238
|
+
return;
|
|
239
|
+
const decoded = decodeThreadId(thread.id ?? "");
|
|
240
|
+
const continuationToken = `slack:${decoded.channelId}:${decoded.threadTs}`;
|
|
241
|
+
const fileParts = collectSlackFileParts(message, uploadPolicy);
|
|
242
|
+
const turnMessage = buildSlackTurnMessage(message.text, fileParts);
|
|
243
|
+
await send(turnMessage, {
|
|
244
|
+
auth: runOpts.auth,
|
|
245
|
+
continuationToken,
|
|
246
|
+
state: {
|
|
247
|
+
serializedThread: thread.toJSON(),
|
|
248
|
+
teamId: teamId ?? null,
|
|
249
|
+
},
|
|
257
250
|
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
activeSend = null;
|
|
261
|
-
}
|
|
251
|
+
});
|
|
252
|
+
return await chat.webhooks.slack(req, { waitUntil });
|
|
262
253
|
}),
|
|
263
254
|
],
|
|
264
255
|
async receive(input, { send }) {
|
|
@@ -127,6 +127,27 @@ interface AuthorizationDefinitionBase {
|
|
|
127
127
|
* full design.
|
|
128
128
|
*/
|
|
129
129
|
readonly principalType: "app" | "user";
|
|
130
|
+
/**
|
|
131
|
+
* Optional metadata marker attached by `connect()` from
|
|
132
|
+
* `@vercel/connect/ash` so downstream tooling — eg. a future Ash
|
|
133
|
+
* compiler step that surfaces connector identifiers in build output,
|
|
134
|
+
* or the Vercel dashboard rendering deep links to a connector's
|
|
135
|
+
* settings page — can detect Vercel Connect-backed connections at
|
|
136
|
+
* compile time without inspecting `getToken`'s closure state.
|
|
137
|
+
*
|
|
138
|
+
* The runtime token-fetch path ignores this field; it is purely
|
|
139
|
+
* provider attribution. Authors writing their own `getToken`
|
|
140
|
+
* callbacks (raw bearer tokens, custom callbacks) should leave it
|
|
141
|
+
* unset.
|
|
142
|
+
*
|
|
143
|
+
* `connector` carries whatever value the author passed to
|
|
144
|
+
* `connect()` — UID like `"oauth/mcp-linear-app"` or opaque
|
|
145
|
+
* `"scl_..."`; both forms address the same connector on the Vercel
|
|
146
|
+
* Connect side.
|
|
147
|
+
*/
|
|
148
|
+
readonly vercelConnect?: {
|
|
149
|
+
readonly connector: string;
|
|
150
|
+
};
|
|
130
151
|
}
|
|
131
152
|
/**
|
|
132
153
|
* Non-interactive authorization: the runtime only ever calls
|
|
@@ -67,17 +67,42 @@ export function normalizeAuthorizationSpec(authorization, prefix, fieldName = "a
|
|
|
67
67
|
throw new Error(`${prefix} ${message}`);
|
|
68
68
|
}
|
|
69
69
|
const auth = authorization;
|
|
70
|
+
const vercelConnect = extractVercelConnectMarker(auth.vercelConnect);
|
|
70
71
|
if (auth.startAuthorization !== undefined && auth.completeAuthorization !== undefined) {
|
|
71
|
-
|
|
72
|
+
const interactive = {
|
|
72
73
|
completeAuthorization: auth.completeAuthorization,
|
|
73
74
|
getToken: auth.getToken,
|
|
74
75
|
principalType: "user",
|
|
75
76
|
startAuthorization: auth.startAuthorization,
|
|
76
77
|
};
|
|
78
|
+
return vercelConnect === undefined ? interactive : { ...interactive, vercelConnect };
|
|
77
79
|
}
|
|
78
|
-
|
|
80
|
+
const nonInteractive = {
|
|
79
81
|
getToken: auth.getToken,
|
|
80
82
|
principalType: (auth.principalType ??
|
|
81
83
|
"app"),
|
|
82
84
|
};
|
|
85
|
+
return vercelConnect === undefined ? nonInteractive : { ...nonInteractive, vercelConnect };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Reads the optional `vercelConnect: { connector: string }` marker
|
|
89
|
+
* attached by `@vercel/connect/ash`'s `connect()` helper. Returns the
|
|
90
|
+
* parsed marker when present and well-formed, otherwise `undefined`.
|
|
91
|
+
*
|
|
92
|
+
* The marker is opaque to the runtime — it exists so downstream tooling
|
|
93
|
+
* (eg. the Ash compiler / Vercel dashboard) can attribute the auth
|
|
94
|
+
* back to a Vercel Connect connector without inspecting `getToken`'s
|
|
95
|
+
* closure state. Validation is lenient (a malformed marker is dropped,
|
|
96
|
+
* not thrown) so a misbehaving auth provider can't fail an otherwise-
|
|
97
|
+
* valid connection.
|
|
98
|
+
*/
|
|
99
|
+
function extractVercelConnectMarker(value) {
|
|
100
|
+
if (value === null || typeof value !== "object") {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const connector = value.connector;
|
|
104
|
+
if (typeof connector !== "string" || connector.length === 0) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return { connector };
|
|
83
108
|
}
|