create-loopengine 0.1.32 → 0.1.33
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 +99 -0
package/package.json
CHANGED
|
@@ -95,6 +95,13 @@ import {
|
|
|
95
95
|
setDefaultDecision,
|
|
96
96
|
ActauthRuleExistsError,
|
|
97
97
|
ActauthRuleNotFoundError,
|
|
98
|
+
installAbility,
|
|
99
|
+
listInstalledAbilities,
|
|
100
|
+
AbilityAlreadyInstalledError,
|
|
101
|
+
AbilityCollisionError,
|
|
102
|
+
AbilityManifestError,
|
|
103
|
+
AbilityVersionError,
|
|
104
|
+
searchPublicAbilities,
|
|
98
105
|
describeModelProviders,
|
|
99
106
|
describeGateways,
|
|
100
107
|
scaffoldAgent,
|
|
@@ -967,6 +974,75 @@ async function handleEnvPut(req: IncomingMessage, res: ServerResponse, agentName
|
|
|
967
974
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true }))
|
|
968
975
|
}
|
|
969
976
|
|
|
977
|
+
// Backs the Admin UI's Abilities tab list — everything currently
|
|
978
|
+
// installed for this agent. Read-only, so no extra auth gate beyond the
|
|
979
|
+
// normal Basic Auth middleware every admin route already has.
|
|
980
|
+
function handleAbilitiesGet(res: ServerResponse, agentName: string): void {
|
|
981
|
+
if (!getEntry(agentName)) {
|
|
982
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
983
|
+
return
|
|
984
|
+
}
|
|
985
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ abilities: listInstalledAbilities(agentName) }))
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Backs the Abilities tab's search box — proxies to npm's own public
|
|
989
|
+
// registry search (loopengine's own searchPublicAbilities) so the
|
|
990
|
+
// browser never needs direct access to registry.npmjs.org, and the
|
|
991
|
+
// actual query text (the loopengine-ability keyword filter) stays a
|
|
992
|
+
// server-side detail rather than something the client has to know to
|
|
993
|
+
// construct.
|
|
994
|
+
async function handleAbilitiesSearchGet(res: ServerResponse, agentName: string, query: string | null): Promise<void> {
|
|
995
|
+
if (!getEntry(agentName)) {
|
|
996
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
997
|
+
return
|
|
998
|
+
}
|
|
999
|
+
try {
|
|
1000
|
+
const results = await searchPublicAbilities(query ?? undefined)
|
|
1001
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ results }))
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
res.writeHead(502, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Installs an ability — any spec `npm pack` accepts (a bare/scoped
|
|
1008
|
+
// registry name, a pinned version, a git URL, or a local `file:../path`
|
|
1009
|
+
// for a private ability) — into this agent's own tree. Meaningfully
|
|
1010
|
+
// more sensitive than the rest of this admin surface: this runs
|
|
1011
|
+
// arbitrary third-party code inside the agent's project the next time
|
|
1012
|
+
// it's loaded, not just a config value or a credential. Same posture as
|
|
1013
|
+
// handleEnvPut's own secret-writing gate above — refuses outright when
|
|
1014
|
+
// LOOPENGINE_ADMIN_AUTH isn't configured at all, rather than proceeding
|
|
1015
|
+
// open like most routes here do.
|
|
1016
|
+
async function handleAbilityInstallPost(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
1017
|
+
if (!adminAuth) {
|
|
1018
|
+
res.writeHead(403, { 'content-type': 'application/json' }).end(
|
|
1019
|
+
JSON.stringify({ error: 'Refusing to install an ability without LOOPENGINE_ADMIN_AUTH configured — set it before installing abilities through this route.' }),
|
|
1020
|
+
)
|
|
1021
|
+
return
|
|
1022
|
+
}
|
|
1023
|
+
if (!getEntry(agentName)) {
|
|
1024
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1025
|
+
return
|
|
1026
|
+
}
|
|
1027
|
+
const body = await readJsonBody(req)
|
|
1028
|
+
if (typeof body.spec !== 'string' || !body.spec) {
|
|
1029
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'spec is required' }))
|
|
1030
|
+
return
|
|
1031
|
+
}
|
|
1032
|
+
try {
|
|
1033
|
+
const result = await installAbility(agentName, body.spec)
|
|
1034
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(result))
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
const status =
|
|
1037
|
+
err instanceof AbilityAlreadyInstalledError || err instanceof AbilityCollisionError
|
|
1038
|
+
? 409
|
|
1039
|
+
: err instanceof AbilityManifestError || err instanceof AbilityVersionError
|
|
1040
|
+
? 422
|
|
1041
|
+
: 500
|
|
1042
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
970
1046
|
// Backs the Actauth tab's rule editor — parses a rule body shared by both
|
|
971
1047
|
// the add (POST) and update (PUT, minus `name`) routes below.
|
|
972
1048
|
function parseActauthRuleBody(body: Record<string, unknown>, requireName: boolean): { ok: true; value: { name: string; scope: string; tool: string; decision: Decision } } | { ok: false; error: string } {
|
|
@@ -2116,6 +2192,29 @@ const server = createServer(async (req, res) => {
|
|
|
2116
2192
|
return
|
|
2117
2193
|
}
|
|
2118
2194
|
|
|
2195
|
+
// Backs the Admin UI's Abilities tab — see handleAbilitiesGet/
|
|
2196
|
+
// handleAbilitiesSearchGet/handleAbilityInstallPost's own doc
|
|
2197
|
+
// comments above. The /search route is checked before the plain
|
|
2198
|
+
// :agentName routes right below since 'search' would otherwise
|
|
2199
|
+
// itself need to be excluded from matching as an agent name (it
|
|
2200
|
+
// never would be one in practice, but this ordering means never
|
|
2201
|
+
// having to think about it).
|
|
2202
|
+
const abilitiesSearchMatch = pathname.match(/^\/agents\/([^/]+)\/abilities\/search$/)
|
|
2203
|
+
if (abilitiesSearchMatch && req.method === 'GET') {
|
|
2204
|
+
const query = new URL(req.url ?? '/', 'http://localhost').searchParams.get('q')
|
|
2205
|
+
await handleAbilitiesSearchGet(res, decodeURIComponent(abilitiesSearchMatch[1]), query)
|
|
2206
|
+
return
|
|
2207
|
+
}
|
|
2208
|
+
const abilitiesMatch = pathname.match(/^\/agents\/([^/]+)\/abilities$/)
|
|
2209
|
+
if (abilitiesMatch && req.method === 'GET') {
|
|
2210
|
+
handleAbilitiesGet(res, decodeURIComponent(abilitiesMatch[1]))
|
|
2211
|
+
return
|
|
2212
|
+
}
|
|
2213
|
+
if (abilitiesMatch && req.method === 'POST') {
|
|
2214
|
+
await handleAbilityInstallPost(req, res, decodeURIComponent(abilitiesMatch[1]))
|
|
2215
|
+
return
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2119
2218
|
// Backs the Actauth tab's rule editor and default_decision control.
|
|
2120
2219
|
// The default-decision route is checked before the :ruleName routes
|
|
2121
2220
|
// right below since 'default-decision' would otherwise itself match
|