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 +1 -1
- package/template/adapters/http.ts +80 -6
- package/template/package.json +1 -1
package/package.json
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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]))
|