nothumanallowed 17.0.0 → 17.0.2

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/README.md CHANGED
@@ -27,7 +27,7 @@ nha run "Design a Kubernetes deployment for a 10K RPS API"
27
27
  nha ui
28
28
  ```
29
29
 
30
- **Do I need a local model?** No. You need one LLM, and you choose which: an API key of a cloud provider, or a model running on your machine. A local model is the option that needs no key and sends nothing out. The hosted free tier (Liara) that earlier versions used by default is currently offline: set a provider before the first prompt.
30
+ **Do I need a local model?** No. You need one LLM, and you choose which: an API key of a cloud provider, or a model running on your machine. A local model is the option that needs no key and sends nothing out. Earlier versions started on a hosted free model (Liara). It is offline and no longer the default: a new install has no provider until you set one.
31
31
 
32
32
  ## Studio — Visual Agentic Workflows
33
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "17.0.0",
3
+ "version": "17.0.2",
4
4
  "description": "Local AI assistant: 80 tools (Gmail, Calendar, Drive, GitHub, Slack, browser, code, files), 38 agents, multi-agent deliberation that runs entirely on your machine (Legion X), visual workflows (Studio, AWF, WebCraft). Install with `npm i -g nothumanallowed`, run with `nha ui`. Bring your own LLM: an API key or a local model (Ollama, any OpenAI-compatible endpoint). Your data stays on your PC — OAuth tokens local, no cloud. Open-source MIT.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -582,7 +582,7 @@ function cmdConfig(args) {
582
582
  console.log(`\n ${BOLD}NHA Configuration${NC} ${D}(~/.nha/config.json)${NC}\n`);
583
583
 
584
584
  console.log(` ${C}LLM${NC}`);
585
- console.log(` Provider: ${W}${config.llm.provider}${NC}`);
585
+ console.log(` Provider: ${config.llm.provider ? W + config.llm.provider : R + '(not set)'}${NC}`);
586
586
  console.log(` API Key: ${config.llm.apiKey ? G + config.llm.apiKey.slice(0, 12) + '...' + NC : R + '(not set)' + NC}`);
587
587
  if (config.llm.openaiKey) console.log(` OpenAI Key: ${G}${config.llm.openaiKey.slice(0, 12)}...${NC}`);
588
588
  if (config.llm.geminiKey) console.log(` Gemini Key: ${G}${config.llm.geminiKey.slice(0, 12)}...${NC}`);
@@ -10,7 +10,7 @@ import fs from 'fs';
10
10
  import path from 'path';
11
11
  import { loadConfig } from '../config.mjs';
12
12
  import { AGENTS_DIR, AGENTS } from '../constants.mjs';
13
- import { getProviderCall, getApiKey, parseAgentFile } from '../services/llm.mjs';
13
+ import { getProviderCall, getApiKey, hostedOfflineError, parseAgentFile } from '../services/llm.mjs';
14
14
  import { fail, info, ok, C, G, Y, D, W, BOLD, NC, M } from '../ui.mjs';
15
15
 
16
16
  export async function cmdAsk(args) {
@@ -108,15 +108,8 @@ export async function cmdAsk(args) {
108
108
  let response = '';
109
109
 
110
110
  if (provider === 'nha') {
111
- // NHA Free tier — Liara Vision
112
- const res = await fetch('https://nothumanallowed.com/api/v1/liara/vision', {
113
- method: 'POST',
114
- headers: { 'Content-Type': 'application/json' },
115
- body: JSON.stringify({ image_base64: base64, prompt: imagePrompt }),
116
- });
117
- if (!res.ok) throw new Error(`Liara Vision ${res.status}`);
118
- const data = await res.json();
119
- response = data.description || data.text || JSON.stringify(data);
111
+ // The hosted model no longer reads images: nothing is sent to it.
112
+ throw hostedOfflineError();
120
113
  } else if (provider === 'anthropic') {
121
114
  const res = await fetch('https://api.anthropic.com/v1/messages', {
122
115
  method: 'POST',
package/src/config.mjs CHANGED
@@ -11,7 +11,10 @@ const LEGACY_PIF = path.join(os.homedir(), '.pif-agent.json');
11
11
  const DEFAULT_CONFIG = {
12
12
  version: 1,
13
13
  llm: {
14
- provider: 'nha',
14
+ // Empty = not chosen yet. It used to be 'nha', the hosted free model,
15
+ // which is offline: a new install must not start on a provider that
16
+ // cannot answer. A provider saved by an existing user is left as it is.
17
+ provider: '',
15
18
  apiKey: '',
16
19
  openaiKey: '',
17
20
  geminiKey: '',
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '17.0.0';
8
+ export const VERSION = '17.0.2';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -6061,9 +6061,30 @@ var GETH_DOMAIN_AFFINITY = {
6061
6061
  * A replacement comes from the preferred categories, then the allowed ones,
6062
6062
  * and is never an agent already in the list. With no replacement left the
6063
6063
  * original stays: too few agents is worse than one out of place.
6064
- * LOGOS always passes (domain-agnostic reasoner). A general or uncertain
6065
- * domain (confidence below 0.3) changes nothing.
6064
+ * The domain-agnostic agents always pass. A general or uncertain domain
6065
+ * (confidence below 0.3) changes nothing.
6066
6066
  */
6067
+ // Their work applies to any subject, whatever category the catalog files them
6068
+ // under: LOGOS checks the reasoning, VERITAS checks the claims.
6069
+ var GETH_DOMAIN_AGNOSTIC_AGENTS = ['LOGOS', 'VERITAS'];
6070
+
6071
+ var GETH_DOMAIN_TIER_PROHIBITED = 3;
6072
+
6073
+ /**
6074
+ * How well an agent fits the question's domain: 0 preferred category,
6075
+ * 1 allowed, 2 neither listed, 3 prohibited. A general or uncertain domain
6076
+ * (confidence below 0.3) ranks every agent the same, and the domain-agnostic
6077
+ * agents are never prohibited.
6078
+ */
6079
+ function gethDomainTier(agentName, category, domain) {
6080
+ if (!domain || domain.family === 'general' || domain.confidence < 0.3) return 0;
6081
+ var affinity = GETH_DOMAIN_AFFINITY[domain.family] || GETH_DOMAIN_AFFINITY.general;
6082
+ if (affinity.preferred.indexOf(category) !== -1) return 0;
6083
+ if (affinity.allowed.indexOf(category) !== -1) return 1;
6084
+ if (affinity.prohibited.indexOf(category) === -1) return 2;
6085
+ return GETH_DOMAIN_AGNOSTIC_AGENTS.indexOf(String(agentName).toUpperCase()) !== -1 ? 1 : GETH_DOMAIN_TIER_PROHIBITED;
6086
+ }
6087
+
6067
6088
  function gethValidateAgentDomainAffinity(agents, domain, available) {
6068
6089
  var unchanged = agents.map(function(a) { return { name: a.name, category: a.category }; });
6069
6090
  if (domain.family === 'general' || domain.confidence < 0.3) return unchanged;
@@ -6085,7 +6106,7 @@ function gethValidateAgentDomainAffinity(agents, domain, available) {
6085
6106
  }
6086
6107
 
6087
6108
  return agents.map(function(agent) {
6088
- if (agent.name.toUpperCase() === 'LOGOS' || affinity.prohibited.indexOf(agent.category) === -1) {
6109
+ if (GETH_DOMAIN_AGNOSTIC_AGENTS.indexOf(agent.name.toUpperCase()) !== -1 || affinity.prohibited.indexOf(agent.category) === -1) {
6089
6110
  return { name: agent.name, category: agent.category };
6090
6111
  }
6091
6112
  var replacement = findReplacement();
@@ -7300,21 +7321,32 @@ class LocalGethOrchestrator {
7300
7321
  /**
7301
7322
  * One orchestrator call with provider fallback. Tries the preferred provider,
7302
7323
  * then the others in order; throws the last error when all of them fail.
7324
+ *
7325
+ * `usable(text)` is optional: when it returns false the answer cannot be
7326
+ * read (a model that chats instead of following the format), and the next
7327
+ * configured provider is asked. If none does better, the first unreadable
7328
+ * answer is returned and the caller decides what to make of it.
7303
7329
  */
7304
- async orchestratorChat(session, systemPrompt, userMessage, maxTokens, agentTag) {
7330
+ async orchestratorChat(session, systemPrompt, userMessage, maxTokens, agentTag, usable) {
7305
7331
  var preferred = this.pickOrchestratorProvider(session);
7306
7332
  var order = [preferred].concat(session.providers.filter(function(p) { return p !== preferred; }));
7307
7333
  var lastErr = null;
7334
+ var unreadable = null;
7308
7335
  for (var i = 0; i < order.length; i++) {
7309
7336
  try {
7310
7337
  var text = await this.llm.chatWithProvider(order[i], systemPrompt, userMessage, {
7311
7338
  maxTokens: maxTokens, agentTag: agentTag,
7312
7339
  });
7340
+ if (usable && !usable(text)) {
7341
+ if (!unreadable) unreadable = { text: text, provider: order[i] };
7342
+ continue;
7343
+ }
7313
7344
  return { text: text, provider: order[i] };
7314
7345
  } catch (err) {
7315
7346
  lastErr = err;
7316
7347
  }
7317
7348
  }
7349
+ if (unreadable) return unreadable;
7318
7350
  throw lastErr || new Error('No provider available for the orchestrator');
7319
7351
  }
7320
7352
 
@@ -7882,11 +7914,22 @@ class LocalGethOrchestrator {
7882
7914
  assignments.forEach(function(a) { assigned[a.agentName] = true; });
7883
7915
  var categoriesInUse = {};
7884
7916
  assignments.forEach(function(a) { categoriesInUse[a.category] = true; });
7917
+ // The agents added here answer the same question as the ones PROMETHEUS
7918
+ // chose, so the same domain rule applies: an agent the guardrail just
7919
+ // removed, or one of a category prohibited for the domain, is not
7920
+ // brought back in by the top-up. Fitting categories come first.
7921
+ var topUpDomain = session.domain || gethClassifyDomain(session.prompt);
7922
+ var removedByGuardrail = {};
7923
+ (session.domainReplacements || []).forEach(function(r) { removedByGuardrail[String(r.from).toUpperCase()] = true; });
7885
7924
  var candidates = this.getCatalog()
7886
- .filter(function(a) { return !assigned[a.agentName] && PARLIAMENT_AGENT_NAMES.indexOf(a.agentName) === -1; });
7925
+ .filter(function(a) { return !assigned[a.agentName] && PARLIAMENT_AGENT_NAMES.indexOf(a.agentName) === -1; })
7926
+ .filter(function(a) { return !removedByGuardrail[a.agentName.toUpperCase()]; })
7927
+ .map(function(a) { return { agent: a, tier: gethDomainTier(a.agentName, a.category, topUpDomain) }; })
7928
+ .filter(function(c) { return c.tier < GETH_DOMAIN_TIER_PROHIBITED; });
7887
7929
  candidates = gethShuffle(candidates).sort(function(x, y) {
7888
- return (categoriesInUse[x.category] ? 1 : 0) - (categoriesInUse[y.category] ? 1 : 0);
7889
- });
7930
+ if (x.tier !== y.tier) return x.tier - y.tier;
7931
+ return (categoriesInUse[x.agent.category] ? 1 : 0) - (categoriesInUse[y.agent.category] ? 1 : 0);
7932
+ }).map(function(c) { return c.agent; });
7890
7933
  for (var ci = 0; ci < candidates.length && assignments.length < providerFloor; ci++) {
7891
7934
  assignments.push({
7892
7935
  agentName: candidates[ci].agentName,
@@ -8465,16 +8508,23 @@ class LocalGethOrchestrator {
8465
8508
  'At most 12 claims, the most consequential first. If nothing deserves a warning, return an empty list.\n\n' +
8466
8509
  'Respond ONLY with JSON: {"claims":[{"claim":"the claim, quoted or closely paraphrased","agents":["AGENT"],"status":"contradicted|doubtful|unsupported","note":"why, in one sentence"}]}';
8467
8510
 
8511
+ // A list of claims, even an empty one, or null when the answer is not one.
8512
+ function readClaims(text) {
8513
+ var json = extractJSON(text);
8514
+ if (json && Array.isArray(json.claims)) return json;
8515
+ // Cut off by the token limit: the claims already written still count.
8516
+ var salvagedClaims = gethSalvageArrayObjects(text, 'claims');
8517
+ return salvagedClaims.length > 0 ? { claims: salvagedClaims } : null;
8518
+ }
8519
+
8468
8520
  try {
8521
+ // Any of the user's providers can do this: one that cannot answer in
8522
+ // the format is followed by the next.
8469
8523
  var result = await this.orchestratorChat(session, systemPrompt,
8470
- 'Question: ' + session.prompt + '\n\nAgents\' final positions:\n\n' + body, 4096, '_fact_check');
8471
- var parsed = extractJSON(result.text);
8472
- if (!parsed || !Array.isArray(parsed.claims)) {
8473
- // Cut off by the token limit: the claims already written still count.
8474
- var salvagedClaims = gethSalvageArrayObjects(result.text, 'claims');
8475
- if (salvagedClaims.length === 0) return null;
8476
- parsed = { claims: salvagedClaims };
8477
- }
8524
+ 'Question: ' + session.prompt + '\n\nAgents\' final positions:\n\n' + body, 4096, '_fact_check',
8525
+ function(text) { return readClaims(text) !== null; });
8526
+ var parsed = readClaims(result.text);
8527
+ if (!parsed) return null;
8478
8528
  var allowed = ['contradicted', 'doubtful', 'unsupported'];
8479
8529
  var claims = [];
8480
8530
  for (var i = 0; i < parsed.claims.length && claims.length < 12; i++) {
@@ -8792,7 +8842,10 @@ class LocalGethOrchestrator {
8792
8842
 
8793
8843
  try {
8794
8844
  // 1024 tokens cut the list of omissions short on real deliberations.
8795
- var result = await this.orchestratorChat(session, systemPrompt, userMessage, 4096, 'ATHENA');
8845
+ var result = await this.orchestratorChat(session, systemPrompt, userMessage, 4096, 'ATHENA', function(text) {
8846
+ var json = extractJSON(text);
8847
+ return !!(json && (json.verdict === 'PASS' || json.verdict === 'FLAG')) || gethSalvageAthenaVerdict(text) !== null;
8848
+ });
8796
8849
  var parsed = extractJSON(result.text);
8797
8850
  var truncated = false;
8798
8851
  if (!parsed || (parsed.verdict !== 'PASS' && parsed.verdict !== 'FLAG')) {
@@ -1234,22 +1234,8 @@ export async function callLLMVision(config, systemPrompt, userMessage, media) {
1234
1234
  const provider = config.llm.provider || 'anthropic';
1235
1235
  const model = config.llm.model || null;
1236
1236
 
1237
- // NHA Free tier — use Liara Vision (no API key needed)
1238
- if (provider === 'nha') {
1239
- const { base64, mimeType } = media;
1240
- if (!base64) throw new Error('media.base64 required for vision');
1241
- const res = await fetch('https://nothumanallowed.com/api/v1/liara/vision', {
1242
- method: 'POST',
1243
- headers: { 'Content-Type': 'application/json', 'x-nha-client': 'cli' },
1244
- body: JSON.stringify({ image_base64: base64, prompt: userMessage || 'Describe this image in detail.' }),
1245
- });
1246
- if (!res.ok) {
1247
- const err = await res.text().catch(() => '');
1248
- throw new Error(`Liara Vision ${res.status}: ${err.slice(0, 200)}`);
1249
- }
1250
- const data = await res.json();
1251
- return data.description || data.text || JSON.stringify(data);
1252
- }
1237
+ // The hosted model no longer reads images: nothing is sent to it.
1238
+ if (provider === 'nha') throw hostedOfflineError();
1253
1239
 
1254
1240
  const apiKey = getApiKey(config, provider);
1255
1241
  if (!apiKey) throw new Error(`No API key for ${provider}. Vision requires Claude, GPT-4 or Gemini with your own key (nha config set key YOUR_KEY).`);