create-loopengine 0.1.46 → 0.1.48

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.46",
3
+ "version": "0.1.48",
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",
@@ -93,6 +93,7 @@ import {
93
93
  SkillNotFoundError,
94
94
  listDeclaredEnvVars,
95
95
  setEnvVar,
96
+ unsetEnvVar,
96
97
  EnvVarNameError,
97
98
  readActauthConfig,
98
99
  addActauthRule,
@@ -103,6 +104,7 @@ import {
103
104
  ActauthRuleNotFoundError,
104
105
  installAbility,
105
106
  upgradeAbility,
107
+ removeAbility,
106
108
  listInstalledAbilities,
107
109
  backfillDependencies,
108
110
  AbilityAlreadyInstalledError,
@@ -1067,6 +1069,31 @@ async function handleEnvPut(req: IncomingMessage, res: ServerResponse, agentName
1067
1069
  res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true }))
1068
1070
  }
1069
1071
 
1072
+ // Same auth requirement as handleEnvPut above and the same reasoning —
1073
+ // removing a secret from .env is still a secrets-management action,
1074
+ // not just config, so this refuses outright rather than proceeding
1075
+ // open when LOOPENGINE_ADMIN_AUTH isn't configured at all.
1076
+ async function handleEnvDelete(res: ServerResponse, agentName: string, varName: string): Promise<void> {
1077
+ if (!adminAuth) {
1078
+ res.writeHead(403, { 'content-type': 'application/json' }).end(
1079
+ JSON.stringify({ error: 'Refusing to unset an env var without LOOPENGINE_ADMIN_AUTH configured — set it before managing secrets through this route.' }),
1080
+ )
1081
+ return
1082
+ }
1083
+ if (!getEntry(agentName)) {
1084
+ res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
1085
+ return
1086
+ }
1087
+ try {
1088
+ unsetEnvVar(varName)
1089
+ } catch (err) {
1090
+ const status = err instanceof EnvVarNameError ? 400 : 500
1091
+ res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
1092
+ return
1093
+ }
1094
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true }))
1095
+ }
1096
+
1070
1097
  // A cheap, good-enough presence check — existsSync(node_modules/<dep>)
1071
1098
  // — rather than an actual module resolution (require.resolve/
1072
1099
  // import.meta.resolve): this only ever decides whether to show an
@@ -1202,6 +1229,34 @@ async function handleAbilityUpgradePost(req: IncomingMessage, res: ServerRespons
1202
1229
  }
1203
1230
  }
1204
1231
 
1232
+ // Uninstalls an ability — deletes its tool/skill files and actauth
1233
+ // rules (bin/ability-manager.ts's own removeAbility). Same admin-auth
1234
+ // posture as install/upgrade above: this mutates the agent's own tree,
1235
+ // even though — unlike install/upgrade — it never runs new third-party
1236
+ // code. `removed`/`refused` pass straight through on success (not an
1237
+ // error: a dirty file left in place, same as remove-ability's own CLI
1238
+ // behavior, is a normal partial outcome the Admin UI shows and offers
1239
+ // to retry with force, not a failure).
1240
+ async function handleAbilityUninstallDelete(res: ServerResponse, agentName: string, abilityName: string, force: boolean): Promise<void> {
1241
+ if (!adminAuth) {
1242
+ res.writeHead(403, { 'content-type': 'application/json' }).end(
1243
+ JSON.stringify({ error: 'Refusing to uninstall an ability without LOOPENGINE_ADMIN_AUTH configured — set it before uninstalling abilities through this route.' }),
1244
+ )
1245
+ return
1246
+ }
1247
+ if (!getEntry(agentName)) {
1248
+ res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
1249
+ return
1250
+ }
1251
+ try {
1252
+ const result = removeAbility(agentName, abilityName, force)
1253
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(result))
1254
+ } catch (err) {
1255
+ const status = err instanceof AbilityNotInstalledError ? 404 : 500
1256
+ res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
1257
+ }
1258
+ }
1259
+
1205
1260
  const execFileAsync = promisify(execFile)
1206
1261
  const NPM_INSTALL_DEPS_TIMEOUT_MS = 180_000
1207
1262
 
@@ -2570,6 +2625,10 @@ const server = createServer(async (req, res) => {
2570
2625
  await handleEnvPut(req, res, decodeURIComponent(envVarMatch[1]), decodeURIComponent(envVarMatch[2]))
2571
2626
  return
2572
2627
  }
2628
+ if (envVarMatch && req.method === 'DELETE') {
2629
+ await handleEnvDelete(res, decodeURIComponent(envVarMatch[1]), decodeURIComponent(envVarMatch[2]))
2630
+ return
2631
+ }
2573
2632
 
2574
2633
  // Backs the Admin UI's Abilities tab — see handleAbilitiesGet/
2575
2634
  // handleAbilitiesSearchGet/handleAbilityInstallPost's own doc
@@ -2594,6 +2653,12 @@ const server = createServer(async (req, res) => {
2594
2653
  await handleAbilityInstallDepsPost(req, res, decodeURIComponent(abilityInstallDepsMatch[1]), decodeURIComponent(abilityInstallDepsMatch[2]))
2595
2654
  return
2596
2655
  }
2656
+ const abilityMatch = pathname.match(/^\/agents\/([^/]+)\/abilities\/([^/]+)$/)
2657
+ if (abilityMatch && req.method === 'DELETE') {
2658
+ const force = new URL(req.url ?? '/', 'http://localhost').searchParams.get('force') === 'true'
2659
+ await handleAbilityUninstallDelete(res, decodeURIComponent(abilityMatch[1]), decodeURIComponent(abilityMatch[2]), force)
2660
+ return
2661
+ }
2597
2662
  const abilitiesMatch = pathname.match(/^\/agents\/([^/]+)\/abilities$/)
2598
2663
  if (abilitiesMatch && req.method === 'GET') {
2599
2664
  await handleAbilitiesGet(res, decodeURIComponent(abilitiesMatch[1]))