Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions server/src/workflowsTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,20 @@ export async function workflowsTabRoutes(app: FastifyInstance): Promise<void> {

// What an administrator can pick from: every workflow on the account the
// Studio is deployed under, which on a site deployment is the site's list.
// When the deployment's credential is missing, expired, or rejected, the
// viewer's own key lists their account instead, and the answer says so.
app.get('/api/workflows/catalog', async req => {
const key = gatewayKey() ?? viewer(req).key
const rows = await listFor(key)
const dep = gatewayKey()
const mine = platformKeyFor(req.user?.id)
let rows: WorkflowRow[] | null = null
let source: 'deployment' | 'viewer' = 'deployment'
let depError: unknown = null
if (dep) rows = await listFor(dep).catch(e => { depError = e; return null })
if (!rows && mine.own && mine.key) { rows = await listFor(mine.key); source = 'viewer' }
if (!rows) throw depError ?? new KbError(409, 'No platform credential: add your ACTIVATE API key under Settings, Model access.')
const curated = new Set(curatedNames())
return {
source,
workflows: rows.map(w => ({
name: w.name,
displayName: w.displayName || w.name,
Expand Down
21 changes: 21 additions & 0 deletions server/test/workflowsTab.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,24 @@ test('an expired platform credential is reported as such, not as an internal err
assert.equal(other.status ?? other.statusCode, 502)
assert.equal(other.message, 'The platform did not answer: connection reset by peer')
})

test('with the deployment credential expired, the catalog lists the viewer\'s own account', async () => {
const { setUserKey } = await import('../dist/credentials.js')
setUserKey('viewer-1', 'viewer-key', false)
const app2 = Fastify()
app2.setErrorHandler(sanitizedErrorHandler(app2))
app2.addHook('onRequest', async req => { req.user = { id: 'viewer-1', username: 'viewer' } })
await app2.register(workflowsTabRoutes)
await app2.ready()
resetWorkflowsTabForTests()
setWorkflowsCli(async (args, key) => {
if (key === 'deployment-key') throw new Error("2026-10-02T14:33:54Z [ERROR] Authentication has expired. Please authenticate again using 'pw auth'.")
if (args[1] === 'ls') return JSON.stringify([{ name: 'mine-only', displayName: 'Mine only' }])
throw new Error(`unexpected ${args.join(' ')}`)
})
const res = await app2.inject({ method: 'GET', url: '/api/workflows/catalog' })
assert.equal(res.statusCode, 200)
const body = res.json()
assert.equal(body.source, 'viewer')
assert.deepEqual(body.workflows.map(w => w.name), ['mine-only'])
})
5 changes: 5 additions & 0 deletions web/src/components/WorkflowsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface CatalogRow { name: string; displayName: string; description: string; t

export function WorkflowsSection() {
const [catalog, setCatalog] = useState<CatalogRow[] | null>(null)
const [source, setSource] = useState<'deployment' | 'viewer'>('deployment')
const [picked, setPicked] = useState<string[]>([])
const [filter, setFilter] = useState('')
const [note, setNote] = useState('')
Expand All @@ -23,6 +24,7 @@ export function WorkflowsSection() {
const d = await r.json()
if (!r.ok) throw new Error(d.error ?? `${r.status}`)
setCatalog(d.workflows)
setSource(d.source === 'viewer' ? 'viewer' : 'deployment')
}).catch(e => setNote(`Cannot list the platform's workflows: ${(e as Error).message}`))
}, [])

Expand Down Expand Up @@ -81,6 +83,9 @@ export function WorkflowsSection() {
</div>

<div className="tool-group">On the platform{catalog ? ` (${catalog.length})` : ''}</div>
{catalog && source === 'viewer' && (
<p className="muted">Listed from your own account, because the deployment's platform credential is not usable. Until it is renewed, other viewers can run a workflow picked here only if it is already in their own account.</p>
)}
<input className="field" placeholder="Filter by name or tag" value={filter} onChange={e => setFilter(e.target.value)} />
{!catalog && !note && <p className="muted">Loading…</p>}
{catalog && (
Expand Down
Loading