create-loopengine 0.1.31 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-loopengine",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Scaffold a new loopengine agent project — a runnable HTTP server with one example agent, in your own repo.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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,
@@ -920,8 +927,8 @@ function handleSkillDelete(res: ServerResponse, agentName: string, skillId: stri
920
927
  res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true }))
921
928
  }
922
929
 
923
- // Backs the Admin UI's "Environment" section — every env var a package
924
- // (see loopengine's own PACKAGES.md, bin/package-manager.ts) declared it
930
+ // Backs the Admin UI's "Environment" section — every env var an ability
931
+ // (see loopengine's own ABILITIES.md, bin/ability-manager.ts) declared it
925
932
  // needs for this agent, with set/not-set status only. Read-only, so no
926
933
  // extra auth gate beyond the normal Basic Auth middleware every admin
927
934
  // route already has.
@@ -938,7 +945,7 @@ function handleEnvGet(res: ServerResponse, agentName: string): void {
938
945
  // credential), so unlike every other route here, this one refuses
939
946
  // outright when LOOPENGINE_ADMIN_AUTH isn't set at all, rather than just
940
947
  // warning at startup and proceeding open like the rest of this file does
941
- // (see loopengine's own PACKAGES.md, "Managing package secrets in the
948
+ // (see loopengine's own ABILITIES.md, "Managing ability secrets in the
942
949
  // Admin UI" — LOOPENGINE_ADMIN_AUTH being optional today is an
943
950
  // acceptable gap for config, not for secrets).
944
951
  async function handleEnvPut(req: IncomingMessage, res: ServerResponse, agentName: string, varName: string): Promise<void> {
@@ -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