@brunobracaioli/aibrain 0.4.0-beta.6 → 0.4.0-beta.8

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.
@@ -151,7 +151,7 @@ type Register = (
151
151
  ) => void;
152
152
 
153
153
  const MAX_RESULT_BYTES = 256 * 1024;
154
- const CLAUDE_HOOK_VERSION = '0.4.0-beta.1';
154
+ const CLAUDE_HOOK_VERSION = '0.4.0-beta.7';
155
155
  const MAX_DIAGNOSTIC_BYTE_COUNT = 128 * 1024 * 1024;
156
156
  const COOLDOWN_MS = 60_000;
157
157
  const MAX_TRUNCATED_CODE_POINTS = 300;
@@ -463,6 +463,10 @@ function v2WireOptions(options: V2Options): Record<string, unknown> {
463
463
 
464
464
  function safeErrorCategory(error: unknown): string {
465
465
  const record = isRecord(error) ? error : {};
466
+ // A coded error names its own reason; the keyword guess below would read
467
+ // "AI Brain plan does not meet the minimum estimated reduction" as invalid_plan.
468
+ if (COMPACT_ERROR_CODES.includes(record.compactErrorCode as CompactErrorCode))
469
+ return record.compactErrorCode as CompactErrorCode;
466
470
  const code = typeof record.code === 'string' ? record.code.toLowerCase() : '';
467
471
  const name = typeof record.name === 'string' ? record.name.toLowerCase() : '';
468
472
  const message = typeof record.message === 'string' ? record.message.toLowerCase() : '';
@@ -16,7 +16,7 @@ export const MAX_LOG_FILES = 3;
16
16
  const LOCK_WAIT_MS = 250;
17
17
  const LOCK_STALE_MS = 30_000;
18
18
  const ADMISSION_MS = 60 * 60 * 1000;
19
- export const ADAPTER_VERSION = '0.4.0-beta.6';
19
+ export const ADAPTER_VERSION = '0.4.0-beta.8';
20
20
  const MAX_DIAGNOSTIC_COUNT = 10_000_000;
21
21
  const MAX_DIAGNOSTIC_BYTES = 128 * 1024 * 1024;
22
22
  const CHECKPOINT_BACKENDS = new Set(['native_store', 'private_file']);
package/dist/cli.mjs CHANGED
@@ -40,7 +40,7 @@ import { extensionsSummary, hooksCommand, mcpCommand } from './extensions-cli.mj
40
40
  import { sessionServers } from './mcp-servers.mjs';
41
41
  import { anyFormatter } from './guardrails/format.mjs';
42
42
 
43
- const VERSION = '0.4.0-beta.6';
43
+ const VERSION = '0.4.0-beta.8';
44
44
  // The command is `aisac`; `aibrain` is an alias installed from the same entry.
45
45
  const COMMAND = 'aisac';
46
46
  const ENDPOINT = 'https://mcp.ai-brain.aisac.cloud/mcp';
@@ -167,6 +167,10 @@ export function hookIds() {
167
167
  * @property {Readonly<Record<CatalogLocale, string>>} [loginNote] Why and how this login differs.
168
168
  * @property {Readonly<Record<CatalogLocale, string>>} description
169
169
  * @property {Readonly<Record<CatalogLocale, string>>} reaches What the server can reach (FR7).
170
+ * @property {readonly string[]} [blockedTools] Tools that spend money, removed in both
171
+ * clients whatever the user's permission mode is (FR7.2). Exact names.
172
+ * @property {readonly string[]} [blockedToolPrefixes] Name prefixes removed in Claude Code
173
+ * only, through a glob deny rule; Codex documents exact names only (FR7.2).
170
174
  */
171
175
 
172
176
  /** Session name of a catalog server, so it never collides with the user's own. */
@@ -215,6 +219,9 @@ export const MCP_CATALOG = Object.freeze([
215
219
  source: 'https://github.com/supabase-community/supabase-mcp',
216
220
  transport: { type: 'http', url: 'https://mcp.supabase.com/mcp?read_only=true' },
217
221
  login: 'host-oauth',
222
+ // read_only=true should already leave these out; the vendor does not publish
223
+ // what it removes, so they are blocked as a second layer (FR7.2).
224
+ blockedTools: ['create_project', 'confirm_cost', 'create_branch'],
218
225
  description: {
219
226
  en: "Supabase's hosted server, read-only: tables, SQL, logs, advisors and docs.",
220
227
  'pt-BR':
@@ -234,15 +241,19 @@ export const MCP_CATALOG = Object.freeze([
234
241
  source: 'https://vercel.com/docs/agent-resources/vercel-mcp',
235
242
  transport: { type: 'http', url: 'https://mcp.vercel.com' },
236
243
  login: 'host-oauth',
244
+ // buy_domain charges the team after a quote, and its confirm flag is a model
245
+ // argument; a domain transfer may be charged (FR7.1, FR7.2).
246
+ blockedTools: ['buy_domain', 'create_or_transfer_domain'],
247
+ blockedToolPrefixes: ['buy_'],
237
248
  description: {
238
249
  en: "Vercel's hosted server: projects, deployments, logs, domains and environment variables.",
239
250
  'pt-BR':
240
251
  'Servidor hospedado da Vercel: projetos, deploys, logs, domínios e variáveis de ambiente.',
241
252
  },
242
253
  reaches: {
243
- en: 'The teams you grant at login, with the access of your Vercel user: projects, deployments, build and runtime logs, domains and DNS, and environment variables, including decrypted values. It also has write tools (deploy, change settings, DNS records) and no read-only mode, so keep the confirmation of each call on.',
254
+ en: 'The teams you grant at login, with the access of your Vercel user: projects, deployments, build and runtime logs, domains and DNS, and environment variables, including decrypted values. It also has write tools (deployments, environment variables, DNS records, firewall rules, project settings) and no read-only mode, so keep the confirmation of each call on. Buying domains is blocked by AISAC.',
244
255
  'pt-BR':
245
- 'Os times que você liberar no login, com o acesso do seu usuário Vercel: projetos, deploys, logs de build e de execução, domínios e DNS, e variáveis de ambiente, inclusive valores decifrados. Também tem ferramentas de escrita (deploy, configurações, registros DNS) e não tem modo somente leitura, então mantenha a confirmação de cada chamada ligada.',
256
+ 'Os times que você liberar no login, com o acesso do seu usuário Vercel: projetos, deploys, logs de build e de execução, domínios e DNS, e variáveis de ambiente, inclusive valores decifrados. Também tem ferramentas de escrita (deploys, variáveis de ambiente, registros DNS, regras de firewall, configurações de projeto) e não tem modo somente leitura, então mantenha a confirmação de cada chamada ligada. A compra de domínios é bloqueada pela AISAC.',
246
257
  },
247
258
  },
248
259
  {
@@ -273,6 +273,11 @@ export function serverInfo(entry, locale, item) {
273
273
  note: 'Hosted by the vendor: its tools can change without a client release, so keep the confirmation of each call on.',
274
274
  },
275
275
  reaches: entry.reaches[locale],
276
+ // Tools that spend money, removed whatever the permission mode is (FR7.2).
277
+ blockedTools: [...(entry.blockedTools ?? [])],
278
+ ...(entry.blockedToolPrefixes === undefined
279
+ ? {}
280
+ : { blockedToolPrefixes: [...entry.blockedToolPrefixes] }),
276
281
  login,
277
282
  ...(variable === undefined ? {} : { tokenVariable: variable }),
278
283
  ...(entry.loginNote === undefined ? {} : { loginNote: entry.loginNote[locale] }),
@@ -293,6 +298,16 @@ function printInfo(info) {
293
298
  const where = info.transport.type === 'local' ? info.transport.command : info.transport.url;
294
299
  console.log(`Transport: ${info.transport.type}, ${where}. ${info.transport.note}`);
295
300
  console.log(`Reaches: ${info.reaches}`);
301
+ if (info.blockedTools.length > 0) {
302
+ const prefixes = info.blockedToolPrefixes ?? [];
303
+ const claudeOnly =
304
+ prefixes.length === 0
305
+ ? ''
306
+ : ` In Claude Code, also any tool whose name starts with ${prefixes.join(', ')}.`;
307
+ console.log(
308
+ `Blocked by AISAC, because they spend money: ${info.blockedTools.join(', ')}.${claudeOnly} They stay blocked even if you allow every tool or skip permission prompts.`,
309
+ );
310
+ }
296
311
  if (info.loginNote !== undefined) console.log(`Login: ${info.loginNote}`);
297
312
  console.log(`Log in from Claude Code: ${info.login.claude}`);
298
313
  console.log(`Log in from Codex: ${info.login.codex}`);
package/dist/launch.mjs CHANGED
@@ -19,7 +19,13 @@ import {
19
19
  writeLaunchMarker,
20
20
  } from './engineering-pack.mjs';
21
21
  import { prepareGuardrails } from './guardrails-plugin.mjs';
22
- import { CODEX_MCP_ENV, claudeMcpEntries, mcpLaunchLines, sessionServers } from './mcp-servers.mjs';
22
+ import {
23
+ CODEX_MCP_ENV,
24
+ claudeDenyRules,
25
+ claudeMcpEntries,
26
+ mcpLaunchLines,
27
+ sessionServers,
28
+ } from './mcp-servers.mjs';
23
29
  import { enabledServerIds, readPreferences } from './extensions-preferences.mjs';
24
30
  import {
25
31
  CODEX_ENGINEERING_ENV,
@@ -561,6 +567,14 @@ export async function launchClaude(config, root, args, dependencies = {}) {
561
567
  for (const rule of [...userAllow, ...requiredAllow]) {
562
568
  if (!mergedAllow.includes(rule)) mergedAllow.push(rule);
563
569
  }
570
+ // Catalog tools that spend money stay denied whatever the user allows or the
571
+ // permission mode is (spec aisac-engineering-extensions FR7.2).
572
+ const userDeny = Array.isArray(userPermissions.deny) ? userPermissions.deny : [];
573
+ /** @type {unknown[]} */
574
+ const mergedDeny = [];
575
+ for (const rule of [...userDeny, ...claudeDenyRules()]) {
576
+ if (!mergedDeny.includes(rule)) mergedDeny.push(rule);
577
+ }
564
578
  const settings = {
565
579
  ...(launchArgs.settings?.sandbox === undefined ? {} : { sandbox: launchArgs.settings.sandbox }),
566
580
  pluginConfigs: {
@@ -570,6 +584,7 @@ export async function launchClaude(config, root, args, dependencies = {}) {
570
584
  permissions: {
571
585
  ...userPermissions,
572
586
  allow: mergedAllow,
587
+ deny: mergedDeny,
573
588
  },
574
589
  };
575
590
  try {
@@ -8,7 +8,7 @@
8
8
  * third-party credential: logins live in the host's own store, and a token
9
9
  * variable is only named, never read here.
10
10
  */
11
- import { MCP_SERVER_PREFIX, mcpById } from './extensions-catalog.mjs';
11
+ import { MCP_CATALOG, MCP_SERVER_PREFIX, mcpById } from './extensions-catalog.mjs';
12
12
  import { tomlBasicString } from './codex-engineering.mjs';
13
13
 
14
14
  /** @typedef {import('./extensions-catalog.mjs').McpEntry} McpEntry */
@@ -80,7 +80,27 @@ export function claudeMcpEntries(entries) {
80
80
  }
81
81
 
82
82
  /**
83
- * Session-only `-c` overrides for the owned Codex app-server.
83
+ * Claude Code deny rules for the tools that spend money (FR7.2). They cover every
84
+ * catalog server, loaded or not, so the settings file can be written before the
85
+ * license is confirmed; a rule for a server that is not loaded matches nothing.
86
+ * Claude evaluates deny rules before allow rules, in every permission mode, and a
87
+ * deny rule removes the tool from the model's context (measured M1, Claude 2.1.318).
88
+ * @param {readonly McpEntry[]} [entries]
89
+ * @returns {string[]}
90
+ */
91
+ export function claudeDenyRules(entries = MCP_CATALOG) {
92
+ return entries.flatMap((entry) => {
93
+ const prefix = `mcp__${serverName(entry.id)}__`;
94
+ return [
95
+ ...(entry.blockedTools ?? []).map((tool) => `${prefix}${tool}`),
96
+ ...(entry.blockedToolPrefixes ?? []).map((start) => `${prefix}${start}*`),
97
+ ];
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Session-only `-c` overrides for the owned Codex app-server. A server with
103
+ * blocked tools gets `disabled_tools` (FR7.2; measured M2, Codex 0.164.0).
84
104
  * @param {readonly McpEntry[]} entries
85
105
  * @returns {string[]}
86
106
  */
@@ -88,6 +108,11 @@ export function codexMcpConfigArgs(entries) {
88
108
  return entries.flatMap((entry) => {
89
109
  const key = `mcp_servers.${serverName(entry.id)}`;
90
110
  const { transport } = entry;
111
+ const blocked = entry.blockedTools ?? [];
112
+ const disabled =
113
+ blocked.length === 0
114
+ ? []
115
+ : ['-c', `${key}.disabled_tools=[${blocked.map(tomlBasicString).join(',')}]`];
91
116
  if (transport.type === 'stdio')
92
117
  return [
93
118
  '-c',
@@ -96,6 +121,7 @@ export function codexMcpConfigArgs(entries) {
96
121
  `${key}.args=[${transport.args.map(tomlBasicString).join(',')}]`,
97
122
  '-c',
98
123
  `${key}.startup_timeout_sec=${STDIO_STARTUP_TIMEOUT_SEC}`,
124
+ ...disabled,
99
125
  ];
100
126
  return [
101
127
  '-c',
@@ -103,6 +129,7 @@ export function codexMcpConfigArgs(entries) {
103
129
  ...(transport.bearerEnv === undefined
104
130
  ? []
105
131
  : ['-c', `${key}.bearer_token_env_var=${tomlBasicString(transport.bearerEnv)}`]),
132
+ ...disabled,
106
133
  ];
107
134
  });
108
135
  }
package/dist/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@brunobracaioli/aibrain",
3
- "version": "0.4.0-beta.6",
3
+ "version": "0.4.0-beta.8",
4
4
  "type": "module"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brunobracaioli/aibrain",
3
- "version": "0.4.0-beta.6",
3
+ "version": "0.4.0-beta.8",
4
4
  "private": false,
5
5
  "description": "AISAC client for Claude Code and Codex: AI Brain memory, context management and AISAC Engineering",
6
6
  "license": "UNLICENSED",