@yemi33/minions 0.1.934 → 0.1.935
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/CHANGELOG.md +2 -1
- package/dashboard/js/settings.js +36 -1
- package/dashboard.js +18 -1
- package/docs/teams-setup.md +53 -1
- package/engine/preflight.js +10 -4
- package/engine/shared.js +5 -1
- package/engine/teams.js +47 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.935 (2026-04-14)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- certificate-based auth for Teams integration (#1027)
|
|
6
7
|
- add adoPollEnabled/ghPollEnabled engine settings
|
|
7
8
|
- doc-chat abort kills LLM process + queued messages auto-process
|
|
8
9
|
- add /api/work-items/reopen endpoint and reopen-work-item CC action (#982)
|
package/dashboard/js/settings.js
CHANGED
|
@@ -14,6 +14,7 @@ async function openSettings() {
|
|
|
14
14
|
const e = data.engine || {};
|
|
15
15
|
const c = data.claude || {};
|
|
16
16
|
const agents = data.agents || {};
|
|
17
|
+
const t = data.teams || {};
|
|
17
18
|
|
|
18
19
|
const agentRows = Object.entries(agents).map(function([id, a]) {
|
|
19
20
|
return '<tr>' +
|
|
@@ -96,6 +97,28 @@ async function openSettings() {
|
|
|
96
97
|
'</div>' +
|
|
97
98
|
'</div>' +
|
|
98
99
|
|
|
100
|
+
'<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Teams Integration</h3>' +
|
|
101
|
+
'<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:8px">' +
|
|
102
|
+
settingsToggle('Enable Teams', 'set-teams-enabled', !!t.enabled, 'Connect Minions to Microsoft Teams') +
|
|
103
|
+
'</div>' +
|
|
104
|
+
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">' +
|
|
105
|
+
settingsField('App ID', 'set-teams-appId', t.appId || '', '', 'Microsoft App ID from Azure Bot Configuration') +
|
|
106
|
+
settingsField('App Password', 'set-teams-appPassword', t.appPassword || '', '', 'Client secret (leave blank for certificate auth)') +
|
|
107
|
+
'</div>' +
|
|
108
|
+
'<div style="font-size:10px;color:var(--muted);margin-bottom:4px;font-weight:600">Certificate Auth (alternative to client secret)</div>' +
|
|
109
|
+
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">' +
|
|
110
|
+
settingsField('Certificate Path', 'set-teams-certPath', t.certPath || '', '', 'Path to PEM certificate file') +
|
|
111
|
+
settingsField('Private Key Path', 'set-teams-privateKeyPath', t.privateKeyPath || '', '', 'Path to PEM private key file') +
|
|
112
|
+
settingsField('Tenant ID', 'set-teams-tenantId', t.tenantId || '', '', 'Azure AD tenant ID (required for cert auth)') +
|
|
113
|
+
'</div>' +
|
|
114
|
+
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
|
|
115
|
+
settingsField('Notify Events', 'set-teams-notifyEvents', (t.notifyEvents || []).join(', '), '', 'Comma-separated event types to notify') +
|
|
116
|
+
settingsField('Inbox Poll Interval', 'set-teams-inboxPollInterval', t.inboxPollInterval || 15000, 'ms', 'How often to check for Teams messages') +
|
|
117
|
+
'</div>' +
|
|
118
|
+
'<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:16px">' +
|
|
119
|
+
settingsToggle('CC Mirror', 'set-teams-ccMirror', t.ccMirror !== false, 'Mirror Command Center responses to Teams') +
|
|
120
|
+
'</div>' +
|
|
121
|
+
|
|
99
122
|
'<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Claude CLI</h3>' +
|
|
100
123
|
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
|
|
101
124
|
settingsField('Output Format', 'set-outputFormat', c.outputFormat || 'stream-json', '', '') +
|
|
@@ -236,6 +259,18 @@ async function saveSettings() {
|
|
|
236
259
|
permissionMode: document.getElementById('set-permissionMode').value,
|
|
237
260
|
};
|
|
238
261
|
|
|
262
|
+
const teamsPayload = {
|
|
263
|
+
enabled: document.getElementById('set-teams-enabled').checked,
|
|
264
|
+
appId: document.getElementById('set-teams-appId').value,
|
|
265
|
+
appPassword: document.getElementById('set-teams-appPassword').value,
|
|
266
|
+
certPath: document.getElementById('set-teams-certPath').value,
|
|
267
|
+
privateKeyPath: document.getElementById('set-teams-privateKeyPath').value,
|
|
268
|
+
tenantId: document.getElementById('set-teams-tenantId').value,
|
|
269
|
+
notifyEvents: document.getElementById('set-teams-notifyEvents').value,
|
|
270
|
+
inboxPollInterval: document.getElementById('set-teams-inboxPollInterval').value,
|
|
271
|
+
ccMirror: document.getElementById('set-teams-ccMirror').checked,
|
|
272
|
+
};
|
|
273
|
+
|
|
239
274
|
const agentsPayload = {};
|
|
240
275
|
document.querySelectorAll('[data-agent][data-field]').forEach(function(el) {
|
|
241
276
|
const id = el.dataset.agent;
|
|
@@ -247,7 +282,7 @@ async function saveSettings() {
|
|
|
247
282
|
// Save config
|
|
248
283
|
const res = await fetch('/api/settings', {
|
|
249
284
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
250
|
-
body: JSON.stringify({ engine: enginePayload, claude: claudePayload, agents: agentsPayload })
|
|
285
|
+
body: JSON.stringify({ engine: enginePayload, claude: claudePayload, agents: agentsPayload, teams: teamsPayload })
|
|
251
286
|
});
|
|
252
287
|
const result = await res.json();
|
|
253
288
|
if (!res.ok) throw new Error(result.error);
|
package/dashboard.js
CHANGED
|
@@ -3702,6 +3702,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3702
3702
|
engine: { ...shared.ENGINE_DEFAULTS, ...(config.engine || {}) },
|
|
3703
3703
|
claude: { ...shared.DEFAULT_CLAUDE, ...(config.claude || {}) },
|
|
3704
3704
|
agents: config.agents || {},
|
|
3705
|
+
teams: { ...shared.ENGINE_DEFAULTS.teams, ...(config.teams || {}) },
|
|
3705
3706
|
routing,
|
|
3706
3707
|
});
|
|
3707
3708
|
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
@@ -3795,6 +3796,22 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3795
3796
|
}
|
|
3796
3797
|
}
|
|
3797
3798
|
|
|
3799
|
+
if (body.teams) {
|
|
3800
|
+
if (!config.teams) config.teams = {};
|
|
3801
|
+
const tm = body.teams;
|
|
3802
|
+
if (tm.enabled !== undefined) config.teams.enabled = !!tm.enabled;
|
|
3803
|
+
for (const key of ['appId', 'appPassword', 'certPath', 'privateKeyPath', 'tenantId']) {
|
|
3804
|
+
if (tm[key] !== undefined) config.teams[key] = String(tm[key] || '');
|
|
3805
|
+
}
|
|
3806
|
+
if (tm.notifyEvents !== undefined) {
|
|
3807
|
+
config.teams.notifyEvents = Array.isArray(tm.notifyEvents) ? tm.notifyEvents : String(tm.notifyEvents || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
3808
|
+
}
|
|
3809
|
+
if (tm.inboxPollInterval !== undefined) config.teams.inboxPollInterval = Math.max(5000, Number(tm.inboxPollInterval) || 15000);
|
|
3810
|
+
if (tm.ccMirror !== undefined) config.teams.ccMirror = !!tm.ccMirror;
|
|
3811
|
+
// Invalidate cached adapter so credential changes take effect
|
|
3812
|
+
teams._resetAdapter();
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3798
3815
|
safeWrite(configPath, config);
|
|
3799
3816
|
// Refresh in-memory CONFIG so subsequent reads see the update
|
|
3800
3817
|
reloadConfig();
|
|
@@ -4571,7 +4588,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4571
4588
|
|
|
4572
4589
|
// Settings
|
|
4573
4590
|
{ method: 'GET', path: '/api/settings', desc: 'Return current engine + claude + routing config', handler: handleSettingsRead },
|
|
4574
|
-
{ method: 'POST', path: '/api/settings', desc: 'Update engine + claude + agent config', params: 'engine?, claude?, agents?', handler: handleSettingsUpdate },
|
|
4591
|
+
{ method: 'POST', path: '/api/settings', desc: 'Update engine + claude + agent + teams config', params: 'engine?, claude?, agents?, teams?', handler: handleSettingsUpdate },
|
|
4575
4592
|
{ method: 'POST', path: '/api/settings/routing', desc: 'Update routing.md', params: 'content', handler: handleSettingsRouting },
|
|
4576
4593
|
{ method: 'POST', path: '/api/settings/reset', desc: 'Reset engine + claude + agent settings to defaults', handler: handleSettingsReset },
|
|
4577
4594
|
|
package/docs/teams-setup.md
CHANGED
|
@@ -84,7 +84,10 @@ Add a `teams` section to your `config.json`:
|
|
|
84
84
|
|-------|-------------|
|
|
85
85
|
| `enabled` | Master switch — `true` to activate Teams integration |
|
|
86
86
|
| `appId` | Microsoft App ID from the Azure Bot Configuration page |
|
|
87
|
-
| `appPassword` | Client secret value from Entra ID Certificates & secrets |
|
|
87
|
+
| `appPassword` | Client secret value from Entra ID Certificates & secrets (leave blank for certificate auth) |
|
|
88
|
+
| `certPath` | Path to PEM certificate file (certificate auth only) |
|
|
89
|
+
| `privateKeyPath` | Path to PEM private key file (certificate auth only) |
|
|
90
|
+
| `tenantId` | Azure AD tenant ID (required for certificate auth) |
|
|
88
91
|
| `notifyEvents` | Which events trigger Teams notifications (see below) |
|
|
89
92
|
| `ccMirror` | Mirror CC dashboard responses to Teams (`true`/`false`) |
|
|
90
93
|
| `inboxPollInterval` | How often to check for new Teams messages, in ms (default: 15000) |
|
|
@@ -93,6 +96,55 @@ Add a `teams` section to your `config.json`:
|
|
|
93
96
|
|
|
94
97
|
> **Security note:** `config.json` is gitignored by default and should never be committed. For shared machines, consider setting the app password via an environment variable and reading it in your config setup.
|
|
95
98
|
|
|
99
|
+
### Certificate Auth (Alternative to Client Secret)
|
|
100
|
+
|
|
101
|
+
Some tenants prohibit creating client secrets in Entra ID. Use certificate-based authentication instead — no client secret needed.
|
|
102
|
+
|
|
103
|
+
#### Generate a Self-Signed Certificate
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Generate a private key and self-signed certificate (valid for 1 year)
|
|
107
|
+
openssl req -x509 -newkey rsa:2048 -keyout bot-private-key.pem -out bot-cert.pem -days 365 -nodes -subj "/CN=minions-bot"
|
|
108
|
+
|
|
109
|
+
# Verify the certificate
|
|
110
|
+
openssl x509 -in bot-cert.pem -text -noout
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This creates two files:
|
|
114
|
+
- `bot-cert.pem` — the public certificate (upload to Entra ID)
|
|
115
|
+
- `bot-private-key.pem` — the private key (keep secret, do not commit)
|
|
116
|
+
|
|
117
|
+
#### Upload the Certificate to Entra ID
|
|
118
|
+
|
|
119
|
+
1. In the [Azure Portal](https://portal.azure.com), go to **Entra ID** > **App registrations**.
|
|
120
|
+
2. Find and click the app registration for your bot (same App ID from Step 3).
|
|
121
|
+
3. Click **Certificates & secrets** in the left sidebar.
|
|
122
|
+
4. Click the **Certificates** tab, then **Upload certificate**.
|
|
123
|
+
5. Select your `bot-cert.pem` file and click **Add**.
|
|
124
|
+
|
|
125
|
+
#### Configure Minions for Certificate Auth
|
|
126
|
+
|
|
127
|
+
Use certificate fields instead of `appPassword` in your `config.json`:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"teams": {
|
|
132
|
+
"enabled": true,
|
|
133
|
+
"appId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
134
|
+
"certPath": "/path/to/bot-cert.pem",
|
|
135
|
+
"privateKeyPath": "/path/to/bot-private-key.pem",
|
|
136
|
+
"tenantId": "your-azure-ad-tenant-id",
|
|
137
|
+
"notifyEvents": ["pr-merged", "agent-completed", "plan-completed", "agent-failed"],
|
|
138
|
+
"ccMirror": true,
|
|
139
|
+
"inboxPollInterval": 15000
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
> **Finding your Tenant ID:** In the Azure Portal, go to **Entra ID** > **Overview**. The **Tenant ID** is listed under Basic Information.
|
|
145
|
+
|
|
146
|
+
> When both `appPassword` and cert fields are configured, certificate auth takes precedence.
|
|
147
|
+
|
|
96
148
|
## Step 4: Set the Messaging Endpoint
|
|
97
149
|
|
|
98
150
|
The messaging endpoint is the URL that Azure Bot Framework sends incoming messages to. It must point to your Minions dashboard's `/api/bot` route.
|
package/engine/preflight.js
CHANGED
|
@@ -193,14 +193,20 @@ function doctor(minionsHome) {
|
|
|
193
193
|
runtimeResults.push({ name: 'Agents configured', ok: false, message: 'no agents in config.json' });
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
// Check Teams integration
|
|
196
|
+
// Check Teams integration — supports client secret OR certificate auth
|
|
197
197
|
const teams = config.teams;
|
|
198
198
|
if (teams && teams.enabled === true) {
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
const hasSecret = !!teams.appId && !!teams.appPassword;
|
|
200
|
+
const hasCert = !!teams.appId && !!teams.certPath && !!teams.privateKeyPath && !!teams.tenantId;
|
|
201
|
+
if (!hasSecret && !hasCert) {
|
|
202
|
+
const missing = [
|
|
203
|
+
!teams.appId && 'appId',
|
|
204
|
+
!teams.appPassword && !teams.certPath && 'appPassword or certPath+privateKeyPath+tenantId',
|
|
205
|
+
].filter(Boolean).join(', ');
|
|
201
206
|
runtimeResults.push({ name: 'Teams integration', ok: 'warn', message: `enabled but missing: ${missing}` });
|
|
202
207
|
} else {
|
|
203
|
-
|
|
208
|
+
const authMode = hasCert ? 'certificate' : 'client secret';
|
|
209
|
+
runtimeResults.push({ name: 'Teams integration', ok: true, message: `configured (${authMode})` });
|
|
204
210
|
}
|
|
205
211
|
} else {
|
|
206
212
|
runtimeResults.push({ name: 'Teams integration', ok: 'warn', message: 'disabled — see docs/teams-setup.md' });
|
package/engine/shared.js
CHANGED
|
@@ -556,11 +556,15 @@ const ENGINE_DEFAULTS = {
|
|
|
556
556
|
ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
|
|
557
557
|
heartbeatTimeouts: {}, // populated after WORK_TYPE is defined (below)
|
|
558
558
|
ccMaxTurns: 50, // max tool-use turns for CC/doc-chat before CLI stops
|
|
559
|
-
// Teams integration — config.teams shape: { enabled, appId, appPassword, notifyEvents, ccMirror, inboxPollInterval }
|
|
559
|
+
// Teams integration — config.teams shape: { enabled, appId, appPassword, certPath, privateKeyPath, tenantId, notifyEvents, ccMirror, inboxPollInterval }
|
|
560
|
+
// Auth modes: (1) appId + appPassword (client secret), or (2) appId + certPath + privateKeyPath + tenantId (certificate)
|
|
560
561
|
teams: {
|
|
561
562
|
enabled: false,
|
|
562
563
|
appId: '',
|
|
563
564
|
appPassword: '',
|
|
565
|
+
certPath: '', // PEM certificate file path (certificate auth)
|
|
566
|
+
privateKeyPath: '', // PEM private key file path (certificate auth)
|
|
567
|
+
tenantId: '', // Azure AD tenant ID (required for certificate auth)
|
|
564
568
|
notifyEvents: ['pr-merged', 'agent-completed', 'plan-completed', 'agent-failed'],
|
|
565
569
|
ccMirror: true,
|
|
566
570
|
inboxPollInterval: 15000,
|
package/engine/teams.js
CHANGED
|
@@ -8,7 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const shared = require('./shared');
|
|
9
9
|
const queries = require('./queries');
|
|
10
10
|
|
|
11
|
-
const { log, safeJson, mutateJsonFileLocked, ENGINE_DEFAULTS } = shared;
|
|
11
|
+
const { log, safeRead, safeJson, mutateJsonFileLocked, ENGINE_DEFAULTS } = shared;
|
|
12
12
|
const { ENGINE_DIR, getConfig } = queries;
|
|
13
13
|
const cards = require('./teams-cards');
|
|
14
14
|
|
|
@@ -50,10 +50,16 @@ function getTeamsConfig() {
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Returns true if Teams integration is enabled and has required credentials.
|
|
53
|
+
* Supports two auth modes:
|
|
54
|
+
* (1) Client secret: appId + appPassword
|
|
55
|
+
* (2) Certificate: appId + certPath + privateKeyPath + tenantId
|
|
53
56
|
*/
|
|
54
57
|
function isTeamsEnabled() {
|
|
55
58
|
const cfg = getTeamsConfig();
|
|
56
|
-
|
|
59
|
+
if (cfg.enabled !== true || !cfg.appId) return false;
|
|
60
|
+
const hasSecret = !!cfg.appPassword;
|
|
61
|
+
const hasCert = !!cfg.certPath && !!cfg.privateKeyPath && !!cfg.tenantId;
|
|
62
|
+
return hasSecret || hasCert;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
// Cached adapter instance — created once per process
|
|
@@ -62,6 +68,9 @@ let _adapter = null;
|
|
|
62
68
|
/**
|
|
63
69
|
* Create and return a BotFrameworkAdapter instance.
|
|
64
70
|
* Returns null when Teams is disabled or botbuilder is not installed.
|
|
71
|
+
* Supports two auth modes:
|
|
72
|
+
* (1) Client secret: uses ConfigurationBotFrameworkAuthentication with appPassword
|
|
73
|
+
* (2) Certificate: uses CertificateServiceClientCredentialsFactory with PEM cert + key
|
|
65
74
|
*/
|
|
66
75
|
function createAdapter() {
|
|
67
76
|
if (_adapter) return _adapter;
|
|
@@ -75,15 +84,43 @@ function createAdapter() {
|
|
|
75
84
|
if (!botbuilder) return null;
|
|
76
85
|
|
|
77
86
|
const cfg = getTeamsConfig();
|
|
87
|
+
const useCert = !!cfg.certPath && !!cfg.privateKeyPath && !!cfg.tenantId;
|
|
88
|
+
|
|
78
89
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
if (useCert) {
|
|
91
|
+
let connector;
|
|
92
|
+
try {
|
|
93
|
+
connector = require('botframework-connector');
|
|
94
|
+
} catch {
|
|
95
|
+
log('warn', 'botframework-connector not installed — certificate auth unavailable. Install via: npm install botframework-connector');
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const cert = safeRead(cfg.certPath);
|
|
99
|
+
const privateKey = safeRead(cfg.privateKeyPath);
|
|
100
|
+
if (!cert || !privateKey) {
|
|
101
|
+
log('warn', `Teams cert auth failed — could not read cert (${cfg.certPath}) or key (${cfg.privateKeyPath})`);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const credentialsFactory = new connector.CertificateServiceClientCredentialsFactory(
|
|
105
|
+
cfg.appId, cert, privateKey, cfg.tenantId
|
|
106
|
+
);
|
|
107
|
+
_adapter = new botbuilder.CloudAdapter(
|
|
108
|
+
new botbuilder.ConfigurationBotFrameworkAuthentication(
|
|
109
|
+
{ MicrosoftAppId: cfg.appId, MicrosoftAppType: 'SingleTenant', MicrosoftAppTenantId: cfg.tenantId },
|
|
110
|
+
credentialsFactory
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
log('info', 'Teams adapter created (certificate auth)');
|
|
114
|
+
} else {
|
|
115
|
+
_adapter = new botbuilder.CloudAdapter(
|
|
116
|
+
new botbuilder.ConfigurationBotFrameworkAuthentication({
|
|
117
|
+
MicrosoftAppId: cfg.appId,
|
|
118
|
+
MicrosoftAppPassword: cfg.appPassword,
|
|
119
|
+
MicrosoftAppType: 'SingleTenant',
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
log('info', 'Teams adapter created (client secret auth)');
|
|
123
|
+
}
|
|
87
124
|
return _adapter;
|
|
88
125
|
} catch (err) {
|
|
89
126
|
log('warn', `Teams adapter creation failed: ${err.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.935",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|