create-loopengine 0.1.65 → 0.1.67
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 +88 -1
- package/template/package.json +1 -1
package/package.json
CHANGED
|
@@ -125,6 +125,13 @@ import {
|
|
|
125
125
|
listAgentSlackChannels,
|
|
126
126
|
slackCredentialsFor,
|
|
127
127
|
slackConversationsOn,
|
|
128
|
+
slackClickAllowed,
|
|
129
|
+
agentApprovers,
|
|
130
|
+
setAgentApprovers,
|
|
131
|
+
listAgentSlackMembers,
|
|
132
|
+
replyPrivatelyInSlack,
|
|
133
|
+
type MessagingDecision,
|
|
134
|
+
type DecisionOutcome,
|
|
128
135
|
openSlackAnswerModal,
|
|
129
136
|
parseSlackInteraction,
|
|
130
137
|
replyInSlack,
|
|
@@ -1256,6 +1263,44 @@ async function handleSlackChannels(req: IncomingMessage, res: ServerResponse, ag
|
|
|
1256
1263
|
}
|
|
1257
1264
|
}
|
|
1258
1265
|
|
|
1266
|
+
// Messaging tab → Approvers: who can decide this agent's Slack requests
|
|
1267
|
+
// by replying (core/messaging/), and the people in its channel to pick
|
|
1268
|
+
// from. Changing the list needs the admin login, like saving a secret:
|
|
1269
|
+
// it decides who can approve what the agent does.
|
|
1270
|
+
async function handleApproversPut(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
1271
|
+
const entry = getEntry(agentName)
|
|
1272
|
+
if (!entry) {
|
|
1273
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1274
|
+
return
|
|
1275
|
+
}
|
|
1276
|
+
if (!secretsProtected) {
|
|
1277
|
+
res.writeHead(403, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'Changing approvers needs LOOPENGINE_ADMIN_AUTH set on the server.' }))
|
|
1278
|
+
return
|
|
1279
|
+
}
|
|
1280
|
+
const body = await readJsonBody(req)
|
|
1281
|
+
try {
|
|
1282
|
+
const approvers = setAgentApprovers(entry.config, body.approvers)
|
|
1283
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ approvers }))
|
|
1284
|
+
} catch (err) {
|
|
1285
|
+
const status = err instanceof NotifierFileError ? 400 : 500
|
|
1286
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async function handleSlackMembers(res: ServerResponse, agentName: string): Promise<void> {
|
|
1291
|
+
const entry = getEntry(agentName)
|
|
1292
|
+
if (!entry) {
|
|
1293
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1294
|
+
return
|
|
1295
|
+
}
|
|
1296
|
+
try {
|
|
1297
|
+
const members = await listAgentSlackMembers(entry.config)
|
|
1298
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ members }))
|
|
1299
|
+
} catch (err) {
|
|
1300
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1259
1304
|
// Every approval/question still waiting for this agent — for an app that
|
|
1260
1305
|
// can't receive webhooks (it polls this instead) and for the console.
|
|
1261
1306
|
async function handlePendingGet(res: ServerResponse, agentName: string): Promise<void> {
|
|
@@ -2709,6 +2754,17 @@ async function handleSlackInteraction(req: IncomingMessage, res: ServerResponse,
|
|
|
2709
2754
|
return
|
|
2710
2755
|
}
|
|
2711
2756
|
const interaction = parseSlackInteraction(rawBody)
|
|
2757
|
+
// Once the agent has approvers (Messaging tab), only their clicks count.
|
|
2758
|
+
if (interaction.type !== 'ignore') {
|
|
2759
|
+
const gate = slackClickAllowed(entry.config, interaction.user)
|
|
2760
|
+
if (!gate.allowed) {
|
|
2761
|
+
res.writeHead(200).end()
|
|
2762
|
+
if ('responseUrl' in interaction && interaction.responseUrl) {
|
|
2763
|
+
await replyPrivatelyInSlack(interaction.responseUrl, gate.message ?? 'Only approvers can decide this.').catch((err) => console.error('[loopengine] Slack reply failed:', err))
|
|
2764
|
+
}
|
|
2765
|
+
return
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2712
2768
|
if (interaction.type === 'open_answer') {
|
|
2713
2769
|
if (botToken) await openSlackAnswerModal(botToken, interaction.triggerId, interaction.pendingId).catch((err) => console.error('[loopengine] Slack modal failed:', err))
|
|
2714
2770
|
res.writeHead(200).end()
|
|
@@ -2770,9 +2826,26 @@ async function handleSlackEvents(req: IncomingMessage, res: ServerResponse, agen
|
|
|
2770
2826
|
getCheckpoint: (pendingId) => checkpoints.get(pendingId),
|
|
2771
2827
|
getHistory: (checkpoint) => sessions.getHistory(`${checkpoint.tenant}:${environment}:${checkpoint.agent}:${checkpoint.sessionId}`),
|
|
2772
2828
|
plan: process.env.LOOPENGINE_PLAN,
|
|
2829
|
+
approvers: () => agentApprovers(entry.config),
|
|
2830
|
+
decide: decideFromMessaging,
|
|
2773
2831
|
})
|
|
2774
2832
|
}
|
|
2775
2833
|
|
|
2834
|
+
// A decision made by replying in a request's thread (core/messaging/),
|
|
2835
|
+
// recorded through the same resolve and answer handlers as the console's
|
|
2836
|
+
// buttons.
|
|
2837
|
+
async function decideFromMessaging(pendingId: string, decision: MessagingDecision): Promise<DecisionOutcome> {
|
|
2838
|
+
const capture = captureResponse()
|
|
2839
|
+
if (decision.kind === 'answer') await answerPendingQuestion(capture.res, pendingId, { answer: decision.answer })
|
|
2840
|
+
else await resolvePendingApproval(capture.res, pendingId, { decision: decision.kind })
|
|
2841
|
+
const { status, body } = await capture.done
|
|
2842
|
+
if (status >= 400) return { error: typeof body.error === 'string' ? body.error : `HTTP ${status}` }
|
|
2843
|
+
if (body.alreadyResolved) return { alreadyResolved: true }
|
|
2844
|
+
if (body.resolved) return { stillWaiting: true }
|
|
2845
|
+
if (Array.isArray(body.pending) && body.pending.length) return { morePending: true }
|
|
2846
|
+
return { text: typeof body.text === 'string' ? body.text : undefined }
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2776
2849
|
// Set once shutdown() starts, so /healthz turns 503 while the server
|
|
2777
2850
|
// drains and a router or load balancer stops sending it new requests.
|
|
2778
2851
|
let shuttingDown = false
|
|
@@ -3098,7 +3171,11 @@ const server = createServer(async (req, res) => {
|
|
|
3098
3171
|
// The Settings section's Upgrade button (web/upgrade-admin.ts).
|
|
3099
3172
|
if (req.method === 'GET' && pathname === '/config/upgrade') {
|
|
3100
3173
|
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(
|
|
3101
|
-
JSON.stringify({
|
|
3174
|
+
JSON.stringify({
|
|
3175
|
+
...(await describeUpgrade(undefined, { refresh: new URL(req.url ?? '/', 'http://localhost').searchParams.get('refresh') === '1' })),
|
|
3176
|
+
canUpgrade: secretsProtected,
|
|
3177
|
+
restartsItself: exitOnCodeChange,
|
|
3178
|
+
}),
|
|
3102
3179
|
)
|
|
3103
3180
|
return
|
|
3104
3181
|
}
|
|
@@ -3323,6 +3400,16 @@ const server = createServer(async (req, res) => {
|
|
|
3323
3400
|
return
|
|
3324
3401
|
}
|
|
3325
3402
|
|
|
3403
|
+
const approversMatch = req.method === 'PUT' && pathname.match(/^\/agents\/([^/]+)\/notifier\/approvers$/)
|
|
3404
|
+
if (approversMatch) {
|
|
3405
|
+
await handleApproversPut(req, res, decodeURIComponent(approversMatch[1]))
|
|
3406
|
+
return
|
|
3407
|
+
}
|
|
3408
|
+
const slackMembersMatch = req.method === 'GET' && pathname.match(/^\/agents\/([^/]+)\/notifier\/slack-members$/)
|
|
3409
|
+
if (slackMembersMatch) {
|
|
3410
|
+
await handleSlackMembers(res, decodeURIComponent(slackMembersMatch[1]))
|
|
3411
|
+
return
|
|
3412
|
+
}
|
|
3326
3413
|
const slackChannelsMatch = req.method === 'POST' && pathname.match(/^\/agents\/([^/]+)\/notifier\/slack-channels$/)
|
|
3327
3414
|
if (slackChannelsMatch) {
|
|
3328
3415
|
await handleSlackChannels(req, res, decodeURIComponent(slackChannelsMatch[1]))
|