create-loopengine 0.1.47 → 0.1.49

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.47",
3
+ "version": "0.1.49",
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",
@@ -94,6 +94,10 @@ import {
94
94
  listDeclaredEnvVars,
95
95
  setEnvVar,
96
96
  unsetEnvVar,
97
+ setAgentEnvVar,
98
+ unsetAgentEnvVar,
99
+ createAgentEnv,
100
+ agentDirFor,
97
101
  EnvVarNameError,
98
102
  readActauthConfig,
99
103
  addActauthRule,
@@ -104,6 +108,7 @@ import {
104
108
  ActauthRuleNotFoundError,
105
109
  installAbility,
106
110
  upgradeAbility,
111
+ removeAbility,
107
112
  listInstalledAbilities,
108
113
  backfillDependencies,
109
114
  AbilityAlreadyInstalledError,
@@ -1023,6 +1028,14 @@ function handleEnvGet(res: ServerResponse, agentName: string): void {
1023
1028
  res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(listDeclaredEnvVars(agentName)))
1024
1029
  }
1025
1030
 
1031
+ // `?slot=agent` targets this agent's own agents/<name>/.env (a per-agent
1032
+ // value, read by its tools through ToolContext.env) instead of the
1033
+ // project-wide .env — see loopengine's DeclaredEnvVar.slot.
1034
+ function envSlotOf(req: IncomingMessage): 'shared' | 'agent' | undefined {
1035
+ const slot = new URL(req.url ?? '/', 'http://localhost').searchParams.get('slot') ?? 'shared'
1036
+ return slot === 'shared' || slot === 'agent' ? slot : undefined
1037
+ }
1038
+
1026
1039
  // Writes a raw secret value into .env — meaningfully more sensitive than
1027
1040
  // the rest of the admin surface (config/business data vs. an actual
1028
1041
  // credential), so unlike every other route here, this one refuses
@@ -1042,16 +1055,31 @@ async function handleEnvPut(req: IncomingMessage, res: ServerResponse, agentName
1042
1055
  res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
1043
1056
  return
1044
1057
  }
1058
+ const slot = envSlotOf(req)
1059
+ if (!slot) {
1060
+ res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: "slot must be 'shared' or 'agent'" }))
1061
+ return
1062
+ }
1045
1063
  const body = await readJsonBody(req)
1046
1064
  if (typeof body.value !== 'string') {
1047
1065
  res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'value is required' }))
1048
1066
  return
1049
1067
  }
1068
+ const declaredRows = listDeclaredEnvVars(agentName).filter((v) => v.name === varName)
1069
+ // A shared value for an agent-scoped var would never be read
1070
+ // (loopengine's createAgentEnv never falls back for one) — refuse it
1071
+ // rather than accept a write that silently does nothing.
1072
+ if (slot === 'shared' && declaredRows.some((v) => v.scope === 'agent')) {
1073
+ res.writeHead(400, { 'content-type': 'application/json' }).end(
1074
+ JSON.stringify({ error: `${varName} is declared scope 'agent' — set it per agent (slot=agent), not project-wide.` }),
1075
+ )
1076
+ return
1077
+ }
1050
1078
  // A dropdown-backed var (AbilityEnvDecl.options) is still writable
1051
1079
  // through this same raw route — worth rejecting an out-of-set value
1052
1080
  // here too, not just skipping validation because the Admin UI's own
1053
1081
  // <select> would never have produced one.
