maiass 5.15.3 → 5.15.6

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.
@@ -6,7 +6,7 @@ import { SYMBOLS } from './symbols.js';
6
6
  import { loadEnvironmentConfig } from './config.js';
7
7
  import { generateMachineFingerprint } from './machine-fingerprint.js';
8
8
  import { retrieveSecureVariable, storeSecureVariable, removeSecureVariable } from './secure-storage.js';
9
- import { getClientName, getClientVersion } from './client-info.js';
9
+ import { getClientName, getClientVersion, isAIModeOff, isCI, ciHeaders } from './client-info.js';
10
10
  import { getSingleCharInput } from './input-utils.js';
11
11
  import fs from 'fs';
12
12
  import path from 'path';
@@ -40,8 +40,24 @@ function maskToken(token) {
40
40
  */
41
41
  async function createAnonymousSubscriptionIfNeeded() {
42
42
  const debugMode = process.env.MAIASS_DEBUG === 'true';
43
-
43
+
44
44
  try {
45
+ // MAI-98 defense-in-depth: don't mint a throwaway anonymous sub when AI is
46
+ // off, or on an ephemeral CI runner with no existing token (fresh
47
+ // fingerprint per run drains the global free-credit pool).
48
+ if (isAIModeOff()) {
49
+ if (debugMode) {
50
+ log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] AI mode is off (MAIASS_AI_MODE=${process.env.MAIASS_AI_MODE}); skipping anonymous subscription`);
51
+ }
52
+ return null;
53
+ }
54
+ if (isCI()) {
55
+ if (debugMode) {
56
+ log.debug(SYMBOLS.INFO, '[MAIASS DEBUG] CI environment detected with no existing token; skipping anonymous subscription (MAI-98)');
57
+ }
58
+ return null;
59
+ }
60
+
45
61
  log.info(SYMBOLS.INFO, 'Generating machine fingerprint...');
46
62
  const machineFingerprint = generateMachineFingerprint();
47
63
 
@@ -58,7 +74,8 @@ async function createAnonymousSubscriptionIfNeeded() {
58
74
  headers: {
59
75
  'Content-Type': 'application/json',
60
76
  'X-Client-Name': getClientName(),
61
- 'X-Client-Version': getClientVersion()
77
+ 'X-Client-Version': getClientVersion(),
78
+ ...ciHeaders()
62
79
  },
63
80
  body: JSON.stringify({
64
81
  machine_fingerprint: machineFingerprint
@@ -334,8 +351,7 @@ export async function handleAccountInfoCommand(options = {}) {
334
351
 
335
352
  // If still no API key and AI mode is not off, create anonymous subscription
336
353
  if (!apiKey || apiKey === 'DISABLED') {
337
- const aiMode = process.env.MAIASS_AI_MODE || 'ask';
338
- if (aiMode !== 'off') {
354
+ if (!isAIModeOff()) {
339
355
  if (debugMode) {
340
356
  log.debug(SYMBOLS.INFO, '[MAIASS DEBUG] No AI token found, creating anonymous subscription...');
341
357
  }
@@ -29,7 +29,7 @@ import { SYMBOLS } from './symbols.js';
29
29
  import colors from './colors.js';
30
30
  import { createAnonymousSubscriptionIfNeeded } from './commit.js';
31
31
  import { generateMachineFingerprint } from './machine-fingerprint.js';
32
- import { getClientName, getClientVersion } from './client-info.js';
32
+ import { getClientName, getClientVersion, isAIModeOff, ciHeaders } from './client-info.js';
33
33
  import { getSingleCharInput } from './input-utils.js';
34
34
 
35
35
  export const FLAGS = ['--cleanup-changelogs'];
@@ -48,8 +48,10 @@ const MAX_OUTPUT_TOKENS = 2000;
48
48
  * anything other than 'off' as on; we mirror that here.
49
49
  */
50
50
  function isAIModeActive() {
51
- const mode = String(process.env.MAIASS_AI_MODE || 'ask').toLowerCase();
52
- return mode !== 'off' && mode !== 'false' && mode !== 'disabled';
51
+ // MAI-98: delegate to the shared parser so "off"-ish spellings (off/false/no/
52
+ // 0/disabled, plus the YAML-boolean "false" produced by an unquoted GH Actions
53
+ // `MAIASS_AI_MODE: off`) are all treated as off consistently across the CLI.
54
+ return !isAIModeOff();
53
55
  }
54
56
 
55
57
  function executeGitCommand(command) {
@@ -548,7 +550,8 @@ async function callCleanupAI(systemContent, userContent) {
548
550
  'X-Machine-Fingerprint': generateMachineFingerprint(),
549
551
  'X-Client-Name': getClientName(),
550
552
  'X-Client-Version': getClientVersion(),
551
- 'X-Subscription-ID': process.env.MAIASS_SUBSCRIPTION_ID || ''
553
+ 'X-Subscription-ID': process.env.MAIASS_SUBSCRIPTION_ID || '',
554
+ ...ciHeaders()
552
555
  },
553
556
  body: JSON.stringify(requestBody),
554
557
  signal: controller.signal
@@ -58,3 +58,56 @@ export function getClientInfo() {
58
58
  version: getClientVersion()
59
59
  };
60
60
  }
61
+
62
+ /**
63
+ * Robustly decide whether AI is effectively OFF based on MAIASS_AI_MODE.
64
+ *
65
+ * MAI-98: the GitHub Actions version-bump workflow set `MAIASS_AI_MODE: off`
66
+ * (an unquoted YAML boolean) which GitHub stringifies to "false". We also need
67
+ * to treat the various "off"-ish spellings consistently everywhere so no AI
68
+ * path ever fires when the operator meant to disable it. A null/undefined/empty
69
+ * value is NOT off (default behaviour is the interactive 'ask' mode).
70
+ *
71
+ * @param {string|undefined} [mode] - raw MAIASS_AI_MODE value (defaults to env)
72
+ * @returns {boolean} true when AI should be considered disabled
73
+ */
74
+ export function isAIModeOff(mode = process.env.MAIASS_AI_MODE) {
75
+ if (mode === undefined || mode === null) return false;
76
+ const normalised = String(mode).trim().toLowerCase();
77
+ if (normalised === '') return false;
78
+ return ['off', 'false', 'no', '0', 'disabled'].includes(normalised);
79
+ }
80
+
81
+ /**
82
+ * Detect whether we are running inside a CI / automation environment.
83
+ *
84
+ * MAI-98: ephemeral CI runners produce a fresh machine fingerprint on every
85
+ * run, so any anonymous-subscription mint there creates a brand new sub +
86
+ * free-credit grant that is never reused — draining the global free pool.
87
+ * When in CI with no pre-existing token we must never mint an anonymous sub.
88
+ *
89
+ * @returns {boolean} true when a known CI environment is detected
90
+ */
91
+ export function isCI() {
92
+ return (
93
+ process.env.CI === 'true' ||
94
+ !!process.env.GITHUB_ACTIONS ||
95
+ !!process.env.GITLAB_CI ||
96
+ !!process.env.CIRCLECI ||
97
+ !!process.env.BUILDKITE ||
98
+ !!process.env.TF_BUILD
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Headers to attach to MAIASS proxy / token requests so the server can
104
+ * independently detect CI and apply its own anti-abuse handling.
105
+ *
106
+ * MAI-98 SHARED CONTRACT with worker-api-specialist: the header name is
107
+ * `X-MAIASS-CI` and is only sent when {@link isCI} is true.
108
+ *
109
+ * @returns {Object} header object ({} when not in CI)
110
+ */
111
+ export function ciHeaders() {
112
+ return isCI() ? { 'X-MAIASS-CI': 'true' } : {};
113
+ }
package/lib/commit.js CHANGED
@@ -10,7 +10,7 @@ import readline from 'readline';
10
10
  import { loadEnvironmentConfig } from './config.js';
11
11
  import { generateMachineFingerprint } from './machine-fingerprint.js';
12
12
  import { storeSecureVariable, retrieveSecureVariable } from './secure-storage.js';
13
- import { getClientName, getClientVersion } from './client-info.js';
13
+ import { getClientName, getClientVersion, isAIModeOff, isCI, ciHeaders } from './client-info.js';
14
14
  import { getSingleCharInput, getMultiLineInput } from './input-utils.js';
15
15
  import { logCommit } from './devlog.js';
16
16
  import colors from './colors.js';
@@ -206,6 +206,25 @@ async function createAnonymousSubscriptionIfNeeded() {
206
206
  return existingToken;
207
207
  }
208
208
 
209
+ // MAI-98 defense-in-depth: never mint an anonymous subscription when AI is
210
+ // effectively off, or when running on an ephemeral CI runner with no
211
+ // pre-existing token. Both conditions otherwise create a fresh anonymous
212
+ // sub + free-credit grant on every run (CI fingerprints are non-reusable),
213
+ // draining the global free-credit pool. The caller MUST treat a null return
214
+ // as "no AI" and proceed without it — the version bump must never block.
215
+ if (isAIModeOff()) {
216
+ if (debugMode) {
217
+ log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] AI mode is off (MAIASS_AI_MODE=${process.env.MAIASS_AI_MODE}); skipping anonymous subscription`);
218
+ }
219
+ return null;
220
+ }
221
+ if (isCI()) {
222
+ if (debugMode) {
223
+ log.debug(SYMBOLS.INFO, '[MAIASS DEBUG] CI environment detected with no existing token; skipping anonymous subscription to avoid pool drain (MAI-98)');
224
+ }
225
+ return null;
226
+ }
227
+
209
228
  log.info(SYMBOLS.INFO, 'Generating machine fingerprint...');
210
229
  const machineFingerprint = generateMachineFingerprint();
211
230
 
@@ -223,7 +242,8 @@ async function createAnonymousSubscriptionIfNeeded() {
223
242
  headers: {
224
243
  'Content-Type': 'application/json',
225
244
  'X-Client-Name': getClientName(),
226
- 'X-Client-Version': getClientVersion()
245
+ 'X-Client-Version': getClientVersion(),
246
+ ...ciHeaders()
227
247
  },
228
248
  body: JSON.stringify({
229
249
  machine_fingerprint: machineFingerprint
@@ -513,7 +533,8 @@ ${gitDiff}`;
513
533
  'X-Machine-Fingerprint': generateMachineFingerprint(),
514
534
  'X-Client-Name': getClientName(),
515
535
  'X-Client-Version': getClientVersion(),
516
- 'X-Subscription-ID': process.env.MAIASS_SUBSCRIPTION_ID || ''
536
+ 'X-Subscription-ID': process.env.MAIASS_SUBSCRIPTION_ID || '',
537
+ ...ciHeaders()
517
538
  },
518
539
  body: JSON.stringify(requestBody),
519
540
  signal: controller.signal
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "maiass",
3
3
  "type": "module",
4
- "version": "5.15.3",
4
+ "version": "5.15.6",
5
5
  "description": "AI commit messages, version bumps, and changelogs from one command. Stages, commits, merges, tags. Anonymous on first run — no email, no card.",
6
6
  "main": "maiass.mjs",
7
7
  "bin": {
@@ -72,4 +72,9 @@ jobs:
72
72
  - name: Bump version
73
73
  run: maiass -a patch
74
74
  env:
75
- MAIASS_AI_MODE: off # Disable AI no credits used in CI
75
+ # Quoted: unquoted `off` is a YAML boolean that GitHub stringifies to
76
+ # "false", and a repo's tracked .env.maiass (loaded with override) can
77
+ # still flip AI back on — both previously minted a throwaway anonymous
78
+ # subscription on every CI run. The CLI now also self-guards via CI
79
+ # detection (see MAI-98), but keep this explicit and correct.
80
+ MAIASS_AI_MODE: "off" # Disable AI — no credits used in CI