@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 CHANGED
@@ -1,8 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.934 (2026-04-14)
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)
@@ -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
 
@@ -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.
@@ -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
- if (!teams.appId || !teams.appPassword) {
200
- const missing = [!teams.appId && 'appId', !teams.appPassword && 'appPassword'].filter(Boolean).join(', ');
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
- runtimeResults.push({ name: 'Teams integration', ok: true, message: 'configured' });
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
- return cfg.enabled === true && !!cfg.appId && !!cfg.appPassword;
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
- _adapter = new botbuilder.CloudAdapter(
80
- new botbuilder.ConfigurationBotFrameworkAuthentication({
81
- MicrosoftAppId: cfg.appId,
82
- MicrosoftAppPassword: cfg.appPassword,
83
- MicrosoftAppType: 'SingleTenant',
84
- })
85
- );
86
- log('info', 'Teams adapter created successfully');
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.934",
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"