create-loopengine 0.1.76 → 0.1.78
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/package.json +1 -1
- package/template/adapters/http.ts +488 -18
- package/template/package.json +1 -1
package/package.json
CHANGED
|
@@ -148,6 +148,7 @@ import {
|
|
|
148
148
|
slackClickAllowed,
|
|
149
149
|
agentApprovers,
|
|
150
150
|
setAgentApprovers,
|
|
151
|
+
setAgentPrimaryNotifier,
|
|
151
152
|
listAgentSlackMembers,
|
|
152
153
|
replyPrivatelyInSlack,
|
|
153
154
|
slackArgs,
|
|
@@ -189,6 +190,32 @@ import {
|
|
|
189
190
|
AbilityNotInstalledError,
|
|
190
191
|
searchPublicAbilities,
|
|
191
192
|
fetchLatestAbilityVersion,
|
|
193
|
+
describeAbilitySources,
|
|
194
|
+
listPrivateAbilities,
|
|
195
|
+
removeAbilitySourceFromConsole,
|
|
196
|
+
saveAbilitySourceFromConsole,
|
|
197
|
+
abilityDetails,
|
|
198
|
+
WorkflowError,
|
|
199
|
+
createWorkflow,
|
|
200
|
+
forgetWorkflow,
|
|
201
|
+
getPlanJob,
|
|
202
|
+
listAgentWorkflows,
|
|
203
|
+
planWorkflow,
|
|
204
|
+
runWorkflowNow,
|
|
205
|
+
startPlanJob,
|
|
206
|
+
type WorkflowAgentInfo,
|
|
207
|
+
workflowPageHtml,
|
|
208
|
+
MAX_UPLOAD_BYTES,
|
|
209
|
+
installUploadedAbility,
|
|
210
|
+
installWorkspaceAbility,
|
|
211
|
+
type AbilityUpload,
|
|
212
|
+
WorkspaceAbilityError,
|
|
213
|
+
listWorkspaceAbilities,
|
|
214
|
+
removeWorkspaceAbility,
|
|
215
|
+
AbilitySourceError,
|
|
216
|
+
abilityOrigin,
|
|
217
|
+
readAbilitySources,
|
|
218
|
+
type AbilitySource,
|
|
192
219
|
describeModelProviders,
|
|
193
220
|
loopengineVersion,
|
|
194
221
|
describeUpgrade,
|
|
@@ -196,6 +223,7 @@ import {
|
|
|
196
223
|
UpgradeError,
|
|
197
224
|
setModelProviderKey,
|
|
198
225
|
clearModelProviderKey,
|
|
226
|
+
listProviderModels,
|
|
199
227
|
ModelKeyError,
|
|
200
228
|
describeTimeZone,
|
|
201
229
|
setWorkspaceTimeZone,
|
|
@@ -319,6 +347,32 @@ function readRawBody(req: IncomingMessage): Promise<string> {
|
|
|
319
347
|
})
|
|
320
348
|
}
|
|
321
349
|
|
|
350
|
+
/** readJsonBody with a size cap, for uploads: a body past `maxBytes` is
|
|
351
|
+
* refused before it's all read. */
|
|
352
|
+
function readLimitedJsonBody(req: IncomingMessage, maxBytes: number): Promise<Record<string, unknown>> {
|
|
353
|
+
return new Promise((resolve, reject) => {
|
|
354
|
+
const chunks: Buffer[] = []
|
|
355
|
+
let size = 0
|
|
356
|
+
req.on('data', (chunk: Buffer) => {
|
|
357
|
+
size += chunk.length
|
|
358
|
+
if (size > maxBytes) {
|
|
359
|
+
reject(new WorkspaceAbilityError('The upload is larger than 25 MB'))
|
|
360
|
+
req.destroy()
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
chunks.push(chunk)
|
|
364
|
+
})
|
|
365
|
+
req.on('end', () => {
|
|
366
|
+
try {
|
|
367
|
+
resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {})
|
|
368
|
+
} catch (err) {
|
|
369
|
+
reject(err)
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
req.on('error', reject)
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
322
376
|
function readJsonBody(req: IncomingMessage): Promise<Record<string, unknown>> {
|
|
323
377
|
return new Promise((resolve, reject) => {
|
|
324
378
|
let data = ''
|
|
@@ -866,7 +920,9 @@ function parseAgentTemplateOptions(body: Record<string, unknown>): { ok: true; v
|
|
|
866
920
|
provider !== 'deepseek' &&
|
|
867
921
|
provider !== 'kimi' &&
|
|
868
922
|
provider !== 'glm' &&
|
|
869
|
-
provider !== 'gemini'
|
|
923
|
+
provider !== 'gemini' &&
|
|
924
|
+
provider !== 'xai' &&
|
|
925
|
+
provider !== 'meta'
|
|
870
926
|
) {
|
|
871
927
|
return { ok: false, error: `unsupported model provider '${String(provider)}'` }
|
|
872
928
|
}
|
|
@@ -968,6 +1024,175 @@ async function handleInterviews(req: IncomingMessage, res: ServerResponse, pathn
|
|
|
968
1024
|
}
|
|
969
1025
|
}
|
|
970
1026
|
|
|
1027
|
+
// "Describe a workflow" (loopengine's web/workflow-admin.ts): what the planner and the
|
|
1028
|
+
// checks know about each agent: its tools, abilities, rules and primary
|
|
1029
|
+
// messaging app.
|
|
1030
|
+
async function workflowAgentInfo(): Promise<WorkflowAgentInfo[]> {
|
|
1031
|
+
return Promise.all(
|
|
1032
|
+
listAgents().flatMap((name) => {
|
|
1033
|
+
const entry = getEntry(name)
|
|
1034
|
+
if (!entry) return []
|
|
1035
|
+
return [(async (): Promise<WorkflowAgentInfo> => {
|
|
1036
|
+
const { rules, defaultDecision } = readActauthConfig(name)
|
|
1037
|
+
// Abilities by default: gateway tools (Composio and the like) are
|
|
1038
|
+
// left out, so a workflow is never built on one.
|
|
1039
|
+
let tools: string[] = []
|
|
1040
|
+
let gateway = new Set<string>()
|
|
1041
|
+
try {
|
|
1042
|
+
gateway = new Set((await loadGatewayToolsFromDir(agentDir(name))).map((t) => t.name))
|
|
1043
|
+
const own = entry.config.tools ?? (await loadDefaultTools(entry.config))
|
|
1044
|
+
const all = [...own, ...(await loadSubagentAsTools(entry.config))]
|
|
1045
|
+
tools = [...new Set(all.map((t) => t.name))].filter((t) => !gateway.has(t))
|
|
1046
|
+
} catch {}
|
|
1047
|
+
let abilities: string[] = []
|
|
1048
|
+
try {
|
|
1049
|
+
abilities = listInstalledAbilities(name).map((a) => a.name)
|
|
1050
|
+
} catch {}
|
|
1051
|
+
let messaging: string | null = null
|
|
1052
|
+
try {
|
|
1053
|
+
const n = describeAgentNotifier(entry.config, defaultWebhook)
|
|
1054
|
+
messaging = n.source === 'none' ? null : (n.primary ?? n.channel ?? null)
|
|
1055
|
+
} catch {}
|
|
1056
|
+
const decided = (d: unknown): d is 'allow' | 'ask' | 'deny' => d === 'allow' || d === 'ask' || d === 'deny'
|
|
1057
|
+
return {
|
|
1058
|
+
name,
|
|
1059
|
+
tools,
|
|
1060
|
+
abilities,
|
|
1061
|
+
rules: rules.flatMap((r) => (decided(r.decision) && !gateway.has(r.tool) ? [{ tool: r.tool, decision: r.decision, scope: r.scope }] : [])),
|
|
1062
|
+
defaultDecision: decided(defaultDecision) ? defaultDecision : 'ask',
|
|
1063
|
+
messaging,
|
|
1064
|
+
}
|
|
1065
|
+
})()]
|
|
1066
|
+
}),
|
|
1067
|
+
)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function handleWorkflows(req: IncomingMessage, res: ServerResponse, pathname: string): Promise<void> {
|
|
1071
|
+
const json = (status: number, body: unknown): void => {
|
|
1072
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(body))
|
|
1073
|
+
}
|
|
1074
|
+
const [action, jobId] = pathname.split('/').slice(2) // after /workflows
|
|
1075
|
+
const dirOf = (name: string) => join(projectDir(), 'agents', name)
|
|
1076
|
+
try {
|
|
1077
|
+
// No model to plan with until an agent names one and its key is set
|
|
1078
|
+
// (building the model call throws without the key).
|
|
1079
|
+
let picked: ReturnType<typeof pickInterviewModel>
|
|
1080
|
+
try {
|
|
1081
|
+
picked = pickInterviewModel(interviewAgents())
|
|
1082
|
+
} catch {
|
|
1083
|
+
picked = undefined
|
|
1084
|
+
}
|
|
1085
|
+
if (!action && req.method === 'GET') return json(200, { ready: Boolean(picked), canInstallAbilities: secretsProtected, agents: listAgents() })
|
|
1086
|
+
// A planning job's progress (the page checks every few seconds).
|
|
1087
|
+
if (action === 'plan' && jobId && req.method === 'GET') {
|
|
1088
|
+
const job = getPlanJob(jobId)
|
|
1089
|
+
return job ? json(200, job) : json(404, { error: 'No such planning job (it may have expired); plan it again.' })
|
|
1090
|
+
}
|
|
1091
|
+
if (action === 'plan' && !jobId && req.method === 'POST') {
|
|
1092
|
+
if (!picked) return json(409, { error: 'No model is ready to plan with: set one on an agent (Overview → Model) and add its key under Models.' })
|
|
1093
|
+
const body = await readJsonBody(req)
|
|
1094
|
+
const base = interviewTools(interviewAgents())
|
|
1095
|
+
// The same model, with room for a whole plan in one reply (an
|
|
1096
|
+
// agent's own model call stops at its maxTokens, often 4096).
|
|
1097
|
+
let modelCall = picked.modelCall
|
|
1098
|
+
try {
|
|
1099
|
+
modelCall = (await synthesizeCreateModelCall({ ...picked.model, maxTokens: 8192 } as AgentModelConfig))()
|
|
1100
|
+
} catch {}
|
|
1101
|
+
if (typeof body.description !== 'string' || body.description.trim().length < 15) {
|
|
1102
|
+
return json(400, { error: 'Describe the workflow in a sentence or two: what starts it, the steps, and where you want to decide.' })
|
|
1103
|
+
}
|
|
1104
|
+
const job = startPlanJob(() => planWorkflow(body.description, modelCall, {
|
|
1105
|
+
agentInfo: workflowAgentInfo,
|
|
1106
|
+
// A summary: an agent's first tools and its rules that aren't the
|
|
1107
|
+
// default, to keep what the model reads short.
|
|
1108
|
+
listAgents: async () => (await workflowAgentInfo()).map((a) => ({
|
|
1109
|
+
name: a.name,
|
|
1110
|
+
abilities: a.abilities,
|
|
1111
|
+
messaging: a.messaging,
|
|
1112
|
+
defaultDecision: a.defaultDecision,
|
|
1113
|
+
toolCount: a.tools.length,
|
|
1114
|
+
tools: a.tools.slice(0, 40),
|
|
1115
|
+
rules: a.rules.filter((r) => r.decision !== a.defaultDecision).slice(0, 40).map((r) => `${r.tool}: ${r.decision}`),
|
|
1116
|
+
})),
|
|
1117
|
+
searchPublicAbilities: (query) => base.searchPublicAbilities(query),
|
|
1118
|
+
listWorkspaceAbilities: async () => listWorkspaceAbilities().map((a) => ({ name: a.name, description: a.description, spec: a.spec })),
|
|
1119
|
+
abilityDetails: async (spec) => {
|
|
1120
|
+
const d = await abilityDetails(spec)
|
|
1121
|
+
return { name: d.name, tools: d.tools.map((t) => ({ name: t.name, description: t.description.slice(0, 300), decision: t.decision ?? null })), settings: d.settings.map((x) => x.name) }
|
|
1122
|
+
},
|
|
1123
|
+
listConnections: () => base.listConnections(),
|
|
1124
|
+
}))
|
|
1125
|
+
return json(202, job)
|
|
1126
|
+
}
|
|
1127
|
+
if (action === 'create' && req.method === 'POST') {
|
|
1128
|
+
const body = await readJsonBody(req)
|
|
1129
|
+
const model = pickAgentModel(interviewAgents())
|
|
1130
|
+
const created = await createWorkflow(body, {
|
|
1131
|
+
agentExists: (name) => Boolean(getEntry(name)) || existsSync(dirOf(name)),
|
|
1132
|
+
agentDir: dirOf,
|
|
1133
|
+
agentInfo: workflowAgentInfo,
|
|
1134
|
+
scaffoldAgent: async (name, systemPrompt) => {
|
|
1135
|
+
const parsed = parseAgentTemplateOptions({ systemPrompt, ...(model ? { model } : {}) })
|
|
1136
|
+
if (!parsed.ok) throw new WorkflowError(parsed.error)
|
|
1137
|
+
await scaffoldAgent(projectDir(), name, parsed.value)
|
|
1138
|
+
},
|
|
1139
|
+
registerAgent: async (name) => {
|
|
1140
|
+
const indexPath = join(dirOf(name), 'index.ts')
|
|
1141
|
+
registerAgent(await loadAgentModule(indexPath, indexPath))
|
|
1142
|
+
},
|
|
1143
|
+
// Skills, rules and gateway tools are read on every run, so a
|
|
1144
|
+
// loaded agent already sees them; new ability code restarts the
|
|
1145
|
+
// server (exitIfCodeChanged). Only an agent not loaded yet is.
|
|
1146
|
+
reloadAgent: async (name) => {
|
|
1147
|
+
if (getEntry(name)) return
|
|
1148
|
+
const indexPath = join(dirOf(name), 'index.ts')
|
|
1149
|
+
registerAgent(await loadAgentModule(indexPath, indexPath))
|
|
1150
|
+
},
|
|
1151
|
+
// As for templates and the Abilities tab: installing runs
|
|
1152
|
+
// third-party code, so only behind the admin login.
|
|
1153
|
+
...(secretsProtected ? { installAbility: async (name: string, spec: string) => void (await installAbility(name, spec)) } : {}),
|
|
1154
|
+
envValue: (name, variable) => createAgentEnv(name, dirOf(name)).get(variable),
|
|
1155
|
+
messagingConfigured: (name) => {
|
|
1156
|
+
const entry = getEntry(name)
|
|
1157
|
+
return entry ? describeAgentNotifier(entry.config, defaultWebhook).source !== 'none' : false
|
|
1158
|
+
},
|
|
1159
|
+
})
|
|
1160
|
+
json(200, created)
|
|
1161
|
+
// New tools in an agent that was already running load with a restart,
|
|
1162
|
+
// as after installing an ability from its tab.
|
|
1163
|
+
if (!created.newAgent && created.abilities.installed.length) exitIfCodeChanged(res)
|
|
1164
|
+
return
|
|
1165
|
+
}
|
|
1166
|
+
return json(404, { error: 'not found' })
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
1169
|
+
if (err instanceof WorkflowError) return json(400, { error: message })
|
|
1170
|
+
console.error('[loopengine] workflow failed:', err)
|
|
1171
|
+
return json(502, { error: `Couldn't plan or create the workflow: ${message}` })
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// An agent's workflows (its Overview): list, run now, forget.
|
|
1176
|
+
async function handleAgentWorkflows(req: IncomingMessage, res: ServerResponse, agentName: string, id: string | undefined, action: string | undefined): Promise<void> {
|
|
1177
|
+
const json = (status: number, body: unknown): void => {
|
|
1178
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(body))
|
|
1179
|
+
}
|
|
1180
|
+
if (!getEntry(agentName)) return json(404, { error: `unknown agent '${agentName}'` })
|
|
1181
|
+
const dir = agentDir(agentName)
|
|
1182
|
+
try {
|
|
1183
|
+
if (!id && req.method === 'GET') return json(200, { workflows: await listAgentWorkflows(agentName, dir) })
|
|
1184
|
+
if (id && action === 'run' && req.method === 'POST') return json(200, await runWorkflowNow(agentName, dir, id))
|
|
1185
|
+
if (id && !action && req.method === 'DELETE') {
|
|
1186
|
+
forgetWorkflow(dir, id)
|
|
1187
|
+
return json(200, { workflows: await listAgentWorkflows(agentName, dir) })
|
|
1188
|
+
}
|
|
1189
|
+
return json(404, { error: 'not found' })
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
1192
|
+
return json(err instanceof WorkflowError ? 400 : 500, { error: message })
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
971
1196
|
// Templates (loopengine's web/templates-admin.ts): ready-made agents,
|
|
972
1197
|
// created through the same scaffold and registration as Agents → + New.
|
|
973
1198
|
// A template agent takes the model of an agent that already works here,
|
|
@@ -1367,7 +1592,8 @@ async function handleNotifierPut(req: IncomingMessage, res: ServerResponse, agen
|
|
|
1367
1592
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ...describeAgentNotifier(entry.config, defaultWebhook), canSave: secretsProtected }))
|
|
1368
1593
|
}
|
|
1369
1594
|
|
|
1370
|
-
|
|
1595
|
+
// `channel` removes just that app's connection; without it, all of them.
|
|
1596
|
+
function handleNotifierDelete(res: ServerResponse, agentName: string, channel?: string): void {
|
|
1371
1597
|
if (!secretsProtected) {
|
|
1372
1598
|
res.writeHead(403, { 'content-type': 'application/json' }).end(
|
|
1373
1599
|
JSON.stringify({ error: 'Refusing to remove a notifier without LOOPENGINE_ADMIN_AUTH configured.' }),
|
|
@@ -1379,17 +1605,42 @@ function handleNotifierDelete(res: ServerResponse, agentName: string): void {
|
|
|
1379
1605
|
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1380
1606
|
return
|
|
1381
1607
|
}
|
|
1382
|
-
removeAgentNotifier(entry.config)
|
|
1608
|
+
removeAgentNotifier(entry.config, channel)
|
|
1383
1609
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ...describeAgentNotifier(entry.config, defaultWebhook), canSave: secretsProtected }))
|
|
1384
1610
|
}
|
|
1385
1611
|
|
|
1386
|
-
|
|
1612
|
+
// A body naming a `channel` tests that app's saved connection, primary
|
|
1613
|
+
// or not; without one, whichever notifier applies.
|
|
1614
|
+
async function handleNotifierTest(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
1615
|
+
const entry = getEntry(agentName)
|
|
1616
|
+
if (!entry) {
|
|
1617
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1618
|
+
return
|
|
1619
|
+
}
|
|
1620
|
+
const body = await readJsonBody(req).catch(() => ({}) as Record<string, unknown>)
|
|
1621
|
+
const channel = typeof body.channel === 'string' ? body.channel : undefined
|
|
1622
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await testAgentNotifier(entry.config, defaultWebhook, channel)))
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// The Messaging tab's "Make primary": which saved connection is used.
|
|
1626
|
+
async function handleNotifierPrimaryPut(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
1627
|
+
if (!secretsProtected) {
|
|
1628
|
+
res.writeHead(403, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'Refusing to change the notifier without LOOPENGINE_ADMIN_AUTH configured.' }))
|
|
1629
|
+
return
|
|
1630
|
+
}
|
|
1387
1631
|
const entry = getEntry(agentName)
|
|
1388
1632
|
if (!entry) {
|
|
1389
1633
|
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1390
1634
|
return
|
|
1391
1635
|
}
|
|
1392
|
-
|
|
1636
|
+
try {
|
|
1637
|
+
setAgentPrimaryNotifier(entry.config, (await readJsonBody(req)).channel)
|
|
1638
|
+
} catch (err) {
|
|
1639
|
+
const status = err instanceof NotifierFileError ? 400 : 500
|
|
1640
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1641
|
+
return
|
|
1642
|
+
}
|
|
1643
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ...describeAgentNotifier(entry.config, defaultWebhook), canSave: secretsProtected }))
|
|
1393
1644
|
}
|
|
1394
1645
|
|
|
1395
1646
|
// Backs the console's Slack channel picker. A bot token typed into the form
|
|
@@ -1585,6 +1836,10 @@ async function handleAbilitiesGet(res: ServerResponse, agentName: string): Promi
|
|
|
1585
1836
|
return
|
|
1586
1837
|
}
|
|
1587
1838
|
const installed = listInstalledAbilities(agentName)
|
|
1839
|
+
let sources: AbilitySource[] = []
|
|
1840
|
+
try {
|
|
1841
|
+
sources = readAbilitySources()
|
|
1842
|
+
} catch {}
|
|
1588
1843
|
const abilities = await Promise.all(
|
|
1589
1844
|
installed.map(async (a) => {
|
|
1590
1845
|
// a.dependencies is only ever undefined for a record from before
|
|
@@ -1594,9 +1849,13 @@ async function handleAbilitiesGet(res: ServerResponse, agentName: string): Promi
|
|
|
1594
1849
|
// otherwise permanently hide "Install deps" for an ability that
|
|
1595
1850
|
// genuinely still needs something.
|
|
1596
1851
|
const dependencies = a.dependencies !== undefined ? a.dependencies : ((await backfillDependencies(agentName, a.name)) ?? [])
|
|
1597
|
-
|
|
1852
|
+
// Public or private, for the console's badge. Only a public one is
|
|
1853
|
+
// checked against npm for a newer version: a private ability could
|
|
1854
|
+
// share its name with an unrelated public package.
|
|
1855
|
+
const origin = abilityOrigin(a.spec, a.name, sources)
|
|
1856
|
+
const latestVersion = origin.kind === 'public' ? await fetchLatestAbilityVersion(a.name) : null
|
|
1598
1857
|
const missingDependencies = dependencies.filter((dep) => !hasNodeModule(dep))
|
|
1599
|
-
return { ...a, dependencies, latestVersion, upToDate: latestVersion !== null && latestVersion === a.version, missingDependencies }
|
|
1858
|
+
return { ...a, origin, dependencies, latestVersion, upToDate: latestVersion !== null && latestVersion === a.version, missingDependencies }
|
|
1600
1859
|
}),
|
|
1601
1860
|
)
|
|
1602
1861
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ abilities }))
|
|
@@ -1786,12 +2045,86 @@ function parseActauthRuleBody(body: Record<string, unknown>, requireName: boolea
|
|
|
1786
2045
|
return { ok: true, value: { name: typeof body.name === 'string' ? body.name : '', scope: body.scope, tool: body.tool, decision: body.decision } }
|
|
1787
2046
|
}
|
|
1788
2047
|
|
|
1789
|
-
|
|
2048
|
+
// The Permissions data. `rules`: every rule, each with the ability it
|
|
2049
|
+
// belongs to (empty for none): the one that added it when installed, else
|
|
2050
|
+
// the one whose tool it's for. Edited in full from the Advanced tab.
|
|
2051
|
+
// `tools`: every tool the agent has (its own, subagents, Composio), each
|
|
2052
|
+
// with its ability and decision: its rule for everyone (*/*), else its
|
|
2053
|
+
// first rule, else the default; `scopeNote` says when that isn't the whole
|
|
2054
|
+
// story, so the Permissions tab doesn't look like it has duplicates.
|
|
2055
|
+
async function actauthConfigWithAbilities(agentName: string) {
|
|
2056
|
+
const config = readActauthConfig(agentName)
|
|
2057
|
+
let installed: Array<{ name: string; tools: string[]; actauthRules: string[] }> = []
|
|
2058
|
+
try {
|
|
2059
|
+
installed = listInstalledAbilities(agentName)
|
|
2060
|
+
} catch {}
|
|
2061
|
+
const abilityFor = (tool: string, ruleName?: string) =>
|
|
2062
|
+
(installed.find((a) => ruleName && a.actauthRules.includes(ruleName)) ?? installed.find((a) => a.tools.includes(tool)))?.name ?? null
|
|
2063
|
+
const rules = config.rules.map((rule) => ({ ...rule, ability: abilityFor(rule.tool, rule.name) }))
|
|
2064
|
+
|
|
2065
|
+
const entry = getEntry(agentName)
|
|
2066
|
+
let toolNames: string[] = []
|
|
2067
|
+
if (entry) {
|
|
2068
|
+
try {
|
|
2069
|
+
const own = entry.config.tools ?? (await loadDefaultTools(entry.config))
|
|
2070
|
+
const all = [...own, ...(await loadSubagentAsTools(entry.config)), ...(await loadGatewayToolsFromDir(agentDir(agentName)))]
|
|
2071
|
+
toolNames = [...new Set(all.map((t) => t.name))]
|
|
2072
|
+
} catch {}
|
|
2073
|
+
}
|
|
2074
|
+
const tools = toolNames.map((tool) => {
|
|
2075
|
+
const forTool = rules.filter((r) => r.tool === tool)
|
|
2076
|
+
const rule = forTool.find((r) => r.scope === '*/*') ?? forTool[0]
|
|
2077
|
+
const others = forTool.length - (rule ? 1 : 0)
|
|
2078
|
+
const scopeNote = rule && rule.scope !== '*/*' ? `${rule.scope} only` : others ? `+${others} scoped ${others === 1 ? 'rule' : 'rules'} in Advanced` : null
|
|
2079
|
+
return {
|
|
2080
|
+
tool,
|
|
2081
|
+
ability: rule?.ability ?? abilityFor(tool),
|
|
2082
|
+
decision: rule?.decision ?? config.defaultDecision,
|
|
2083
|
+
isDefault: !rule,
|
|
2084
|
+
// A rule with no name can't be changed here (rules are changed by name).
|
|
2085
|
+
editable: !rule || Boolean(rule.name),
|
|
2086
|
+
scopeNote,
|
|
2087
|
+
}
|
|
2088
|
+
})
|
|
2089
|
+
return { ...config, rules, tools }
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// The Permissions tab's switch for one tool: updates its rule for everyone
|
|
2093
|
+
// (*/*), or adds one (tool-<name>) when it has none.
|
|
2094
|
+
async function handleActauthToolPut(req: IncomingMessage, res: ServerResponse, agentName: string, tool: string): Promise<void> {
|
|
1790
2095
|
if (!getEntry(agentName)) {
|
|
1791
2096
|
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1792
2097
|
return
|
|
1793
2098
|
}
|
|
1794
|
-
|
|
2099
|
+
const body = await readJsonBody(req)
|
|
2100
|
+
if (!isDecision(body.decision)) {
|
|
2101
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: "decision must be 'allow', 'ask', or 'deny'" }))
|
|
2102
|
+
return
|
|
2103
|
+
}
|
|
2104
|
+
const { rules } = readActauthConfig(agentName)
|
|
2105
|
+
const existing = rules.find((r) => r.tool === tool && r.scope === '*/*' && r.name)
|
|
2106
|
+
try {
|
|
2107
|
+
if (existing) {
|
|
2108
|
+
updateActauthRule(agentName, existing.name!, { scope: existing.scope, tool, decision: body.decision })
|
|
2109
|
+
} else {
|
|
2110
|
+
const base = `tool-${tool}`.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').slice(0, 80)
|
|
2111
|
+
let name = base
|
|
2112
|
+
for (let i = 2; rules.some((r) => r.name === name); i++) name = `${base}-${i}`
|
|
2113
|
+
addActauthRule(agentName, { name, scope: '*/*', tool, decision: body.decision })
|
|
2114
|
+
}
|
|
2115
|
+
} catch (err) {
|
|
2116
|
+
res.writeHead(500, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
2117
|
+
return
|
|
2118
|
+
}
|
|
2119
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await actauthConfigWithAbilities(agentName)))
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
async function handleActauthGet(res: ServerResponse, agentName: string): Promise<void> {
|
|
2123
|
+
if (!getEntry(agentName)) {
|
|
2124
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
2125
|
+
return
|
|
2126
|
+
}
|
|
2127
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await actauthConfigWithAbilities(agentName)))
|
|
1795
2128
|
}
|
|
1796
2129
|
|
|
1797
2130
|
async function handleActauthRulePost(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
@@ -1812,7 +2145,7 @@ async function handleActauthRulePost(req: IncomingMessage, res: ServerResponse,
|
|
|
1812
2145
|
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1813
2146
|
return
|
|
1814
2147
|
}
|
|
1815
|
-
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(
|
|
2148
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await actauthConfigWithAbilities(agentName)))
|
|
1816
2149
|
}
|
|
1817
2150
|
|
|
1818
2151
|
async function handleActauthRulePut(req: IncomingMessage, res: ServerResponse, agentName: string, ruleName: string): Promise<void> {
|
|
@@ -1833,10 +2166,10 @@ async function handleActauthRulePut(req: IncomingMessage, res: ServerResponse, a
|
|
|
1833
2166
|
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1834
2167
|
return
|
|
1835
2168
|
}
|
|
1836
|
-
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(
|
|
2169
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await actauthConfigWithAbilities(agentName)))
|
|
1837
2170
|
}
|
|
1838
2171
|
|
|
1839
|
-
function handleActauthRuleDelete(res: ServerResponse, agentName: string, ruleName: string): void {
|
|
2172
|
+
async function handleActauthRuleDelete(res: ServerResponse, agentName: string, ruleName: string): Promise<void> {
|
|
1840
2173
|
if (!getEntry(agentName)) {
|
|
1841
2174
|
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1842
2175
|
return
|
|
@@ -1848,7 +2181,7 @@ function handleActauthRuleDelete(res: ServerResponse, agentName: string, ruleNam
|
|
|
1848
2181
|
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1849
2182
|
return
|
|
1850
2183
|
}
|
|
1851
|
-
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(
|
|
2184
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await actauthConfigWithAbilities(agentName)))
|
|
1852
2185
|
}
|
|
1853
2186
|
|
|
1854
2187
|
async function handleActauthDefaultDecisionPut(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
@@ -1862,7 +2195,7 @@ async function handleActauthDefaultDecisionPut(req: IncomingMessage, res: Server
|
|
|
1862
2195
|
return
|
|
1863
2196
|
}
|
|
1864
2197
|
setDefaultDecision(agentName, body.decision)
|
|
1865
|
-
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(
|
|
2198
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await actauthConfigWithAbilities(agentName)))
|
|
1866
2199
|
}
|
|
1867
2200
|
|
|
1868
2201
|
// GET /agents content-negotiates on this: a browser navigating there sends
|
|
@@ -3334,6 +3667,20 @@ const server = createServer(async (req, res) => {
|
|
|
3334
3667
|
await handleInterviews(req, res, pathname)
|
|
3335
3668
|
return
|
|
3336
3669
|
}
|
|
3670
|
+
// "Describe a workflow": the page, and its routes.
|
|
3671
|
+
if (req.method === 'GET' && pathname === '/workflows/new') {
|
|
3672
|
+
res.writeHead(200, { 'content-type': 'text/html' }).end(workflowPageHtml)
|
|
3673
|
+
return
|
|
3674
|
+
}
|
|
3675
|
+
if (pathname === '/workflows' || pathname === '/workflows/plan' || pathname.startsWith('/workflows/plan/') || pathname === '/workflows/create') {
|
|
3676
|
+
await handleWorkflows(req, res, pathname)
|
|
3677
|
+
return
|
|
3678
|
+
}
|
|
3679
|
+
const agentWorkflowsMatch = pathname.match(/^\/agents\/([^/]+)\/workflows(?:\/([a-z0-9-]+))?(?:\/(run))?$/)
|
|
3680
|
+
if (agentWorkflowsMatch) {
|
|
3681
|
+
await handleAgentWorkflows(req, res, decodeURIComponent(agentWorkflowsMatch[1]), agentWorkflowsMatch[2], agentWorkflowsMatch[3])
|
|
3682
|
+
return
|
|
3683
|
+
}
|
|
3337
3684
|
// Templates: the page, and its routes.
|
|
3338
3685
|
if (req.method === 'GET' && pathname === '/templates') {
|
|
3339
3686
|
res.writeHead(200, { 'content-type': 'text/html' }).end(templatesPageHtml)
|
|
@@ -3416,6 +3763,19 @@ const server = createServer(async (req, res) => {
|
|
|
3416
3763
|
)
|
|
3417
3764
|
return
|
|
3418
3765
|
}
|
|
3766
|
+
// The model dropdown in the New agent and Model forms: what the
|
|
3767
|
+
// provider lists for the saved key. Never includes the key itself.
|
|
3768
|
+
const providerModelsMatch = req.method === 'GET' && pathname.match(/^\/config\/models\/([a-z]+)\/models$/)
|
|
3769
|
+
if (providerModelsMatch) {
|
|
3770
|
+
try {
|
|
3771
|
+
const view = await listProviderModels(providerModelsMatch[1])
|
|
3772
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(view))
|
|
3773
|
+
} catch (err) {
|
|
3774
|
+
const status = err instanceof ModelKeyError ? 400 : 500
|
|
3775
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
3776
|
+
}
|
|
3777
|
+
return
|
|
3778
|
+
}
|
|
3419
3779
|
// A provider's API key, from the Models page: write-only, never read
|
|
3420
3780
|
// back. Same rule as the Environment tab: refused on a server with no
|
|
3421
3781
|
// auth, since anyone who could reach it could replace the key.
|
|
@@ -3608,12 +3968,17 @@ const server = createServer(async (req, res) => {
|
|
|
3608
3968
|
return
|
|
3609
3969
|
}
|
|
3610
3970
|
if (notifierMatch && req.method === 'DELETE') {
|
|
3611
|
-
handleNotifierDelete(res, decodeURIComponent(notifierMatch[1]))
|
|
3971
|
+
handleNotifierDelete(res, decodeURIComponent(notifierMatch[1]), new URL(req.url ?? '/', 'http://localhost').searchParams.get('channel') ?? undefined)
|
|
3612
3972
|
return
|
|
3613
3973
|
}
|
|
3614
3974
|
const notifierTestMatch = req.method === 'POST' && pathname.match(/^\/agents\/([^/]+)\/notifier\/test$/)
|
|
3615
3975
|
if (notifierTestMatch) {
|
|
3616
|
-
await handleNotifierTest(res, decodeURIComponent(notifierTestMatch[1]))
|
|
3976
|
+
await handleNotifierTest(req, res, decodeURIComponent(notifierTestMatch[1]))
|
|
3977
|
+
return
|
|
3978
|
+
}
|
|
3979
|
+
const notifierPrimaryMatch = req.method === 'PUT' && pathname.match(/^\/agents\/([^/]+)\/notifier\/primary$/)
|
|
3980
|
+
if (notifierPrimaryMatch) {
|
|
3981
|
+
await handleNotifierPrimaryPut(req, res, decodeURIComponent(notifierPrimaryMatch[1]))
|
|
3617
3982
|
return
|
|
3618
3983
|
}
|
|
3619
3984
|
// Backs the console's Schedules tab — see handleSchedulesGet above.
|
|
@@ -3683,6 +4048,105 @@ const server = createServer(async (req, res) => {
|
|
|
3683
4048
|
// itself need to be excluded from matching as an agent name (it
|
|
3684
4049
|
// never would be one in practice, but this ordering means never
|
|
3685
4050
|
// having to think about it).
|
|
4051
|
+
// Private ability sources (Settings) and what they offer (the
|
|
4052
|
+
// Abilities page's Private section): web/ability-sources-admin.ts.
|
|
4053
|
+
// Saving or removing one stores a token, so it's refused without auth,
|
|
4054
|
+
// like a model key.
|
|
4055
|
+
if (pathname === '/config/ability-sources' && req.method === 'GET') {
|
|
4056
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify({ ...describeAbilitySources(), canSave: secretsProtected }))
|
|
4057
|
+
return
|
|
4058
|
+
}
|
|
4059
|
+
const abilitySourceMatch = pathname.match(/^\/config\/ability-sources(?:\/([a-z0-9-]+))?$/)
|
|
4060
|
+
if (abilitySourceMatch && (req.method === 'PUT' || req.method === 'DELETE')) {
|
|
4061
|
+
if (!secretsProtected) {
|
|
4062
|
+
res.writeHead(403, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'Refusing to change ability sources without LOOPENGINE_ADMIN_AUTH configured: set it first, or edit ability-sources.yml and .env.' }))
|
|
4063
|
+
return
|
|
4064
|
+
}
|
|
4065
|
+
try {
|
|
4066
|
+
if (req.method === 'PUT') await saveAbilitySourceFromConsole(await readJsonBody(req))
|
|
4067
|
+
else if (abilitySourceMatch[1]) await removeAbilitySourceFromConsole(abilitySourceMatch[1])
|
|
4068
|
+
else throw new AbilitySourceError('Name the source to remove')
|
|
4069
|
+
} catch (err) {
|
|
4070
|
+
const status = err instanceof AbilitySourceError ? 400 : 500
|
|
4071
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
4072
|
+
return
|
|
4073
|
+
}
|
|
4074
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ...describeAbilitySources(), canSave: secretsProtected }))
|
|
4075
|
+
return
|
|
4076
|
+
}
|
|
4077
|
+
// What an ability can do: its tools (what each does, its permission)
|
|
4078
|
+
// and skills. `agent` adds that agent's current decisions.
|
|
4079
|
+
if (pathname === '/abilities/details' && req.method === 'GET') {
|
|
4080
|
+
const params = new URL(req.url ?? '/', 'http://localhost').searchParams
|
|
4081
|
+
const spec = params.get('spec')?.trim()
|
|
4082
|
+
const agent = params.get('agent') ?? undefined
|
|
4083
|
+
if (!spec || (agent && !getEntry(agent))) {
|
|
4084
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: spec ? `unknown agent '${agent}'` : 'spec is required' }))
|
|
4085
|
+
return
|
|
4086
|
+
}
|
|
4087
|
+
try {
|
|
4088
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await abilityDetails(spec, { agent })))
|
|
4089
|
+
} catch (err) {
|
|
4090
|
+
res.writeHead(502, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
4091
|
+
}
|
|
4092
|
+
return
|
|
4093
|
+
}
|
|
4094
|
+
// The workspace's private abilities (core/workspace-abilities.ts):
|
|
4095
|
+
// installed from the Abilities page, added to agents from their tab.
|
|
4096
|
+
if (pathname === '/abilities/workspace' && req.method === 'GET') {
|
|
4097
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify({ abilities: listWorkspaceAbilities() }))
|
|
4098
|
+
return
|
|
4099
|
+
}
|
|
4100
|
+
if (pathname === '/abilities/workspace' && (req.method === 'POST' || req.method === 'DELETE')) {
|
|
4101
|
+
try {
|
|
4102
|
+
if (req.method === 'POST') {
|
|
4103
|
+
const spec = (await readJsonBody(req)).spec
|
|
4104
|
+
if (typeof spec !== 'string' || !spec.trim()) throw new WorkspaceAbilityError('spec is required')
|
|
4105
|
+
await installWorkspaceAbility(spec.trim())
|
|
4106
|
+
} else {
|
|
4107
|
+
removeWorkspaceAbility(new URL(req.url ?? '/', 'http://localhost').searchParams.get('name') ?? '')
|
|
4108
|
+
}
|
|
4109
|
+
} catch (err) {
|
|
4110
|
+
const status = err instanceof WorkspaceAbilityError ? 400 : 502
|
|
4111
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
4112
|
+
return
|
|
4113
|
+
}
|
|
4114
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ abilities: listWorkspaceAbilities() }))
|
|
4115
|
+
return
|
|
4116
|
+
}
|
|
4117
|
+
// An ability uploaded from the browser (a .zip/.tgz, or a folder's
|
|
4118
|
+
// files, base64), stored in the workspace like one from a source.
|
|
4119
|
+
if (pathname === '/abilities/workspace/upload' && req.method === 'POST') {
|
|
4120
|
+
try {
|
|
4121
|
+
// Base64 adds a third, plus the JSON around it.
|
|
4122
|
+
const body = await readLimitedJsonBody(req, Math.ceil(MAX_UPLOAD_BYTES * 1.4) + 64 * 1024)
|
|
4123
|
+
const upload: AbilityUpload =
|
|
4124
|
+
body.kind === 'files' && Array.isArray(body.files)
|
|
4125
|
+
? { kind: 'files', folderName: typeof body.folderName === 'string' ? body.folderName : undefined, files: (body.files as Array<{ path?: unknown; data?: unknown }>).map((f) => ({ path: String(f?.path ?? ''), data: String(f?.data ?? '') })) }
|
|
4126
|
+
: { kind: 'archive', fileName: String(body.fileName ?? ''), data: String(body.data ?? '') }
|
|
4127
|
+
await installUploadedAbility(upload)
|
|
4128
|
+
} catch (err) {
|
|
4129
|
+
const status = err instanceof WorkspaceAbilityError ? 400 : 500
|
|
4130
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
4131
|
+
return
|
|
4132
|
+
}
|
|
4133
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ abilities: listWorkspaceAbilities() }))
|
|
4134
|
+
return
|
|
4135
|
+
}
|
|
4136
|
+
// Public abilities on npm, for the Abilities page (no agent involved).
|
|
4137
|
+
if (pathname === '/abilities/search' && req.method === 'GET') {
|
|
4138
|
+
try {
|
|
4139
|
+
const results = await searchPublicAbilities(new URL(req.url ?? '/', 'http://localhost').searchParams.get('q') ?? undefined)
|
|
4140
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ results }))
|
|
4141
|
+
} catch (err) {
|
|
4142
|
+
res.writeHead(502, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
4143
|
+
}
|
|
4144
|
+
return
|
|
4145
|
+
}
|
|
4146
|
+
if (pathname === '/abilities/private' && req.method === 'GET') {
|
|
4147
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(await listPrivateAbilities()))
|
|
4148
|
+
return
|
|
4149
|
+
}
|
|
3686
4150
|
const abilitiesSearchMatch = pathname.match(/^\/agents\/([^/]+)\/abilities\/search$/)
|
|
3687
4151
|
if (abilitiesSearchMatch && req.method === 'GET') {
|
|
3688
4152
|
const query = new URL(req.url ?? '/', 'http://localhost').searchParams.get('q')
|
|
@@ -3729,13 +4193,19 @@ const server = createServer(async (req, res) => {
|
|
|
3729
4193
|
return
|
|
3730
4194
|
}
|
|
3731
4195
|
|
|
4196
|
+
const actauthToolMatch = req.method === 'PUT' && pathname.match(/^\/agents\/([^/]+)\/actauth\/tools\/([^/]+)$/)
|
|
4197
|
+
if (actauthToolMatch) {
|
|
4198
|
+
await handleActauthToolPut(req, res, decodeURIComponent(actauthToolMatch[1]), decodeURIComponent(actauthToolMatch[2]))
|
|
4199
|
+
return
|
|
4200
|
+
}
|
|
4201
|
+
|
|
3732
4202
|
const actauthRuleMatch = pathname.match(/^\/agents\/([^/]+)\/actauth\/rules\/([^/]+)$/)
|
|
3733
4203
|
if (actauthRuleMatch && req.method === 'PUT') {
|
|
3734
4204
|
await handleActauthRulePut(req, res, decodeURIComponent(actauthRuleMatch[1]), decodeURIComponent(actauthRuleMatch[2]))
|
|
3735
4205
|
return
|
|
3736
4206
|
}
|
|
3737
4207
|
if (actauthRuleMatch && req.method === 'DELETE') {
|
|
3738
|
-
handleActauthRuleDelete(res, decodeURIComponent(actauthRuleMatch[1]), decodeURIComponent(actauthRuleMatch[2]))
|
|
4208
|
+
await handleActauthRuleDelete(res, decodeURIComponent(actauthRuleMatch[1]), decodeURIComponent(actauthRuleMatch[2]))
|
|
3739
4209
|
return
|
|
3740
4210
|
}
|
|
3741
4211
|
|
|
@@ -3747,7 +4217,7 @@ const server = createServer(async (req, res) => {
|
|
|
3747
4217
|
|
|
3748
4218
|
const actauthMatch = req.method === 'GET' && pathname.match(/^\/agents\/([^/]+)\/actauth$/)
|
|
3749
4219
|
if (actauthMatch) {
|
|
3750
|
-
handleActauthGet(res, decodeURIComponent(actauthMatch[1]))
|
|
4220
|
+
await handleActauthGet(res, decodeURIComponent(actauthMatch[1]))
|
|
3751
4221
|
return
|
|
3752
4222
|
}
|
|
3753
4223
|
|