create-loopengine 0.1.32 → 0.1.34
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 +128 -2
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,
|
|
@@ -861,7 +868,18 @@ async function handleEditAgent(req: IncomingMessage, res: ServerResponse, agentN
|
|
|
861
868
|
}
|
|
862
869
|
updateAgent(agentName, { config: configPatch, createModelCall })
|
|
863
870
|
|
|
864
|
-
|
|
871
|
+
// Same "compute before writeHead" reasoning as the GET .../config
|
|
872
|
+
// route above — describeAgent can throw (a broken tools/index.ts
|
|
873
|
+
// import), and chaining it straight into .end()'s argument would send
|
|
874
|
+
// a 200 status before finding that out.
|
|
875
|
+
let described: Record<string, unknown>
|
|
876
|
+
try {
|
|
877
|
+
described = await describeAgent(getEntry(agentName)!)
|
|
878
|
+
} catch (err) {
|
|
879
|
+
res.writeHead(500, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
880
|
+
return
|
|
881
|
+
}
|
|
882
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(described))
|
|
865
883
|
}
|
|
866
884
|
|
|
867
885
|
// Backs the Skills tab's edit form (GET .../skills/:skillId to populate
|
|
@@ -967,6 +985,75 @@ async function handleEnvPut(req: IncomingMessage, res: ServerResponse, agentName
|
|
|
967
985
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true }))
|
|
968
986
|
}
|
|
969
987
|
|
|
988
|
+
// Backs the Admin UI's Abilities tab list — everything currently
|
|
989
|
+
// installed for this agent. Read-only, so no extra auth gate beyond the
|
|
990
|
+
// normal Basic Auth middleware every admin route already has.
|
|
991
|
+
function handleAbilitiesGet(res: ServerResponse, agentName: string): void {
|
|
992
|
+
if (!getEntry(agentName)) {
|
|
993
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
994
|
+
return
|
|
995
|
+
}
|
|
996
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ abilities: listInstalledAbilities(agentName) }))
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Backs the Abilities tab's search box — proxies to npm's own public
|
|
1000
|
+
// registry search (loopengine's own searchPublicAbilities) so the
|
|
1001
|
+
// browser never needs direct access to registry.npmjs.org, and the
|
|
1002
|
+
// actual query text (the loopengine-ability keyword filter) stays a
|
|
1003
|
+
// server-side detail rather than something the client has to know to
|
|
1004
|
+
// construct.
|
|
1005
|
+
async function handleAbilitiesSearchGet(res: ServerResponse, agentName: string, query: string | null): Promise<void> {
|
|
1006
|
+
if (!getEntry(agentName)) {
|
|
1007
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1008
|
+
return
|
|
1009
|
+
}
|
|
1010
|
+
try {
|
|
1011
|
+
const results = await searchPublicAbilities(query ?? undefined)
|
|
1012
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ results }))
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
res.writeHead(502, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Installs an ability — any spec `npm pack` accepts (a bare/scoped
|
|
1019
|
+
// registry name, a pinned version, a git URL, or a local `file:../path`
|
|
1020
|
+
// for a private ability) — into this agent's own tree. Meaningfully
|
|
1021
|
+
// more sensitive than the rest of this admin surface: this runs
|
|
1022
|
+
// arbitrary third-party code inside the agent's project the next time
|
|
1023
|
+
// it's loaded, not just a config value or a credential. Same posture as
|
|
1024
|
+
// handleEnvPut's own secret-writing gate above — refuses outright when
|
|
1025
|
+
// LOOPENGINE_ADMIN_AUTH isn't configured at all, rather than proceeding
|
|
1026
|
+
// open like most routes here do.
|
|
1027
|
+
async function handleAbilityInstallPost(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
1028
|
+
if (!adminAuth) {
|
|
1029
|
+
res.writeHead(403, { 'content-type': 'application/json' }).end(
|
|
1030
|
+
JSON.stringify({ error: 'Refusing to install an ability without LOOPENGINE_ADMIN_AUTH configured — set it before installing abilities through this route.' }),
|
|
1031
|
+
)
|
|
1032
|
+
return
|
|
1033
|
+
}
|
|
1034
|
+
if (!getEntry(agentName)) {
|
|
1035
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1036
|
+
return
|
|
1037
|
+
}
|
|
1038
|
+
const body = await readJsonBody(req)
|
|
1039
|
+
if (typeof body.spec !== 'string' || !body.spec) {
|
|
1040
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'spec is required' }))
|
|
1041
|
+
return
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
const result = await installAbility(agentName, body.spec)
|
|
1045
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(result))
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
const status =
|
|
1048
|
+
err instanceof AbilityAlreadyInstalledError || err instanceof AbilityCollisionError
|
|
1049
|
+
? 409
|
|
1050
|
+
: err instanceof AbilityManifestError || err instanceof AbilityVersionError
|
|
1051
|
+
? 422
|
|
1052
|
+
: 500
|
|
1053
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
970
1057
|
// Backs the Actauth tab's rule editor — parses a rule body shared by both
|
|
971
1058
|
// the add (POST) and update (PUT, minus `name`) routes below.
|
|
972
1059
|
function parseActauthRuleBody(body: Record<string, unknown>, requireName: boolean): { ok: true; value: { name: string; scope: string; tool: string; decision: Decision } } | { ok: false; error: string } {
|
|
@@ -2011,7 +2098,23 @@ const server = createServer(async (req, res) => {
|
|
|
2011
2098
|
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
2012
2099
|
return
|
|
2013
2100
|
}
|
|
2014
|
-
|
|
2101
|
+
// describeAgent dynamically imports this agent's own tools/index.ts
|
|
2102
|
+
// (loadDefaultTools) — a broken import there (a bad tool file, a
|
|
2103
|
+
// dependency the agent's own code needs but doesn't have) throws.
|
|
2104
|
+
// Computed before writeHead, not chained directly into .end() as an
|
|
2105
|
+
// argument — chaining would send the 200 status line first, then
|
|
2106
|
+
// throw while still computing the body, leaving the connection
|
|
2107
|
+
// open with headers sent but no body ever written (a client-side
|
|
2108
|
+
// "Unexpected end of JSON input", not a clear error) instead of a
|
|
2109
|
+
// real 500 with an actual message.
|
|
2110
|
+
let described: Record<string, unknown>
|
|
2111
|
+
try {
|
|
2112
|
+
described = await describeAgent(entry)
|
|
2113
|
+
} catch (err) {
|
|
2114
|
+
res.writeHead(500, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
2115
|
+
return
|
|
2116
|
+
}
|
|
2117
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(described))
|
|
2015
2118
|
return
|
|
2016
2119
|
}
|
|
2017
2120
|
|
|
@@ -2116,6 +2219,29 @@ const server = createServer(async (req, res) => {
|
|
|
2116
2219
|
return
|
|
2117
2220
|
}
|
|
2118
2221
|
|
|
2222
|
+
// Backs the Admin UI's Abilities tab — see handleAbilitiesGet/
|
|
2223
|
+
// handleAbilitiesSearchGet/handleAbilityInstallPost's own doc
|
|
2224
|
+
// comments above. The /search route is checked before the plain
|
|
2225
|
+
// :agentName routes right below since 'search' would otherwise
|
|
2226
|
+
// itself need to be excluded from matching as an agent name (it
|
|
2227
|
+
// never would be one in practice, but this ordering means never
|
|
2228
|
+
// having to think about it).
|
|
2229
|
+
const abilitiesSearchMatch = pathname.match(/^\/agents\/([^/]+)\/abilities\/search$/)
|
|
2230
|
+
if (abilitiesSearchMatch && req.method === 'GET') {
|
|
2231
|
+
const query = new URL(req.url ?? '/', 'http://localhost').searchParams.get('q')
|
|
2232
|
+
await handleAbilitiesSearchGet(res, decodeURIComponent(abilitiesSearchMatch[1]), query)
|
|
2233
|
+
return
|
|
2234
|
+
}
|
|
2235
|
+
const abilitiesMatch = pathname.match(/^\/agents\/([^/]+)\/abilities$/)
|
|
2236
|
+
if (abilitiesMatch && req.method === 'GET') {
|
|
2237
|
+
handleAbilitiesGet(res, decodeURIComponent(abilitiesMatch[1]))
|
|
2238
|
+
return
|
|
2239
|
+
}
|
|
2240
|
+
if (abilitiesMatch && req.method === 'POST') {
|
|
2241
|
+
await handleAbilityInstallPost(req, res, decodeURIComponent(abilitiesMatch[1]))
|
|
2242
|
+
return
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2119
2245
|
// Backs the Actauth tab's rule editor and default_decision control.
|
|
2120
2246
|
// The default-decision route is checked before the :ruleName routes
|
|
2121
2247
|
// right below since 'default-decision' would otherwise itself match
|