create-loopengine 0.1.48 → 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.48",
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,
@@ -1024,6 +1028,14 @@ function handleEnvGet(res: ServerResponse, agentName: string): void {
1024
1028
  res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(listDeclaredEnvVars(agentName)))
1025
1029
  }
1026
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
+
1027
1039
  // Writes a raw secret value into .env — meaningfully more sensitive than
1028
1040
  // the rest of the admin surface (config/business data vs. an actual
1029
1041
  // credential), so unlike every other route here, this one refuses
@@ -1043,16 +1055,31 @@ async function handleEnvPut(req: IncomingMessage, res: ServerResponse, agentName
1043
1055
  res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
1044
1056
  return
1045
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
+ }
1046
1063
  const body = await readJsonBody(req)
1047
1064
  if (typeof body.value !== 'string') {
1048
1065
  res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'value is required' }))
1049
1066
  return
1050
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
+ }
1051
1078
  // A dropdown-backed var (AbilityEnvDecl.options) is still writable
1052
1079
  // through this same raw route — worth rejecting an out-of-set value
1053
1080
  // here too, not just skipping validation because the Admin UI's own
1054
1081
  // <select> would never have produced one.
1055
- const declared = listDeclaredEnvVars(agentName).find((v) => v.name === varName)
1082
+ const declared = declaredRows.find((v) => v.slot === slot) ?? declaredRows[0]
1056
1083
  if (declared?.options && !declared.options.includes(body.value)) {
1057
1084
  res.writeHead(400, { 'content-type': 'application/json' }).end(
1058
1085
  JSON.stringify({ error: `"${body.value}" isn't one of this var's allowed values: ${declared.options.join(', ')}` }),
@@ -1060,7 +1087,8 @@ async function handleEnvPut(req: IncomingMessage, res: ServerResponse, agentName
1060
1087
  return
1061
1088
  }
1062
1089
  try {
1063
- setEnvVar(varName, body.value)
1090
+ if (slot === 'agent') setAgentEnvVar(agentName, varName, body.value)
1091
+ else setEnvVar(varName, body.value)
1064
1092
  } catch (err) {
1065
1093
  const status = err instanceof EnvVarNameError ? 400 : 500
1066
1094
  res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
@@ -1073,7 +1101,7 @@ async function handleEnvPut(req: IncomingMessage, res: ServerResponse, agentName
1073
1101
  // removing a secret from .env is still a secrets-management action,
1074
1102
  // not just config, so this refuses outright rather than proceeding
1075
1103
  // open when LOOPENGINE_ADMIN_AUTH isn't configured at all.
1076
- 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> {
1077
1105
  if (!adminAuth) {
1078
1106
  res.writeHead(403, { 'content-type': 'application/json' }).end(
1079
1107
  JSON.stringify({ error: 'Refusing to unset an env var without LOOPENGINE_ADMIN_AUTH configured — set it before managing secrets through this route.' }),
@@ -1084,8 +1112,14 @@ async function handleEnvDelete(res: ServerResponse, agentName: string, varName:
1084
1112
  res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
1085
1113
  return
1086
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
+ }
1087
1120
  try {
1088
- unsetEnvVar(varName)
1121
+ if (slot === 'agent') unsetAgentEnvVar(agentName, varName)
1122
+ else unsetEnvVar(varName)
1089
1123
  } catch (err) {
1090
1124
  const status = err instanceof EnvVarNameError ? 400 : 500
1091
1125
  res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
@@ -1927,7 +1961,12 @@ async function handlePendingApprovalResolve(req: IncomingMessage, res: ServerRes
1927
1961
  return { checkpoint, result: { kind: 'validation-error', error: validationError } }
1928
1962
  }
1929
1963
  try {
1930
- 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
+ })
1931
1970
  resultBlock = { type: 'tool_result', tool_use_id: item.toolUseId, content: JSON.stringify(output), is_error: false }
1932
1971
  } catch (err) {
1933
1972
  resultBlock = { type: 'tool_result', tool_use_id: item.toolUseId, content: `ERROR: ${err}`, is_error: true }
@@ -2626,7 +2665,7 @@ const server = createServer(async (req, res) => {
2626
2665
  return
2627
2666
  }
2628
2667
  if (envVarMatch && req.method === 'DELETE') {
2629
- await handleEnvDelete(res, decodeURIComponent(envVarMatch[1]), decodeURIComponent(envVarMatch[2]))
2668
+ await handleEnvDelete(req, res, decodeURIComponent(envVarMatch[1]), decodeURIComponent(envVarMatch[2]))
2630
2669
  return
2631
2670
  }
2632
2671
 
@@ -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": {