create-loopengine 0.1.67 → 0.1.68
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 +78 -9
- 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) {
|
|
@@ -2773,23 +2799,39 @@ async function handleSlackInteraction(req: IncomingMessage, res: ServerResponse,
|
|
|
2773
2799
|
res.writeHead(200).end()
|
|
2774
2800
|
if (interaction.type === 'ignore') return
|
|
2775
2801
|
|
|
2802
|
+
// A button under one of the agent's own thread replies: Approve as
|
|
2803
|
+
// changed, Keep original, Confirm, Cancel (core/messaging/engine.ts).
|
|
2804
|
+
if (interaction.type === 'action') {
|
|
2805
|
+
if (!botToken) return
|
|
2806
|
+
const text = await handleMessagingAction(interaction.action, interaction.pendingId, interaction.user ?? '', slackMessagingDeps(agentName, entry, botToken))
|
|
2807
|
+
if (interaction.responseUrl) await replyInSlack(interaction.responseUrl, text).catch((err) => console.error('[loopengine] Slack reply failed:', err))
|
|
2808
|
+
return
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// Approve reports what ran, so the final arguments are in the channel.
|
|
2812
|
+
let ran = ''
|
|
2813
|
+
if (interaction.type === 'decide' && interaction.decision === 'approve') {
|
|
2814
|
+
const item = (await checkpoints.get(interaction.pendingId))?.outstanding[interaction.pendingId]
|
|
2815
|
+
if (item) ran = `\nRan \`${item.tool}\` with \`${JSON.stringify(item.args)}\`.`
|
|
2816
|
+
}
|
|
2776
2817
|
const capture = captureResponse()
|
|
2777
2818
|
if (interaction.type === 'decide') await resolvePendingApproval(capture.res, interaction.pendingId, { decision: interaction.decision })
|
|
2778
2819
|
else await answerPendingQuestion(capture.res, interaction.pendingId, { answer: interaction.answer })
|
|
2779
2820
|
const outcome = await capture.done
|
|
2780
2821
|
if (interaction.responseUrl) {
|
|
2781
|
-
|
|
2822
|
+
let text = slackOutcomeText(interaction.type === 'decide' ? interaction.decision : 'answer', outcome.status, outcome.body, interaction.user)
|
|
2823
|
+
if (ran && outcome.status < 400 && !outcome.body.alreadyResolved) text += ran
|
|
2782
2824
|
await replyInSlack(interaction.responseUrl, text).catch((err) => console.error('[loopengine] Slack reply failed:', err))
|
|
2783
2825
|
}
|
|
2784
2826
|
}
|
|
2785
2827
|
|
|
2786
2828
|
// A Slack app's Event Subscriptions Request URL (core/messaging/):
|
|
2787
2829
|
// 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.
|
|
2830
|
+
// the agent answers about that request, and which an approver can use to
|
|
2831
|
+
// decide or change it. Outside the admin login like the button endpoint
|
|
2832
|
+
// above, and verified with the same signing secret. Slack's URL check is
|
|
2833
|
+
// answered before that, since the app is created before its signing
|
|
2834
|
+
// secret can be saved here; it only echoes Slack's own challenge back.
|
|
2793
2835
|
async function handleSlackEvents(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
2794
2836
|
const rawBody = await readRawBody(req)
|
|
2795
2837
|
const entry = getEntry(agentName)
|
|
@@ -2816,8 +2858,14 @@ async function handleSlackEvents(req: IncomingMessage, res: ServerResponse, agen
|
|
|
2816
2858
|
// Slack wants an answer within three seconds; the reply is posted after.
|
|
2817
2859
|
res.writeHead(200).end()
|
|
2818
2860
|
if (event.type !== 'message' || !firstSighting(event) || !slackConversationsOn(entry.config)) return
|
|
2861
|
+
await handleInboundMessage(event, slackMessagingDeps(agentName, entry, botToken))
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
// What core/messaging/ needs from this server to answer, decide and
|
|
2865
|
+
// change an agent's Slack requests.
|
|
2866
|
+
function slackMessagingDeps(agentName: string, entry: RegistryEntry, botToken: string): MessagingDeps {
|
|
2819
2867
|
const environment = process.env.LOOPENGINE_ENV ?? 'production'
|
|
2820
|
-
|
|
2868
|
+
return {
|
|
2821
2869
|
agentName,
|
|
2822
2870
|
agentDir: agentDirFor(entry.config),
|
|
2823
2871
|
systemPrompt: entry.config.systemPrompt,
|
|
@@ -2827,8 +2875,24 @@ async function handleSlackEvents(req: IncomingMessage, res: ServerResponse, agen
|
|
|
2827
2875
|
getHistory: (checkpoint) => sessions.getHistory(`${checkpoint.tenant}:${environment}:${checkpoint.agent}:${checkpoint.sessionId}`),
|
|
2828
2876
|
plan: process.env.LOOPENGINE_PLAN,
|
|
2829
2877
|
approvers: () => agentApprovers(entry.config),
|
|
2878
|
+
confirmTools: () => agentConfirmTools(entry.config),
|
|
2830
2879
|
decide: decideFromMessaging,
|
|
2831
|
-
|
|
2880
|
+
toolSchema: async (toolName) => (await resolveAgentTool(entry.config, toolName))?.input_schema,
|
|
2881
|
+
// The same checks the console's approve-with-edits applies, plus the
|
|
2882
|
+
// permission rules: changed arguments a rule denies are refused.
|
|
2883
|
+
checkArgs: async (pendingId, args) => {
|
|
2884
|
+
const checkpoint = await checkpoints.get(pendingId)
|
|
2885
|
+
const item = checkpoint?.outstanding[pendingId]
|
|
2886
|
+
if (!checkpoint || !item) return 'the request was already decided'
|
|
2887
|
+
const tool = await resolveAgentTool(entry.config, item.tool)
|
|
2888
|
+
if (!tool) return `the tool '${item.tool}' no longer exists`
|
|
2889
|
+
const invalid = validateEditedArgs(args, tool.input_schema)
|
|
2890
|
+
if (invalid) return `the arguments ${invalid}`
|
|
2891
|
+
const [decision, rule] = loadRules(entry.config).resolve(item.tool, args, { tenant: checkpoint.tenant, environment, agent: checkpoint.agent })
|
|
2892
|
+
if (decision === 'deny') return `a permission rule${rule?.name ? ` (${rule.name})` : ''} doesn't allow it`
|
|
2893
|
+
return undefined
|
|
2894
|
+
},
|
|
2895
|
+
}
|
|
2832
2896
|
}
|
|
2833
2897
|
|
|
2834
2898
|
// A decision made by replying in a request's thread (core/messaging/),
|
|
@@ -2837,7 +2901,7 @@ async function handleSlackEvents(req: IncomingMessage, res: ServerResponse, agen
|
|
|
2837
2901
|
async function decideFromMessaging(pendingId: string, decision: MessagingDecision): Promise<DecisionOutcome> {
|
|
2838
2902
|
const capture = captureResponse()
|
|
2839
2903
|
if (decision.kind === 'answer') await answerPendingQuestion(capture.res, pendingId, { answer: decision.answer })
|
|
2840
|
-
else await resolvePendingApproval(capture.res, pendingId, { decision: decision.kind })
|
|
2904
|
+
else await resolvePendingApproval(capture.res, pendingId, { decision: decision.kind, ...(decision.kind === 'approve' && decision.editedArgs ? { editedArgs: decision.editedArgs } : {}) })
|
|
2841
2905
|
const { status, body } = await capture.done
|
|
2842
2906
|
if (status >= 400) return { error: typeof body.error === 'string' ? body.error : `HTTP ${status}` }
|
|
2843
2907
|
if (body.alreadyResolved) return { alreadyResolved: true }
|
|
@@ -3405,6 +3469,11 @@ const server = createServer(async (req, res) => {
|
|
|
3405
3469
|
await handleApproversPut(req, res, decodeURIComponent(approversMatch[1]))
|
|
3406
3470
|
return
|
|
3407
3471
|
}
|
|
3472
|
+
const confirmToolsMatch = req.method === 'PUT' && pathname.match(/^\/agents\/([^/]+)\/notifier\/confirm-tools$/)
|
|
3473
|
+
if (confirmToolsMatch) {
|
|
3474
|
+
await handleConfirmToolsPut(req, res, decodeURIComponent(confirmToolsMatch[1]))
|
|
3475
|
+
return
|
|
3476
|
+
}
|
|
3408
3477
|
const slackMembersMatch = req.method === 'GET' && pathname.match(/^\/agents\/([^/]+)\/notifier\/slack-members$/)
|
|
3409
3478
|
if (slackMembersMatch) {
|
|
3410
3479
|
await handleSlackMembers(res, decodeURIComponent(slackMembersMatch[1]))
|