create-loopengine 0.1.67 → 0.1.69
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 +84 -10
- package/template/package.json +1 -1
package/package.json
CHANGED
|
@@ -132,6 +132,10 @@ import {
|
|
|
132
132
|
replyPrivatelyInSlack,
|
|
133
133
|
type MessagingDecision,
|
|
134
134
|
type DecisionOutcome,
|
|
135
|
+
type MessagingDeps,
|
|
136
|
+
handleMessagingAction,
|
|
137
|
+
agentConfirmTools,
|
|
138
|
+
setAgentConfirmTools,
|
|
135
139
|
openSlackAnswerModal,
|
|
136
140
|
parseSlackInteraction,
|
|
137
141
|
replyInSlack,
|
|
@@ -1287,6 +1291,28 @@ async function handleApproversPut(req: IncomingMessage, res: ServerResponse, age
|
|
|
1287
1291
|
}
|
|
1288
1292
|
}
|
|
1289
1293
|
|
|
1294
|
+
// Messaging tab → Confirm-required tools: tools whose approval by reply
|
|
1295
|
+
// needs a Confirm click. Admin login needed, like the approvers.
|
|
1296
|
+
async function handleConfirmToolsPut(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
1297
|
+
const entry = getEntry(agentName)
|
|
1298
|
+
if (!entry) {
|
|
1299
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1300
|
+
return
|
|
1301
|
+
}
|
|
1302
|
+
if (!secretsProtected) {
|
|
1303
|
+
res.writeHead(403, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'Changing Confirm-required tools needs LOOPENGINE_ADMIN_AUTH set on the server.' }))
|
|
1304
|
+
return
|
|
1305
|
+
}
|
|
1306
|
+
const body = await readJsonBody(req)
|
|
1307
|
+
try {
|
|
1308
|
+
const tools = setAgentConfirmTools(entry.config, body.tools)
|
|
1309
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ tools }))
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
const status = err instanceof NotifierFileError ? 400 : 500
|
|
1312
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1290
1316
|
async function handleSlackMembers(res: ServerResponse, agentName: string): Promise<void> {
|
|
1291
1317
|
const entry = getEntry(agentName)
|
|
1292
1318
|
if (!entry) {
|
|
@@ -2290,7 +2316,12 @@ async function resolvePendingApproval(res: ServerResponse, pendingId: string, bo
|
|
|
2290
2316
|
sessionId: checkpoint.sessionId,
|
|
2291
2317
|
env: createAgentEnv(entry.config.name, agentDirFor(entry.config)),
|
|
2292
2318
|
})
|
|
2293
|
-
|
|
2319
|
+
// The agent's own history still shows the arguments it asked
|
|
2320
|
+
// for, so say when the approver changed them; otherwise its
|
|
2321
|
+
// reply describes what it asked for, not what ran.
|
|
2322
|
+
const changed = editedArgs && JSON.stringify(editedArgs) !== JSON.stringify(item.args)
|
|
2323
|
+
const content = changed ? `The approver changed the arguments before it ran: ${JSON.stringify(editedArgs)}. Result: ${JSON.stringify(output)}` : JSON.stringify(output)
|
|
2324
|
+
resultBlock = { type: 'tool_result', tool_use_id: item.toolUseId, content, is_error: false }
|
|
2294
2325
|
} catch (err) {
|
|
2295
2326
|
resultBlock = { type: 'tool_result', tool_use_id: item.toolUseId, content: `ERROR: ${err}`, is_error: true }
|
|
2296
2327
|
}
|
|
@@ -2773,23 +2804,39 @@ async function handleSlackInteraction(req: IncomingMessage, res: ServerResponse,
|
|
|
2773
2804
|
res.writeHead(200).end()
|
|
2774
2805
|
if (interaction.type === 'ignore') return
|
|
2775
2806
|
|
|
2807
|
+
// A button under one of the agent's own thread replies: Approve as
|
|
2808
|
+
// changed, Keep original, Confirm, Cancel (core/messaging/engine.ts).
|
|
2809
|
+
if (interaction.type === 'action') {
|
|
2810
|
+
if (!botToken) return
|
|
2811
|
+
const text = await handleMessagingAction(interaction.action, interaction.pendingId, interaction.user ?? '', slackMessagingDeps(agentName, entry, botToken))
|
|
2812
|
+
if (interaction.responseUrl) await replyInSlack(interaction.responseUrl, text).catch((err) => console.error('[loopengine] Slack reply failed:', err))
|
|
2813
|
+
return
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
// Approve reports what ran, so the final arguments are in the channel.
|
|
2817
|
+
let ran = ''
|
|
2818
|
+
if (interaction.type === 'decide' && interaction.decision === 'approve') {
|
|
2819
|
+
const item = (await checkpoints.get(interaction.pendingId))?.outstanding[interaction.pendingId]
|
|
2820
|
+
if (item) ran = `\nRan \`${item.tool}\` with \`${JSON.stringify(item.args)}\`.`
|
|
2821
|
+
}
|
|
2776
2822
|
const capture = captureResponse()
|
|
2777
2823
|
if (interaction.type === 'decide') await resolvePendingApproval(capture.res, interaction.pendingId, { decision: interaction.decision })
|
|
2778
2824
|
else await answerPendingQuestion(capture.res, interaction.pendingId, { answer: interaction.answer })
|
|
2779
2825
|
const outcome = await capture.done
|
|
2780
2826
|
if (interaction.responseUrl) {
|
|
2781
|
-
|
|
2827
|
+
let text = slackOutcomeText(interaction.type === 'decide' ? interaction.decision : 'answer', outcome.status, outcome.body, interaction.user)
|
|
2828
|
+
if (ran && outcome.status < 400 && !outcome.body.alreadyResolved) text += ran
|
|
2782
2829
|
await replyInSlack(interaction.responseUrl, text).catch((err) => console.error('[loopengine] Slack reply failed:', err))
|
|
2783
2830
|
}
|
|
2784
2831
|
}
|
|
2785
2832
|
|
|
2786
2833
|
// A Slack app's Event Subscriptions Request URL (core/messaging/):
|
|
2787
2834
|
// a reply in the thread under an approval request or a question, which
|
|
2788
|
-
// the agent answers about that request
|
|
2789
|
-
//
|
|
2790
|
-
//
|
|
2791
|
-
// before
|
|
2792
|
-
// own challenge back.
|
|
2835
|
+
// the agent answers about that request, and which an approver can use to
|
|
2836
|
+
// decide or change it. Outside the admin login like the button endpoint
|
|
2837
|
+
// above, and verified with the same signing secret. Slack's URL check is
|
|
2838
|
+
// answered before that, since the app is created before its signing
|
|
2839
|
+
// secret can be saved here; it only echoes Slack's own challenge back.
|
|
2793
2840
|
async function handleSlackEvents(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
2794
2841
|
const rawBody = await readRawBody(req)
|
|
2795
2842
|
const entry = getEntry(agentName)
|
|
@@ -2816,8 +2863,14 @@ async function handleSlackEvents(req: IncomingMessage, res: ServerResponse, agen
|
|
|
2816
2863
|
// Slack wants an answer within three seconds; the reply is posted after.
|
|
2817
2864
|
res.writeHead(200).end()
|
|
2818
2865
|
if (event.type !== 'message' || !firstSighting(event) || !slackConversationsOn(entry.config)) return
|
|
2866
|
+
await handleInboundMessage(event, slackMessagingDeps(agentName, entry, botToken))
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
// What core/messaging/ needs from this server to answer, decide and
|
|
2870
|
+
// change an agent's Slack requests.
|
|
2871
|
+
function slackMessagingDeps(agentName: string, entry: RegistryEntry, botToken: string): MessagingDeps {
|
|
2819
2872
|
const environment = process.env.LOOPENGINE_ENV ?? 'production'
|
|
2820
|
-
|
|
2873
|
+
return {
|
|
2821
2874
|
agentName,
|
|
2822
2875
|
agentDir: agentDirFor(entry.config),
|
|
2823
2876
|
systemPrompt: entry.config.systemPrompt,
|
|
@@ -2827,8 +2880,24 @@ async function handleSlackEvents(req: IncomingMessage, res: ServerResponse, agen
|
|
|
2827
2880
|
getHistory: (checkpoint) => sessions.getHistory(`${checkpoint.tenant}:${environment}:${checkpoint.agent}:${checkpoint.sessionId}`),
|
|
2828
2881
|
plan: process.env.LOOPENGINE_PLAN,
|
|
2829
2882
|
approvers: () => agentApprovers(entry.config),
|
|
2883
|
+
confirmTools: () => agentConfirmTools(entry.config),
|
|
2830
2884
|
decide: decideFromMessaging,
|
|
2831
|
-
|
|
2885
|
+
toolSchema: async (toolName) => (await resolveAgentTool(entry.config, toolName))?.input_schema,
|
|
2886
|
+
// The same checks the console's approve-with-edits applies, plus the
|
|
2887
|
+
// permission rules: changed arguments a rule denies are refused.
|
|
2888
|
+
checkArgs: async (pendingId, args) => {
|
|
2889
|
+
const checkpoint = await checkpoints.get(pendingId)
|
|
2890
|
+
const item = checkpoint?.outstanding[pendingId]
|
|
2891
|
+
if (!checkpoint || !item) return 'the request was already decided'
|
|
2892
|
+
const tool = await resolveAgentTool(entry.config, item.tool)
|
|
2893
|
+
if (!tool) return `the tool '${item.tool}' no longer exists`
|
|
2894
|
+
const invalid = validateEditedArgs(args, tool.input_schema)
|
|
2895
|
+
if (invalid) return `the arguments ${invalid}`
|
|
2896
|
+
const [decision, rule] = loadRules(entry.config).resolve(item.tool, args, { tenant: checkpoint.tenant, environment, agent: checkpoint.agent })
|
|
2897
|
+
if (decision === 'deny') return `a permission rule${rule?.name ? ` (${rule.name})` : ''} doesn't allow it`
|
|
2898
|
+
return undefined
|
|
2899
|
+
},
|
|
2900
|
+
}
|
|
2832
2901
|
}
|
|
2833
2902
|
|
|
2834
2903
|
// A decision made by replying in a request's thread (core/messaging/),
|
|
@@ -2837,7 +2906,7 @@ async function handleSlackEvents(req: IncomingMessage, res: ServerResponse, agen
|
|
|
2837
2906
|
async function decideFromMessaging(pendingId: string, decision: MessagingDecision): Promise<DecisionOutcome> {
|
|
2838
2907
|
const capture = captureResponse()
|
|
2839
2908
|
if (decision.kind === 'answer') await answerPendingQuestion(capture.res, pendingId, { answer: decision.answer })
|
|
2840
|
-
else await resolvePendingApproval(capture.res, pendingId, { decision: decision.kind })
|
|
2909
|
+
else await resolvePendingApproval(capture.res, pendingId, { decision: decision.kind, ...(decision.kind === 'approve' && decision.editedArgs ? { editedArgs: decision.editedArgs } : {}) })
|
|
2841
2910
|
const { status, body } = await capture.done
|
|
2842
2911
|
if (status >= 400) return { error: typeof body.error === 'string' ? body.error : `HTTP ${status}` }
|
|
2843
2912
|
if (body.alreadyResolved) return { alreadyResolved: true }
|
|
@@ -3405,6 +3474,11 @@ const server = createServer(async (req, res) => {
|
|
|
3405
3474
|
await handleApproversPut(req, res, decodeURIComponent(approversMatch[1]))
|
|
3406
3475
|
return
|
|
3407
3476
|
}
|
|
3477
|
+
const confirmToolsMatch = req.method === 'PUT' && pathname.match(/^\/agents\/([^/]+)\/notifier\/confirm-tools$/)
|
|
3478
|
+
if (confirmToolsMatch) {
|
|
3479
|
+
await handleConfirmToolsPut(req, res, decodeURIComponent(confirmToolsMatch[1]))
|
|
3480
|
+
return
|
|
3481
|
+
}
|
|
3408
3482
|
const slackMembersMatch = req.method === 'GET' && pathname.match(/^\/agents\/([^/]+)\/notifier\/slack-members$/)
|
|
3409
3483
|
if (slackMembersMatch) {
|
|
3410
3484
|
await handleSlackMembers(res, decodeURIComponent(slackMembersMatch[1]))
|