create-loopengine 0.1.76 → 0.1.77
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 +295 -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,22 @@ import {
|
|
|
189
190
|
AbilityNotInstalledError,
|
|
190
191
|
searchPublicAbilities,
|
|
191
192
|
fetchLatestAbilityVersion,
|
|
193
|
+
describeAbilitySources,
|
|
194
|
+
listPrivateAbilities,
|
|
195
|
+
removeAbilitySourceFromConsole,
|
|
196
|
+
saveAbilitySourceFromConsole,
|
|
197
|
+
abilityDetails,
|
|
198
|
+
MAX_UPLOAD_BYTES,
|
|
199
|
+
installUploadedAbility,
|
|
200
|
+
installWorkspaceAbility,
|
|
201
|
+
type AbilityUpload,
|
|
202
|
+
WorkspaceAbilityError,
|
|
203
|
+
listWorkspaceAbilities,
|
|
204
|
+
removeWorkspaceAbility,
|
|
205
|
+
AbilitySourceError,
|
|
206
|
+
abilityOrigin,
|
|
207
|
+
readAbilitySources,
|
|
208
|
+
type AbilitySource,
|
|
192
209
|
describeModelProviders,
|
|
193
210
|
loopengineVersion,
|
|
194
211
|
describeUpgrade,
|
|
@@ -196,6 +213,7 @@ import {
|
|
|
196
213
|
UpgradeError,
|
|
197
214
|
setModelProviderKey,
|
|
198
215
|
clearModelProviderKey,
|
|
216
|
+
listProviderModels,
|
|
199
217
|
ModelKeyError,
|
|
200
218
|
describeTimeZone,
|
|
201
219
|
setWorkspaceTimeZone,
|
|
@@ -319,6 +337,32 @@ function readRawBody(req: IncomingMessage): Promise<string> {
|
|
|
319
337
|
})
|
|
320
338
|
}
|
|
321
339
|
|
|
340
|
+
/** readJsonBody with a size cap, for uploads: a body past `maxBytes` is
|
|
341
|
+
* refused before it's all read. */
|
|
342
|
+
function readLimitedJsonBody(req: IncomingMessage, maxBytes: number): Promise<Record<string, unknown>> {
|
|
343
|
+
return new Promise((resolve, reject) => {
|
|
344
|
+
const chunks: Buffer[] = []
|
|
345
|
+
let size = 0
|
|
346
|
+
req.on('data', (chunk: Buffer) => {
|
|
347
|
+
size += chunk.length
|
|
348
|
+
if (size > maxBytes) {
|
|
349
|
+
reject(new WorkspaceAbilityError('The upload is larger than 25 MB'))
|
|
350
|
+
req.destroy()
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
chunks.push(chunk)
|
|
354
|
+
})
|
|
355
|
+
req.on('end', () => {
|
|
356
|
+
try {
|
|
357
|
+
resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {})
|
|
358
|
+
} catch (err) {
|
|
359
|
+
reject(err)
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
req.on('error', reject)
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
322
366
|
function readJsonBody(req: IncomingMessage): Promise<Record<string, unknown>> {
|
|
323
367
|
return new Promise((resolve, reject) => {
|
|
324
368
|
let data = ''
|
|
@@ -866,7 +910,9 @@ function parseAgentTemplateOptions(body: Record<string, unknown>): { ok: true; v
|
|
|
866
910
|
provider !== 'deepseek' &&
|
|
867
911
|
provider !== 'kimi' &&
|
|
868
912
|
provider !== 'glm' &&
|
|
869
|
-
provider !== 'gemini'
|
|
913
|
+
provider !== 'gemini' &&
|
|
914
|
+
provider !== 'xai' &&
|
|
915
|
+
provider !== 'meta'
|
|
870
916
|
) {
|
|
871
917
|
return { ok: false, error: `unsupported model provider '${String(provider)}'` }
|
|
872
918
|
}
|
|
@@ -1367,7 +1413,8 @@ async function handleNotifierPut(req: IncomingMessage, res: ServerResponse, agen
|
|
|
1367
1413
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ...describeAgentNotifier(entry.config, defaultWebhook), canSave: secretsProtected }))
|
|
1368
1414
|
}
|
|
1369
1415
|
|
|
1370
|
-
|
|
1416
|
+
// `channel` removes just that app's connection; without it, all of them.
|
|
1417
|
+
function handleNotifierDelete(res: ServerResponse, agentName: string, channel?: string): void {
|
|
1371
1418
|
if (!secretsProtected) {
|
|
1372
1419
|
res.writeHead(403, { 'content-type': 'application/json' }).end(
|
|
1373
1420
|
JSON.stringify({ error: 'Refusing to remove a notifier without LOOPENGINE_ADMIN_AUTH configured.' }),
|
|
@@ -1379,17 +1426,42 @@ function handleNotifierDelete(res: ServerResponse, agentName: string): void {
|
|
|
1379
1426
|
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1380
1427
|
return
|
|
1381
1428
|
}
|
|
1382
|
-
removeAgentNotifier(entry.config)
|
|
1429
|
+
removeAgentNotifier(entry.config, channel)
|
|
1383
1430
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ...describeAgentNotifier(entry.config, defaultWebhook), canSave: secretsProtected }))
|
|
1384
1431
|
}
|
|
1385
1432
|
|
|
1386
|
-
|
|
1433
|
+
// A body naming a `channel` tests that app's saved connection, primary
|
|
1434
|
+
// or not; without one, whichever notifier applies.
|
|
1435
|
+
async function handleNotifierTest(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
1387
1436
|
const entry = getEntry(agentName)
|
|
1388
1437
|
if (!entry) {
|
|
1389
1438
|
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1390
1439
|
return
|
|
1391
1440
|
}
|
|
1392
|
-
|
|
1441
|
+
const body = await readJsonBody(req).catch(() => ({}) as Record<string, unknown>)
|
|
1442
|
+
const channel = typeof body.channel === 'string' ? body.channel : undefined
|
|
1443
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await testAgentNotifier(entry.config, defaultWebhook, channel)))
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// The Messaging tab's "Make primary": which saved connection is used.
|
|
1447
|
+
async function handleNotifierPrimaryPut(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
1448
|
+
if (!secretsProtected) {
|
|
1449
|
+
res.writeHead(403, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'Refusing to change the notifier without LOOPENGINE_ADMIN_AUTH configured.' }))
|
|
1450
|
+
return
|
|
1451
|
+
}
|
|
1452
|
+
const entry = getEntry(agentName)
|
|
1453
|
+
if (!entry) {
|
|
1454
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1455
|
+
return
|
|
1456
|
+
}
|
|
1457
|
+
try {
|
|
1458
|
+
setAgentPrimaryNotifier(entry.config, (await readJsonBody(req)).channel)
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
const status = err instanceof NotifierFileError ? 400 : 500
|
|
1461
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1462
|
+
return
|
|
1463
|
+
}
|
|
1464
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ...describeAgentNotifier(entry.config, defaultWebhook), canSave: secretsProtected }))
|
|
1393
1465
|
}
|
|
1394
1466
|
|
|
1395
1467
|
// Backs the console's Slack channel picker. A bot token typed into the form
|
|
@@ -1585,6 +1657,10 @@ async function handleAbilitiesGet(res: ServerResponse, agentName: string): Promi
|
|
|
1585
1657
|
return
|
|
1586
1658
|
}
|
|
1587
1659
|
const installed = listInstalledAbilities(agentName)
|
|
1660
|
+
let sources: AbilitySource[] = []
|
|
1661
|
+
try {
|
|
1662
|
+
sources = readAbilitySources()
|
|
1663
|
+
} catch {}
|
|
1588
1664
|
const abilities = await Promise.all(
|
|
1589
1665
|
installed.map(async (a) => {
|
|
1590
1666
|
// a.dependencies is only ever undefined for a record from before
|
|
@@ -1594,9 +1670,13 @@ async function handleAbilitiesGet(res: ServerResponse, agentName: string): Promi
|
|
|
1594
1670
|
// otherwise permanently hide "Install deps" for an ability that
|
|
1595
1671
|
// genuinely still needs something.
|
|
1596
1672
|
const dependencies = a.dependencies !== undefined ? a.dependencies : ((await backfillDependencies(agentName, a.name)) ?? [])
|
|
1597
|
-
|
|
1673
|
+
// Public or private, for the console's badge. Only a public one is
|
|
1674
|
+
// checked against npm for a newer version: a private ability could
|
|
1675
|
+
// share its name with an unrelated public package.
|
|
1676
|
+
const origin = abilityOrigin(a.spec, a.name, sources)
|
|
1677
|
+
const latestVersion = origin.kind === 'public' ? await fetchLatestAbilityVersion(a.name) : null
|
|
1598
1678
|
const missingDependencies = dependencies.filter((dep) => !hasNodeModule(dep))
|
|
1599
|
-
return { ...a, dependencies, latestVersion, upToDate: latestVersion !== null && latestVersion === a.version, missingDependencies }
|
|
1679
|
+
return { ...a, origin, dependencies, latestVersion, upToDate: latestVersion !== null && latestVersion === a.version, missingDependencies }
|
|
1600
1680
|
}),
|
|
1601
1681
|
)
|
|
1602
1682
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ abilities }))
|
|
@@ -1786,12 +1866,86 @@ function parseActauthRuleBody(body: Record<string, unknown>, requireName: boolea
|
|
|
1786
1866
|
return { ok: true, value: { name: typeof body.name === 'string' ? body.name : '', scope: body.scope, tool: body.tool, decision: body.decision } }
|
|
1787
1867
|
}
|
|
1788
1868
|
|
|
1789
|
-
|
|
1869
|
+
// The Permissions data. `rules`: every rule, each with the ability it
|
|
1870
|
+
// belongs to (empty for none): the one that added it when installed, else
|
|
1871
|
+
// the one whose tool it's for. Edited in full from the Advanced tab.
|
|
1872
|
+
// `tools`: every tool the agent has (its own, subagents, Composio), each
|
|
1873
|
+
// with its ability and decision: its rule for everyone (*/*), else its
|
|
1874
|
+
// first rule, else the default; `scopeNote` says when that isn't the whole
|
|
1875
|
+
// story, so the Permissions tab doesn't look like it has duplicates.
|
|
1876
|
+
async function actauthConfigWithAbilities(agentName: string) {
|
|
1877
|
+
const config = readActauthConfig(agentName)
|
|
1878
|
+
let installed: Array<{ name: string; tools: string[]; actauthRules: string[] }> = []
|
|
1879
|
+
try {
|
|
1880
|
+
installed = listInstalledAbilities(agentName)
|
|
1881
|
+
} catch {}
|
|
1882
|
+
const abilityFor = (tool: string, ruleName?: string) =>
|
|
1883
|
+
(installed.find((a) => ruleName && a.actauthRules.includes(ruleName)) ?? installed.find((a) => a.tools.includes(tool)))?.name ?? null
|
|
1884
|
+
const rules = config.rules.map((rule) => ({ ...rule, ability: abilityFor(rule.tool, rule.name) }))
|
|
1885
|
+
|
|
1886
|
+
const entry = getEntry(agentName)
|
|
1887
|
+
let toolNames: string[] = []
|
|
1888
|
+
if (entry) {
|
|
1889
|
+
try {
|
|
1890
|
+
const own = entry.config.tools ?? (await loadDefaultTools(entry.config))
|
|
1891
|
+
const all = [...own, ...(await loadSubagentAsTools(entry.config)), ...(await loadGatewayToolsFromDir(agentDir(agentName)))]
|
|
1892
|
+
toolNames = [...new Set(all.map((t) => t.name))]
|
|
1893
|
+
} catch {}
|
|
1894
|
+
}
|
|
1895
|
+
const tools = toolNames.map((tool) => {
|
|
1896
|
+
const forTool = rules.filter((r) => r.tool === tool)
|
|
1897
|
+
const rule = forTool.find((r) => r.scope === '*/*') ?? forTool[0]
|
|
1898
|
+
const others = forTool.length - (rule ? 1 : 0)
|
|
1899
|
+
const scopeNote = rule && rule.scope !== '*/*' ? `${rule.scope} only` : others ? `+${others} scoped ${others === 1 ? 'rule' : 'rules'} in Advanced` : null
|
|
1900
|
+
return {
|
|
1901
|
+
tool,
|
|
1902
|
+
ability: rule?.ability ?? abilityFor(tool),
|
|
1903
|
+
decision: rule?.decision ?? config.defaultDecision,
|
|
1904
|
+
isDefault: !rule,
|
|
1905
|
+
// A rule with no name can't be changed here (rules are changed by name).
|
|
1906
|
+
editable: !rule || Boolean(rule.name),
|
|
1907
|
+
scopeNote,
|
|
1908
|
+
}
|
|
1909
|
+
})
|
|
1910
|
+
return { ...config, rules, tools }
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// The Permissions tab's switch for one tool: updates its rule for everyone
|
|
1914
|
+
// (*/*), or adds one (tool-<name>) when it has none.
|
|
1915
|
+
async function handleActauthToolPut(req: IncomingMessage, res: ServerResponse, agentName: string, tool: string): Promise<void> {
|
|
1790
1916
|
if (!getEntry(agentName)) {
|
|
1791
1917
|
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1792
1918
|
return
|
|
1793
1919
|
}
|
|
1794
|
-
|
|
1920
|
+
const body = await readJsonBody(req)
|
|
1921
|
+
if (!isDecision(body.decision)) {
|
|
1922
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: "decision must be 'allow', 'ask', or 'deny'" }))
|
|
1923
|
+
return
|
|
1924
|
+
}
|
|
1925
|
+
const { rules } = readActauthConfig(agentName)
|
|
1926
|
+
const existing = rules.find((r) => r.tool === tool && r.scope === '*/*' && r.name)
|
|
1927
|
+
try {
|
|
1928
|
+
if (existing) {
|
|
1929
|
+
updateActauthRule(agentName, existing.name!, { scope: existing.scope, tool, decision: body.decision })
|
|
1930
|
+
} else {
|
|
1931
|
+
const base = `tool-${tool}`.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').slice(0, 80)
|
|
1932
|
+
let name = base
|
|
1933
|
+
for (let i = 2; rules.some((r) => r.name === name); i++) name = `${base}-${i}`
|
|
1934
|
+
addActauthRule(agentName, { name, scope: '*/*', tool, decision: body.decision })
|
|
1935
|
+
}
|
|
1936
|
+
} catch (err) {
|
|
1937
|
+
res.writeHead(500, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1938
|
+
return
|
|
1939
|
+
}
|
|
1940
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await actauthConfigWithAbilities(agentName)))
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
async function handleActauthGet(res: ServerResponse, agentName: string): Promise<void> {
|
|
1944
|
+
if (!getEntry(agentName)) {
|
|
1945
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1946
|
+
return
|
|
1947
|
+
}
|
|
1948
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await actauthConfigWithAbilities(agentName)))
|
|
1795
1949
|
}
|
|
1796
1950
|
|
|
1797
1951
|
async function handleActauthRulePost(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
@@ -1812,7 +1966,7 @@ async function handleActauthRulePost(req: IncomingMessage, res: ServerResponse,
|
|
|
1812
1966
|
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1813
1967
|
return
|
|
1814
1968
|
}
|
|
1815
|
-
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(
|
|
1969
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await actauthConfigWithAbilities(agentName)))
|
|
1816
1970
|
}
|
|
1817
1971
|
|
|
1818
1972
|
async function handleActauthRulePut(req: IncomingMessage, res: ServerResponse, agentName: string, ruleName: string): Promise<void> {
|
|
@@ -1833,10 +1987,10 @@ async function handleActauthRulePut(req: IncomingMessage, res: ServerResponse, a
|
|
|
1833
1987
|
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1834
1988
|
return
|
|
1835
1989
|
}
|
|
1836
|
-
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(
|
|
1990
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await actauthConfigWithAbilities(agentName)))
|
|
1837
1991
|
}
|
|
1838
1992
|
|
|
1839
|
-
function handleActauthRuleDelete(res: ServerResponse, agentName: string, ruleName: string): void {
|
|
1993
|
+
async function handleActauthRuleDelete(res: ServerResponse, agentName: string, ruleName: string): Promise<void> {
|
|
1840
1994
|
if (!getEntry(agentName)) {
|
|
1841
1995
|
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1842
1996
|
return
|
|
@@ -1848,7 +2002,7 @@ function handleActauthRuleDelete(res: ServerResponse, agentName: string, ruleNam
|
|
|
1848
2002
|
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1849
2003
|
return
|
|
1850
2004
|
}
|
|
1851
|
-
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(
|
|
2005
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await actauthConfigWithAbilities(agentName)))
|
|
1852
2006
|
}
|
|
1853
2007
|
|
|
1854
2008
|
async function handleActauthDefaultDecisionPut(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
@@ -1862,7 +2016,7 @@ async function handleActauthDefaultDecisionPut(req: IncomingMessage, res: Server
|
|
|
1862
2016
|
return
|
|
1863
2017
|
}
|
|
1864
2018
|
setDefaultDecision(agentName, body.decision)
|
|
1865
|
-
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(
|
|
2019
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await actauthConfigWithAbilities(agentName)))
|
|
1866
2020
|
}
|
|
1867
2021
|
|
|
1868
2022
|
// GET /agents content-negotiates on this: a browser navigating there sends
|
|
@@ -3416,6 +3570,19 @@ const server = createServer(async (req, res) => {
|
|
|
3416
3570
|
)
|
|
3417
3571
|
return
|
|
3418
3572
|
}
|
|
3573
|
+
// The model dropdown in the New agent and Model forms: what the
|
|
3574
|
+
// provider lists for the saved key. Never includes the key itself.
|
|
3575
|
+
const providerModelsMatch = req.method === 'GET' && pathname.match(/^\/config\/models\/([a-z]+)\/models$/)
|
|
3576
|
+
if (providerModelsMatch) {
|
|
3577
|
+
try {
|
|
3578
|
+
const view = await listProviderModels(providerModelsMatch[1])
|
|
3579
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(view))
|
|
3580
|
+
} catch (err) {
|
|
3581
|
+
const status = err instanceof ModelKeyError ? 400 : 500
|
|
3582
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
3583
|
+
}
|
|
3584
|
+
return
|
|
3585
|
+
}
|
|
3419
3586
|
// A provider's API key, from the Models page: write-only, never read
|
|
3420
3587
|
// back. Same rule as the Environment tab: refused on a server with no
|
|
3421
3588
|
// auth, since anyone who could reach it could replace the key.
|
|
@@ -3608,12 +3775,17 @@ const server = createServer(async (req, res) => {
|
|
|
3608
3775
|
return
|
|
3609
3776
|
}
|
|
3610
3777
|
if (notifierMatch && req.method === 'DELETE') {
|
|
3611
|
-
handleNotifierDelete(res, decodeURIComponent(notifierMatch[1]))
|
|
3778
|
+
handleNotifierDelete(res, decodeURIComponent(notifierMatch[1]), new URL(req.url ?? '/', 'http://localhost').searchParams.get('channel') ?? undefined)
|
|
3612
3779
|
return
|
|
3613
3780
|
}
|
|
3614
3781
|
const notifierTestMatch = req.method === 'POST' && pathname.match(/^\/agents\/([^/]+)\/notifier\/test$/)
|
|
3615
3782
|
if (notifierTestMatch) {
|
|
3616
|
-
await handleNotifierTest(res, decodeURIComponent(notifierTestMatch[1]))
|
|
3783
|
+
await handleNotifierTest(req, res, decodeURIComponent(notifierTestMatch[1]))
|
|
3784
|
+
return
|
|
3785
|
+
}
|
|
3786
|
+
const notifierPrimaryMatch = req.method === 'PUT' && pathname.match(/^\/agents\/([^/]+)\/notifier\/primary$/)
|
|
3787
|
+
if (notifierPrimaryMatch) {
|
|
3788
|
+
await handleNotifierPrimaryPut(req, res, decodeURIComponent(notifierPrimaryMatch[1]))
|
|
3617
3789
|
return
|
|
3618
3790
|
}
|
|
3619
3791
|
// Backs the console's Schedules tab — see handleSchedulesGet above.
|
|
@@ -3683,6 +3855,105 @@ const server = createServer(async (req, res) => {
|
|
|
3683
3855
|
// itself need to be excluded from matching as an agent name (it
|
|
3684
3856
|
// never would be one in practice, but this ordering means never
|
|
3685
3857
|
// having to think about it).
|
|
3858
|
+
// Private ability sources (Settings) and what they offer (the
|
|
3859
|
+
// Abilities page's Private section): web/ability-sources-admin.ts.
|
|
3860
|
+
// Saving or removing one stores a token, so it's refused without auth,
|
|
3861
|
+
// like a model key.
|
|
3862
|
+
if (pathname === '/config/ability-sources' && req.method === 'GET') {
|
|
3863
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify({ ...describeAbilitySources(), canSave: secretsProtected }))
|
|
3864
|
+
return
|
|
3865
|
+
}
|
|
3866
|
+
const abilitySourceMatch = pathname.match(/^\/config\/ability-sources(?:\/([a-z0-9-]+))?$/)
|
|
3867
|
+
if (abilitySourceMatch && (req.method === 'PUT' || req.method === 'DELETE')) {
|
|
3868
|
+
if (!secretsProtected) {
|
|
3869
|
+
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.' }))
|
|
3870
|
+
return
|
|
3871
|
+
}
|
|
3872
|
+
try {
|
|
3873
|
+
if (req.method === 'PUT') await saveAbilitySourceFromConsole(await readJsonBody(req))
|
|
3874
|
+
else if (abilitySourceMatch[1]) await removeAbilitySourceFromConsole(abilitySourceMatch[1])
|
|
3875
|
+
else throw new AbilitySourceError('Name the source to remove')
|
|
3876
|
+
} catch (err) {
|
|
3877
|
+
const status = err instanceof AbilitySourceError ? 400 : 500
|
|
3878
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
3879
|
+
return
|
|
3880
|
+
}
|
|
3881
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ...describeAbilitySources(), canSave: secretsProtected }))
|
|
3882
|
+
return
|
|
3883
|
+
}
|
|
3884
|
+
// What an ability can do: its tools (what each does, its permission)
|
|
3885
|
+
// and skills. `agent` adds that agent's current decisions.
|
|
3886
|
+
if (pathname === '/abilities/details' && req.method === 'GET') {
|
|
3887
|
+
const params = new URL(req.url ?? '/', 'http://localhost').searchParams
|
|
3888
|
+
const spec = params.get('spec')?.trim()
|
|
3889
|
+
const agent = params.get('agent') ?? undefined
|
|
3890
|
+
if (!spec || (agent && !getEntry(agent))) {
|
|
3891
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: spec ? `unknown agent '${agent}'` : 'spec is required' }))
|
|
3892
|
+
return
|
|
3893
|
+
}
|
|
3894
|
+
try {
|
|
3895
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await abilityDetails(spec, { agent })))
|
|
3896
|
+
} catch (err) {
|
|
3897
|
+
res.writeHead(502, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
3898
|
+
}
|
|
3899
|
+
return
|
|
3900
|
+
}
|
|
3901
|
+
// The workspace's private abilities (core/workspace-abilities.ts):
|
|
3902
|
+
// installed from the Abilities page, added to agents from their tab.
|
|
3903
|
+
if (pathname === '/abilities/workspace' && req.method === 'GET') {
|
|
3904
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify({ abilities: listWorkspaceAbilities() }))
|
|
3905
|
+
return
|
|
3906
|
+
}
|
|
3907
|
+
if (pathname === '/abilities/workspace' && (req.method === 'POST' || req.method === 'DELETE')) {
|
|
3908
|
+
try {
|
|
3909
|
+
if (req.method === 'POST') {
|
|
3910
|
+
const spec = (await readJsonBody(req)).spec
|
|
3911
|
+
if (typeof spec !== 'string' || !spec.trim()) throw new WorkspaceAbilityError('spec is required')
|
|
3912
|
+
await installWorkspaceAbility(spec.trim())
|
|
3913
|
+
} else {
|
|
3914
|
+
removeWorkspaceAbility(new URL(req.url ?? '/', 'http://localhost').searchParams.get('name') ?? '')
|
|
3915
|
+
}
|
|
3916
|
+
} catch (err) {
|
|
3917
|
+
const status = err instanceof WorkspaceAbilityError ? 400 : 502
|
|
3918
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
3919
|
+
return
|
|
3920
|
+
}
|
|
3921
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ abilities: listWorkspaceAbilities() }))
|
|
3922
|
+
return
|
|
3923
|
+
}
|
|
3924
|
+
// An ability uploaded from the browser (a .zip/.tgz, or a folder's
|
|
3925
|
+
// files, base64), stored in the workspace like one from a source.
|
|
3926
|
+
if (pathname === '/abilities/workspace/upload' && req.method === 'POST') {
|
|
3927
|
+
try {
|
|
3928
|
+
// Base64 adds a third, plus the JSON around it.
|
|
3929
|
+
const body = await readLimitedJsonBody(req, Math.ceil(MAX_UPLOAD_BYTES * 1.4) + 64 * 1024)
|
|
3930
|
+
const upload: AbilityUpload =
|
|
3931
|
+
body.kind === 'files' && Array.isArray(body.files)
|
|
3932
|
+
? { 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 ?? '') })) }
|
|
3933
|
+
: { kind: 'archive', fileName: String(body.fileName ?? ''), data: String(body.data ?? '') }
|
|
3934
|
+
await installUploadedAbility(upload)
|
|
3935
|
+
} catch (err) {
|
|
3936
|
+
const status = err instanceof WorkspaceAbilityError ? 400 : 500
|
|
3937
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
3938
|
+
return
|
|
3939
|
+
}
|
|
3940
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ abilities: listWorkspaceAbilities() }))
|
|
3941
|
+
return
|
|
3942
|
+
}
|
|
3943
|
+
// Public abilities on npm, for the Abilities page (no agent involved).
|
|
3944
|
+
if (pathname === '/abilities/search' && req.method === 'GET') {
|
|
3945
|
+
try {
|
|
3946
|
+
const results = await searchPublicAbilities(new URL(req.url ?? '/', 'http://localhost').searchParams.get('q') ?? undefined)
|
|
3947
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ results }))
|
|
3948
|
+
} catch (err) {
|
|
3949
|
+
res.writeHead(502, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
3950
|
+
}
|
|
3951
|
+
return
|
|
3952
|
+
}
|
|
3953
|
+
if (pathname === '/abilities/private' && req.method === 'GET') {
|
|
3954
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(await listPrivateAbilities()))
|
|
3955
|
+
return
|
|
3956
|
+
}
|
|
3686
3957
|
const abilitiesSearchMatch = pathname.match(/^\/agents\/([^/]+)\/abilities\/search$/)
|
|
3687
3958
|
if (abilitiesSearchMatch && req.method === 'GET') {
|
|
3688
3959
|
const query = new URL(req.url ?? '/', 'http://localhost').searchParams.get('q')
|
|
@@ -3729,13 +4000,19 @@ const server = createServer(async (req, res) => {
|
|
|
3729
4000
|
return
|
|
3730
4001
|
}
|
|
3731
4002
|
|
|
4003
|
+
const actauthToolMatch = req.method === 'PUT' && pathname.match(/^\/agents\/([^/]+)\/actauth\/tools\/([^/]+)$/)
|
|
4004
|
+
if (actauthToolMatch) {
|
|
4005
|
+
await handleActauthToolPut(req, res, decodeURIComponent(actauthToolMatch[1]), decodeURIComponent(actauthToolMatch[2]))
|
|
4006
|
+
return
|
|
4007
|
+
}
|
|
4008
|
+
|
|
3732
4009
|
const actauthRuleMatch = pathname.match(/^\/agents\/([^/]+)\/actauth\/rules\/([^/]+)$/)
|
|
3733
4010
|
if (actauthRuleMatch && req.method === 'PUT') {
|
|
3734
4011
|
await handleActauthRulePut(req, res, decodeURIComponent(actauthRuleMatch[1]), decodeURIComponent(actauthRuleMatch[2]))
|
|
3735
4012
|
return
|
|
3736
4013
|
}
|
|
3737
4014
|
if (actauthRuleMatch && req.method === 'DELETE') {
|
|
3738
|
-
handleActauthRuleDelete(res, decodeURIComponent(actauthRuleMatch[1]), decodeURIComponent(actauthRuleMatch[2]))
|
|
4015
|
+
await handleActauthRuleDelete(res, decodeURIComponent(actauthRuleMatch[1]), decodeURIComponent(actauthRuleMatch[2]))
|
|
3739
4016
|
return
|
|
3740
4017
|
}
|
|
3741
4018
|
|
|
@@ -3747,7 +4024,7 @@ const server = createServer(async (req, res) => {
|
|
|
3747
4024
|
|
|
3748
4025
|
const actauthMatch = req.method === 'GET' && pathname.match(/^\/agents\/([^/]+)\/actauth$/)
|
|
3749
4026
|
if (actauthMatch) {
|
|
3750
|
-
handleActauthGet(res, decodeURIComponent(actauthMatch[1]))
|
|
4027
|
+
await handleActauthGet(res, decodeURIComponent(actauthMatch[1]))
|
|
3751
4028
|
return
|
|
3752
4029
|
}
|
|
3753
4030
|
|