1054
- const declared = listDeclaredEnvVars(agentName).find((v) => v.name === varName)
1082
+ const declared = declaredRows.find((v) => v.slot === slot) ?? declaredRows[0]
1055
1083
  if (declared?.options && !declared.options.includes(body.value)) {
1056
1084
  res.writeHead(400, { 'content-type': 'application/json' }).end(
1057
1085
  JSON.stringify({ error: `"${body.value}" isn't one of this var's allowed values: ${declared.options.join(', ')}` }),
@@ -1059,7 +1087,8 @@ async function handleEnvPut(req: IncomingMessage, res: ServerResponse, agentName
1059
1087
  return
1060
1088
  }
1061
1089
  try {
1062
- setEnvVar(varName, body.value)
1090
+ if (slot === 'agent') setAgentEnvVar(agentName, varName, body.value)
1091
+ else setEnvVar(varName, body.value)
1063
1092
  } catch (err) {
1064
1093
  const status = err instanceof EnvVarNameError ? 400 : 500
1065
1094
  res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
@@ -1072,7 +1101,7 @@ async function handleEnvPut(req: IncomingMessage, res: ServerResponse, agentName
1072
1101
  // removing a secret from .env is still a secrets-management action,
1073
1102
  // not just config, so this refuses outright rather than proceeding
1074
1103
  // open when LOOPENGINE_ADMIN_AUTH isn't configured at all.
1075
- async function handleEnvDelete(res: ServerResponse, agentName: string, varName: string): Promise<void> {
1104
+ async function handleEnvDelete(req: IncomingMessage, res: ServerResponse, agentName: string, varName: string): Promise<void> {
1076
1105
  if (!adminAuth) {
1077
1106
  res.writeHead(403, { 'content-type': 'application/json' }).end(
1078
1107
  JSON.stringify({ error: 'Refusing to unset an env var without LOOPENGINE_ADMIN_AUTH configured — set it before managing secrets through this route.' }),
@@ -1083,8 +1112,14 @@ async function handleEnvDelete(res: ServerResponse, agentName: string, varName:
1083
1112
  res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
1084
1113
  return
1085
1114
  }
1115
+ const slot = envSlotOf(req)
1116
+ if (!slot) {
1117
+ res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: "slot must be 'shared' or 'agent'" }))
1118
+ return
1119
+ }
1086
1120
  try {
1087
- unsetEnvVar(varName)
1121
+ if (slot === 'agent') unsetAgentEnvVar(agentName, varName)
1122
+ else unsetEnvVar(varName)
1088
1123
  } catch (err) {
1089
1124
  const status = err instanceof EnvVarNameError ? 400 : 500
1090
1125
  res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
@@ -1228,6 +1263,34 @@ async function handleAbilityUpgradePost(req: IncomingMessage, res: ServerRespons
1228
1263
  }
1229
1264
  }
1230
1265
 
1266
+ // Uninstalls an ability — deletes its tool/skill files and actauth
1267
+ // rules (bin/ability-manager.ts's own removeAbility). Same admin-auth
1268
+ // posture as install/upgrade above: this mutates the agent's own tree,
1269
+ // even though — unlike install/upgrade — it never runs new third-party
1270
+ // code. `removed`/`refused` pass straight through on success (not an
1271
+ // error: a dirty file left in place, same as remove-ability's own CLI
1272
+ // behavior, is a normal partial outcome the Admin UI shows and offers
1273
+ // to retry with force, not a failure).
1274
+ async function handleAbilityUninstallDelete(res: ServerResponse, agentName: string, abilityName: string, force: boolean): Promise<void> {
1275
+ if (!adminAuth) {
1276
+ res.writeHead(403, { 'content-type': 'application/json' }).end(
1277
+ JSON.stringify({ error: 'Refusing to uninstall an ability without LOOPENGINE_ADMIN_AUTH configured — set it before uninstalling abilities through this route.' }),
1278
+ )
1279
+ return
1280
+ }
1281
+ if (!getEntry(agentName)) {
1282
+ res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
1283
+ return
1284
+ }
1285
+ try {
1286
+ const result = removeAbility(agentName, abilityName, force)
1287
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(result))
1288
+ } catch (err) {
1289
+ const status = err instanceof AbilityNotInstalledError ? 404 : 500
1290
+ res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
1291
+ }
1292
+ }
1293
+
1231
1294
  const execFileAsync = promisify(execFile)
1232
1295
  const NPM_INSTALL_DEPS_TIMEOUT_MS = 180_000
1233
1296
 
@@ -1898,7 +1961,12 @@ async function handlePendingApprovalResolve(req: IncomingMessage, res: ServerRes
1898
1961
  return { checkpoint, result: { kind: 'validation-error', error: validationError } }
1899
1962
  }
1900
1963
  try {
1901
- const output = await tool.execute(editedArgs ?? item.args)
1964
+ const output = await tool.execute(editedArgs ?? item.args, {
1965
+ agentName: entry.config.name,
1966
+ tenant: checkpoint.tenant,
1967
+ sessionId: checkpoint.sessionId,
1968
+ env: createAgentEnv(entry.config.name, agentDirFor(entry.config)),
1969
+ })
1902
1970
  resultBlock = { type: 'tool_result', tool_use_id: item.toolUseId, content: JSON.stringify(output), is_error: false }
1903
1971
  } catch (err) {
1904
1972
  resultBlock = { type: 'tool_result', tool_use_id: item.toolUseId, content: `ERROR: ${err}`, is_error: true }
@@ -2597,7 +2665,7 @@ const server = createServer(async (req, res) => {
2597
2665
  return
2598
2666
  }
2599
2667
  if (envVarMatch && req.method === 'DELETE') {
2600
- await handleEnvDelete(res, decodeURIComponent(envVarMatch[1]), decodeURIComponent(envVarMatch[2]))
2668
+ await handleEnvDelete(req, res, decodeURIComponent(envVarMatch[1]), decodeURIComponent(envVarMatch[2]))
2601
2669
  return
2602
2670
  }
2603
2671
 
@@ -2624,6 +2692,12 @@ const server = createServer(async (req, res) => {
2624
2692
  await handleAbilityInstallDepsPost(req, res, decodeURIComponent(abilityInstallDepsMatch[1]), decodeURIComponent(abilityInstallDepsMatch[2]))
2625
2693
  return
2626
2694
  }
2695
+ const abilityMatch = pathname.match(/^\/agents\/([^/]+)\/abilities\/([^/]+)$/)
2696
+ if (abilityMatch && req.method === 'DELETE') {
2697
+ const force = new URL(req.url ?? '/', 'http://localhost').searchParams.get('force') === 'true'
2698
+ await handleAbilityUninstallDelete(res, decodeURIComponent(abilityMatch[1]), decodeURIComponent(abilityMatch[2]), force)
2699
+ return
2700
+ }
2627
2701
  const abilitiesMatch = pathname.match(/^\/agents\/([^/]+)\/abilities$/)
2628
2702
  if (abilitiesMatch && req.method === 'GET') {
2629
2703
  await handleAbilitiesGet(res, decodeURIComponent(abilitiesMatch[1]))
@@ -9,7 +9,7 @@
9
9
  "cli": "tsx adapters/cli.ts"
10
10
  },
11
11
  "dependencies": {
12
- "loopengine": "^0.1.23",
12
+ "loopengine": "^0.1.62",
13
13
  "actauth": "^0.0.12"
14
14
  },
15
15
  "devDependencies": {