@ornexus/neocortex 3.9.19 → 3.9.22

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.
Files changed (97) hide show
  1. package/install.ps1 +1 -1
  2. package/install.sh +12 -1
  3. package/package.json +4 -2
  4. package/packages/client/dist/commands/activate.js +23 -2
  5. package/packages/client/dist/commands/invoke.js +91 -25
  6. package/packages/client/dist/config/secure-config.d.ts +69 -0
  7. package/packages/client/dist/config/secure-config.js +179 -0
  8. package/targets-stubs/antigravity/gemini.md +1 -1
  9. package/targets-stubs/antigravity/skill/SKILL.md +1 -1
  10. package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
  11. package/targets-stubs/claude-code/neocortex.md +2 -2
  12. package/targets-stubs/codex/agents.md +1 -1
  13. package/targets-stubs/cursor/agent.md +2 -2
  14. package/targets-stubs/gemini-cli/agent.md +2 -2
  15. package/targets-stubs/vscode/agent.md +2 -2
  16. package/packages/client/dist/adapters/adapter-registry.d.ts.map +0 -1
  17. package/packages/client/dist/adapters/adapter-registry.js.map +0 -1
  18. package/packages/client/dist/adapters/antigravity-adapter.d.ts.map +0 -1
  19. package/packages/client/dist/adapters/antigravity-adapter.js.map +0 -1
  20. package/packages/client/dist/adapters/claude-code-adapter.d.ts.map +0 -1
  21. package/packages/client/dist/adapters/claude-code-adapter.js.map +0 -1
  22. package/packages/client/dist/adapters/codex-adapter.d.ts.map +0 -1
  23. package/packages/client/dist/adapters/codex-adapter.js.map +0 -1
  24. package/packages/client/dist/adapters/cursor-adapter.d.ts.map +0 -1
  25. package/packages/client/dist/adapters/cursor-adapter.js.map +0 -1
  26. package/packages/client/dist/adapters/gemini-adapter.d.ts.map +0 -1
  27. package/packages/client/dist/adapters/gemini-adapter.js.map +0 -1
  28. package/packages/client/dist/adapters/index.d.ts.map +0 -1
  29. package/packages/client/dist/adapters/index.js.map +0 -1
  30. package/packages/client/dist/adapters/platform-detector.d.ts.map +0 -1
  31. package/packages/client/dist/adapters/platform-detector.js.map +0 -1
  32. package/packages/client/dist/adapters/target-adapter.d.ts.map +0 -1
  33. package/packages/client/dist/adapters/target-adapter.js.map +0 -1
  34. package/packages/client/dist/adapters/vscode-adapter.d.ts.map +0 -1
  35. package/packages/client/dist/adapters/vscode-adapter.js.map +0 -1
  36. package/packages/client/dist/agent/refresh-stubs.d.ts.map +0 -1
  37. package/packages/client/dist/agent/refresh-stubs.js.map +0 -1
  38. package/packages/client/dist/agent/update-agent-yaml.d.ts.map +0 -1
  39. package/packages/client/dist/agent/update-agent-yaml.js.map +0 -1
  40. package/packages/client/dist/agent/update-description.d.ts.map +0 -1
  41. package/packages/client/dist/agent/update-description.js.map +0 -1
  42. package/packages/client/dist/cache/crypto-utils.d.ts.map +0 -1
  43. package/packages/client/dist/cache/crypto-utils.js.map +0 -1
  44. package/packages/client/dist/cache/encrypted-cache.d.ts.map +0 -1
  45. package/packages/client/dist/cache/encrypted-cache.js.map +0 -1
  46. package/packages/client/dist/cache/index.d.ts.map +0 -1
  47. package/packages/client/dist/cache/index.js.map +0 -1
  48. package/packages/client/dist/cli.d.ts.map +0 -1
  49. package/packages/client/dist/cli.js.map +0 -1
  50. package/packages/client/dist/commands/activate.d.ts.map +0 -1
  51. package/packages/client/dist/commands/activate.js.map +0 -1
  52. package/packages/client/dist/commands/cache-status.d.ts.map +0 -1
  53. package/packages/client/dist/commands/cache-status.js.map +0 -1
  54. package/packages/client/dist/commands/invoke.d.ts.map +0 -1
  55. package/packages/client/dist/commands/invoke.js.map +0 -1
  56. package/packages/client/dist/config/resolver-selection.d.ts.map +0 -1
  57. package/packages/client/dist/config/resolver-selection.js.map +0 -1
  58. package/packages/client/dist/context/context-collector.d.ts.map +0 -1
  59. package/packages/client/dist/context/context-collector.js.map +0 -1
  60. package/packages/client/dist/context/context-sanitizer.d.ts.map +0 -1
  61. package/packages/client/dist/context/context-sanitizer.js.map +0 -1
  62. package/packages/client/dist/index.d.ts.map +0 -1
  63. package/packages/client/dist/index.js.map +0 -1
  64. package/packages/client/dist/license/index.d.ts.map +0 -1
  65. package/packages/client/dist/license/index.js.map +0 -1
  66. package/packages/client/dist/license/license-client.d.ts.map +0 -1
  67. package/packages/client/dist/license/license-client.js.map +0 -1
  68. package/packages/client/dist/machine/fingerprint.d.ts.map +0 -1
  69. package/packages/client/dist/machine/fingerprint.js.map +0 -1
  70. package/packages/client/dist/machine/index.d.ts.map +0 -1
  71. package/packages/client/dist/machine/index.js.map +0 -1
  72. package/packages/client/dist/resilience/circuit-breaker.d.ts.map +0 -1
  73. package/packages/client/dist/resilience/circuit-breaker.js.map +0 -1
  74. package/packages/client/dist/resilience/degradation-manager.d.ts.map +0 -1
  75. package/packages/client/dist/resilience/degradation-manager.js.map +0 -1
  76. package/packages/client/dist/resilience/freshness-indicator.d.ts.map +0 -1
  77. package/packages/client/dist/resilience/freshness-indicator.js.map +0 -1
  78. package/packages/client/dist/resilience/index.d.ts.map +0 -1
  79. package/packages/client/dist/resilience/index.js.map +0 -1
  80. package/packages/client/dist/resilience/recovery-detector.d.ts.map +0 -1
  81. package/packages/client/dist/resilience/recovery-detector.js.map +0 -1
  82. package/packages/client/dist/resolvers/asset-resolver.d.ts.map +0 -1
  83. package/packages/client/dist/resolvers/asset-resolver.js.map +0 -1
  84. package/packages/client/dist/resolvers/local-resolver.d.ts.map +0 -1
  85. package/packages/client/dist/resolvers/local-resolver.js.map +0 -1
  86. package/packages/client/dist/resolvers/remote-resolver.d.ts.map +0 -1
  87. package/packages/client/dist/resolvers/remote-resolver.js.map +0 -1
  88. package/packages/client/dist/telemetry/index.d.ts.map +0 -1
  89. package/packages/client/dist/telemetry/index.js.map +0 -1
  90. package/packages/client/dist/telemetry/offline-queue.d.ts.map +0 -1
  91. package/packages/client/dist/telemetry/offline-queue.js.map +0 -1
  92. package/packages/client/dist/tier/index.d.ts.map +0 -1
  93. package/packages/client/dist/tier/index.js.map +0 -1
  94. package/packages/client/dist/tier/tier-aware-client.d.ts.map +0 -1
  95. package/packages/client/dist/tier/tier-aware-client.js.map +0 -1
  96. package/packages/client/dist/types/index.d.ts.map +0 -1
  97. package/packages/client/dist/types/index.js.map +0 -1
package/install.ps1 CHANGED
@@ -15,7 +15,7 @@ param(
15
15
  [string]$ServerUrl = "https://api.neocortex.ornexus.com"
16
16
  )
17
17
 
18
- $VERSION = "3.9.19"
18
+ $VERSION = "3.9.22"
19
19
 
20
20
  # =============================================================================
21
21
  # CONFIGURACOES
package/install.sh CHANGED
@@ -4,7 +4,7 @@
4
4
  # Development Orchestrator
5
5
 
6
6
  # Versao do instalador
7
- VERSION="3.9.19"
7
+ VERSION="3.9.22"
8
8
 
9
9
  # Flags
10
10
  MIGRATION_DETECTED=false
@@ -569,12 +569,18 @@ setup_thin_client_config() {
569
569
  mkdir -p "$config_dir" 2>/dev/null
570
570
  mkdir -p "$config_dir/cache" 2>/dev/null
571
571
 
572
+ # Story 61.4 - F4 remediation: restrictive permissions
573
+ chmod 700 "$config_dir" 2>/dev/null
574
+ chmod 700 "$config_dir/cache" 2>/dev/null
575
+
572
576
  if [ -f "$config_file" ]; then
573
577
  # Preservar config existente, atualizar apenas serverUrl se necessario
574
578
  local existing_mode
575
579
  existing_mode=$(grep '"mode"' "$config_file" 2>/dev/null | head -1 | sed 's/.*"mode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
576
580
 
577
581
  if [ "$existing_mode" = "active" ] || [ "$existing_mode" = "local" ] || [ "$existing_mode" = "remote" ]; then
582
+ # Story 61.4 - ensure permissions are always enforced even on existing configs
583
+ chmod 600 "$config_file" 2>/dev/null
578
584
  debug "Config existente preservada (mode=$existing_mode)"
579
585
  return 0
580
586
  fi
@@ -602,6 +608,9 @@ setup_thin_client_config() {
602
608
  }
603
609
  EOFCONFIG
604
610
 
611
+ # Story 61.4 - F4 remediation: config file readable only by owner
612
+ chmod 600 "$config_file" 2>/dev/null
613
+
605
614
  debug "Thin client config criada: $config_file"
606
615
  }
607
616
 
@@ -711,6 +720,7 @@ install_agent() {
711
720
 
712
721
  # Dynamic description: patch tier from existing config (if activated)
713
722
  patch_description_tier "$DEST_DIR/neocortex.md"
723
+ patch_description_tier "$DEST_DIR/neocortex.agent.yaml"
714
724
 
715
725
  # Cleanup: remover workflow.md de instalacoes anteriores (v3.8 -> v3.9)
716
726
  if [ -f "$DEST_DIR/workflow.md" ]; then
@@ -1187,6 +1197,7 @@ create_project_dirs() {
1187
1197
 
1188
1198
  # Dynamic description: patch tier
1189
1199
  patch_description_tier "$project_dir/.claude/agents/neocortex/neocortex.md"
1200
+ patch_description_tier "$project_dir/.claude/agents/neocortex/neocortex.agent.yaml"
1190
1201
 
1191
1202
  # Thin-client: cleanup legacy IP from previous installs
1192
1203
  auto_cleanup_legacy_project "$project_dir"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ornexus/neocortex",
3
- "version": "3.9.19",
4
- "description": "Neocortex v3.9.19 - Orquestrador de Desenvolvimento de Epics & Stories para Claude Code",
3
+ "version": "3.9.22",
4
+ "description": "Neocortex v3.9.22 - Orquestrador de Desenvolvimento de Epics & Stories para Claude Code",
5
5
  "keywords": [
6
6
  "claude",
7
7
  "claude-code",
@@ -53,6 +53,8 @@
53
53
  "install.sh",
54
54
  "install.ps1",
55
55
  "packages/client/dist/",
56
+ "!packages/client/dist/**/*.js.map",
57
+ "!packages/client/dist/**/*.d.ts.map",
56
58
  "targets-stubs/"
57
59
  ],
58
60
  "scripts": {
@@ -29,6 +29,7 @@ import { getMachineFingerprint } from '../machine/fingerprint.js';
29
29
  import { updateAgentDescription } from '../agent/update-description.js';
30
30
  import { refreshStubs } from '../agent/refresh-stubs.js';
31
31
  import { updateAgentYaml } from '../agent/update-agent-yaml.js';
32
+ import { saveSecureConfig, setSecureDirPermissions } from '../config/secure-config.js';
32
33
  // ── Version Resolution ────────────────────────────────────────────────────
33
34
  function getInstalledVersion() {
34
35
  try {
@@ -94,11 +95,31 @@ function loadExistingConfig() {
94
95
  }
95
96
  /**
96
97
  * Save user config after successful activation.
97
- * License key is stored to enable LicenseClient re-creation in invoke.
98
+ * License key is encrypted using machine fingerprint (Story 61.2).
99
+ * File permissions set to 600 (Story 61.4).
98
100
  */
99
101
  function saveConfig(config) {
102
+ // Ensure directories exist with secure permissions
100
103
  mkdirSync(CONFIG_DIR, { recursive: true });
101
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', 'utf-8');
104
+ setSecureDirPermissions(CONFIG_DIR);
105
+ const cacheDir = join(CONFIG_DIR, 'cache');
106
+ mkdirSync(cacheDir, { recursive: true });
107
+ setSecureDirPermissions(cacheDir);
108
+ if (config.licenseKey) {
109
+ // Use secure config writer which encrypts the license key
110
+ saveSecureConfig({
111
+ serverUrl: config.serverUrl,
112
+ mode: config.mode,
113
+ machineId: config.machineId,
114
+ activatedAt: config.activatedAt,
115
+ tier: config.tier,
116
+ licenseKey: config.licenseKey,
117
+ });
118
+ }
119
+ else {
120
+ // Fallback: write without license key (should not happen in normal flow)
121
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', 'utf-8');
122
+ }
102
123
  }
103
124
  // ── Activate Command ──────────────────────────────────────────────────────
104
125
  /**
@@ -19,20 +19,22 @@
19
19
  *
20
20
  * Story 45.2 - AC1-AC6
21
21
  */
22
- import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
22
+ import { existsSync, readFileSync, unlinkSync } from 'node:fs';
23
23
  import { join } from 'node:path';
24
24
  import { homedir } from 'node:os';
25
25
  import { LicenseClient } from '../license/license-client.js';
26
26
  import { EncryptedCache } from '../cache/encrypted-cache.js';
27
27
  import { NoOpCache } from '../types/index.js';
28
+ import { TierAwareClient } from '../tier/tier-aware-client.js';
29
+ import { loadSecureConfig } from '../config/secure-config.js';
28
30
  // ── Constants ─────────────────────────────────────────────────────────────
29
31
  const DEFAULT_SERVER_URL = 'https://api.neocortex.ornexus.com';
30
32
  const CONFIG_DIR = join(homedir(), '.neocortex');
31
- const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
32
33
  const CACHE_DIR = join(CONFIG_DIR, 'cache');
33
34
  const MENU_CACHE_FILE = join(CACHE_DIR, 'menu-cache.json');
34
35
  const MENU_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
35
36
  const DEFAULT_TIMEOUT_MS = 30_000;
37
+ const CLIENT_VERSION = '0.1.0';
36
38
  // ── State Snapshot Collection ──────────────────────────────────────────────
37
39
  /**
38
40
  * Read state.json and construct a sanitized snapshot for the server.
@@ -113,14 +115,24 @@ export function collectStateSnapshot(projectRoot) {
113
115
  epics,
114
116
  };
115
117
  }
116
- // ── Menu Cache ────────────────────────────────────────────────────────────
117
- function getMenuCache() {
118
+ // ── Menu Cache (Encrypted - Story 61.1) ──────────────────────────────────
119
+ const MENU_CACHE_KEY = 'neocortex:menu:cache';
120
+ /**
121
+ * Read menu cache from EncryptedCache.
122
+ * Falls back gracefully: if decryption fails or data is stale, returns null.
123
+ * Also cleans up legacy plaintext menu-cache.json if it exists.
124
+ */
125
+ async function getMenuCache(encryptedCache) {
118
126
  try {
119
- if (!existsSync(MENU_CACHE_FILE))
127
+ const raw = await encryptedCache.get(MENU_CACHE_KEY);
128
+ if (!raw)
120
129
  return null;
121
- const raw = readFileSync(MENU_CACHE_FILE, 'utf-8');
122
130
  const cache = JSON.parse(raw);
123
- // Check TTL
131
+ // Invalidate on version mismatch (stale cache from previous install)
132
+ if (cache.version !== CLIENT_VERSION) {
133
+ return null;
134
+ }
135
+ // Check TTL (EncryptedCache also has TTL, but we double-check for version-based invalidation)
124
136
  if (Date.now() - cache.cachedAt > MENU_CACHE_TTL_MS) {
125
137
  return null; // Expired
126
138
  }
@@ -130,32 +142,47 @@ function getMenuCache() {
130
142
  return null;
131
143
  }
132
144
  }
133
- function setMenuCache(instructions, metadata) {
145
+ /**
146
+ * Write menu cache to EncryptedCache.
147
+ * Deletes legacy plaintext menu-cache.json on first encrypted write.
148
+ */
149
+ async function setMenuCache(encryptedCache, instructions, metadata) {
134
150
  try {
135
- mkdirSync(CACHE_DIR, { recursive: true });
136
151
  const cache = {
137
152
  instructions,
138
153
  metadata,
139
154
  cachedAt: Date.now(),
155
+ version: CLIENT_VERSION,
140
156
  };
141
- writeFileSync(MENU_CACHE_FILE, JSON.stringify(cache), 'utf-8');
157
+ await encryptedCache.set(MENU_CACHE_KEY, JSON.stringify(cache), MENU_CACHE_TTL_MS);
158
+ // Delete legacy plaintext menu-cache.json if it exists (F1 remediation)
159
+ deleteLegacyMenuCache();
142
160
  }
143
161
  catch {
144
162
  // Cache write failure is non-critical
145
163
  }
146
164
  }
147
- // ── Config Loading ────────────────────────────────────────────────────────
148
- function loadConfig() {
165
+ /**
166
+ * Remove legacy plaintext menu-cache.json file.
167
+ * Called after successful encrypted write to prevent IP leakage.
168
+ */
169
+ function deleteLegacyMenuCache() {
149
170
  try {
150
- if (existsSync(CONFIG_FILE)) {
151
- const raw = readFileSync(CONFIG_FILE, 'utf-8');
152
- return JSON.parse(raw);
171
+ if (existsSync(MENU_CACHE_FILE)) {
172
+ unlinkSync(MENU_CACHE_FILE);
153
173
  }
154
174
  }
155
175
  catch {
156
- // Ignore
176
+ // Non-critical: best-effort cleanup
157
177
  }
158
- return null;
178
+ }
179
+ // ── Config Loading (Story 61.2 - Secure) ─────────────────────────────────
180
+ /**
181
+ * Load config with automatic decryption of license key.
182
+ * Handles migration from plaintext licenseKey to encryptedLicenseKey.
183
+ */
184
+ function loadConfig() {
185
+ return loadSecureConfig();
159
186
  }
160
187
  async function getAuthTokenAndClient(serverUrl, licenseKey) {
161
188
  try {
@@ -177,7 +204,11 @@ async function getAuthTokenAndClient(serverUrl, licenseKey) {
177
204
  const token = await client.getToken();
178
205
  if (!token)
179
206
  return null;
180
- return { token, client };
207
+ const tierClient = new TierAwareClient({
208
+ cacheProvider,
209
+ licenseClient: client,
210
+ });
211
+ return { token, client, tierClient };
181
212
  }
182
213
  catch {
183
214
  return null;
@@ -193,7 +224,7 @@ async function sendInvokeRequest(serverUrl, body, authToken) {
193
224
  headers: {
194
225
  'Content-Type': 'application/json',
195
226
  'Authorization': `Bearer ${authToken}`,
196
- 'X-Client-Version': '0.1.0',
227
+ 'X-Client-Version': CLIENT_VERSION,
197
228
  },
198
229
  body: JSON.stringify(body),
199
230
  signal: controller.signal,
@@ -245,17 +276,20 @@ async function sendInvokeRequest(serverUrl, body, authToken) {
245
276
  */
246
277
  export async function invoke(options) {
247
278
  const projectRoot = options.projectRoot ?? process.cwd();
248
- const format = options.format ?? 'plain';
249
279
  const platformTarget = options.platformTarget ?? 'claude-code';
250
280
  // 1. Determine server URL
251
281
  const config = loadConfig();
252
282
  const serverUrl = (options.serverUrl ?? config?.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/+$/, '');
253
283
  // 2. Collect state snapshot
254
284
  const stateSnapshot = collectStateSnapshot(projectRoot);
285
+ // 2a. Create encrypted cache for menu (uses licenseKey as passphrase)
286
+ const menuCache = config?.licenseKey
287
+ ? new EncryptedCache({ cacheDir: CACHE_DIR, passphrase: config.licenseKey })
288
+ : null;
255
289
  // 3. Check menu cache for empty invocations (AC6)
256
290
  const trimmedArgs = options.args.trim();
257
- if (!trimmedArgs) {
258
- const cachedMenu = getMenuCache();
291
+ if (!trimmedArgs && menuCache) {
292
+ const cachedMenu = await getMenuCache(menuCache);
259
293
  if (cachedMenu) {
260
294
  return {
261
295
  success: true,
@@ -274,6 +308,24 @@ export async function invoke(options) {
274
308
  exitCode: 2, // Not configured
275
309
  };
276
310
  }
311
+ // 4a. Pre-flight tier check (optimistic -- if it fails, still proceed)
312
+ const trigger = extractTrigger(trimmedArgs);
313
+ if (trigger) {
314
+ try {
315
+ const preFlightResult = await auth.tierClient.preFlightCheck(trigger);
316
+ if (!preFlightResult.allowed) {
317
+ process.stderr.write(`[neocortex] ${preFlightResult.message}\n`);
318
+ return {
319
+ success: false,
320
+ error: preFlightResult.message ?? 'Trigger not available on your plan',
321
+ exitCode: 1,
322
+ };
323
+ }
324
+ }
325
+ catch {
326
+ // Fail-open: pre-flight errors should not block invocation
327
+ }
328
+ }
277
329
  const requestBody = {
278
330
  args: trimmedArgs,
279
331
  projectRoot: projectRoot.replace(homedir(), '~'), // Sanitize absolute path
@@ -303,9 +355,13 @@ export async function invoke(options) {
303
355
  exitCode,
304
356
  };
305
357
  }
306
- // 6. Cache menu responses (AC6)
307
- if (!trimmedArgs && result.data.metadata?.mode === 'menu') {
308
- setMenuCache(result.data.instructions, result.data.metadata);
358
+ // 6. Cache menu responses (AC6) - encrypted (Story 61.1)
359
+ if (!trimmedArgs && result.data.metadata?.mode === 'menu' && menuCache) {
360
+ setMenuCache(menuCache, result.data.instructions, result.data.metadata).catch(() => { });
361
+ }
362
+ // 6a. Update cached quota from server response metadata (Epic 60)
363
+ if (result.data.metadata) {
364
+ auth.tierClient.updateQuotaFromResponse(result.data.metadata).catch(() => { });
309
365
  }
310
366
  return {
311
367
  success: true,
@@ -314,6 +370,16 @@ export async function invoke(options) {
314
370
  exitCode: 0,
315
371
  };
316
372
  }
373
+ // ── Trigger Extraction ──────────────────────────────────────────────────
374
+ /**
375
+ * Extract trigger name from args string.
376
+ * Triggers start with '*' (e.g., "*yolo", "*implement", "*status").
377
+ * Returns the trigger name without the '*' prefix, or null if no trigger.
378
+ */
379
+ function extractTrigger(args) {
380
+ const match = args.match(/^\*([a-zA-Z][\w-]*)/);
381
+ return match ? match[1] : null;
382
+ }
317
383
  // ── CLI Entry Point ───────────────────────────────────────────────────────
318
384
  /**
319
385
  * CLI handler for the invoke command.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @license FSL-1.1
3
+ * Copyright (c) 2026 OrNexus AI
4
+ *
5
+ * This file is part of Neocortex CLI, licensed under the
6
+ * Functional Source License, Version 1.1 (FSL-1.1).
7
+ *
8
+ * Change Date: February 20, 2029
9
+ * Change License: MIT
10
+ *
11
+ * See the LICENSE file in the project root for full license text.
12
+ */
13
+ /** Resolved config with decrypted license key */
14
+ export interface SecureConfig {
15
+ readonly serverUrl?: string;
16
+ readonly mode?: string;
17
+ readonly machineId?: string;
18
+ readonly activatedAt?: string;
19
+ readonly tier?: string;
20
+ readonly licenseKey?: string;
21
+ }
22
+ /**
23
+ * Encrypt a license key using the machine fingerprint as passphrase.
24
+ * Returns the encrypted envelope string.
25
+ */
26
+ export declare function encryptLicenseKey(licenseKey: string): string;
27
+ /**
28
+ * Decrypt a license key using the machine fingerprint as passphrase.
29
+ * Returns the plaintext key or null if decryption fails (hardware changed).
30
+ */
31
+ export declare function decryptLicenseKey(encryptedKey: string): string | null;
32
+ /**
33
+ * Set restrictive file permissions (600) on config files.
34
+ * Skipped on Windows where chmod is not meaningful.
35
+ * Story 61.4 - F4 remediation.
36
+ */
37
+ export declare function setSecureFilePermissions(filePath: string): void;
38
+ /**
39
+ * Set restrictive directory permissions (700) on config directories.
40
+ * Skipped on Windows where chmod is not meaningful.
41
+ * Story 61.4 - F4 remediation.
42
+ */
43
+ export declare function setSecureDirPermissions(dirPath: string): void;
44
+ /**
45
+ * Load config from ~/.neocortex/config.json with automatic migration.
46
+ *
47
+ * If the config contains a plaintext `licenseKey` (old format), it is:
48
+ * 1. Encrypted using machine fingerprint
49
+ * 2. Stored as `encryptedLicenseKey`
50
+ * 3. Old `licenseKey` field removed
51
+ * 4. Config rewritten to disk
52
+ *
53
+ * If decryption of `encryptedLicenseKey` fails (hardware change),
54
+ * returns config with licenseKey = undefined.
55
+ */
56
+ export declare function loadSecureConfig(): SecureConfig | null;
57
+ /**
58
+ * Save config after activation with encrypted license key.
59
+ * This is the primary write path called from activate.ts.
60
+ */
61
+ export declare function saveSecureConfig(config: {
62
+ serverUrl: string;
63
+ mode: string;
64
+ machineId: string;
65
+ activatedAt: string;
66
+ tier?: string;
67
+ licenseKey: string;
68
+ }): void;
69
+ //# sourceMappingURL=secure-config.d.ts.map
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @license FSL-1.1
3
+ * Copyright (c) 2026 OrNexus AI
4
+ *
5
+ * This file is part of Neocortex CLI, licensed under the
6
+ * Functional Source License, Version 1.1 (FSL-1.1).
7
+ *
8
+ * Change Date: February 20, 2029
9
+ * Change License: MIT
10
+ *
11
+ * See the LICENSE file in the project root for full license text.
12
+ */
13
+ /**
14
+ * @neocortex/client - Secure Config
15
+ *
16
+ * Handles reading and writing config.json with encrypted license key.
17
+ * Uses machine fingerprint as encryption seed so the key is bound
18
+ * to the physical machine.
19
+ *
20
+ * Story 61.2 - F2 remediation: license key no longer stored in plaintext.
21
+ *
22
+ * Migration: if config has plaintext `licenseKey`, it is silently
23
+ * migrated to `encryptedLicenseKey` on first read.
24
+ *
25
+ * If decryption fails (e.g. hardware change), returns null for
26
+ * licenseKey, requiring re-activation.
27
+ */
28
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
29
+ import { join } from 'node:path';
30
+ import { homedir } from 'node:os';
31
+ import { encrypt, decrypt } from '../cache/crypto-utils.js';
32
+ import { getMachineFingerprint } from '../machine/fingerprint.js';
33
+ // ── Constants ────────────────────────────────────────────────────────────
34
+ const CONFIG_DIR = join(homedir(), '.neocortex');
35
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
36
+ // ── License Key Encryption ──────────────────────────────────────────────
37
+ /**
38
+ * Encrypt a license key using the machine fingerprint as passphrase.
39
+ * Returns the encrypted envelope string.
40
+ */
41
+ export function encryptLicenseKey(licenseKey) {
42
+ const fingerprint = getMachineFingerprint();
43
+ return encrypt(licenseKey, fingerprint);
44
+ }
45
+ /**
46
+ * Decrypt a license key using the machine fingerprint as passphrase.
47
+ * Returns the plaintext key or null if decryption fails (hardware changed).
48
+ */
49
+ export function decryptLicenseKey(encryptedKey) {
50
+ try {
51
+ const fingerprint = getMachineFingerprint();
52
+ const result = decrypt(encryptedKey, fingerprint);
53
+ // expired flag is not relevant for license keys (no TTL)
54
+ return result.plaintext;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ // ── Secure File Permissions ─────────────────────────────────────────────
61
+ /**
62
+ * Set restrictive file permissions (600) on config files.
63
+ * Skipped on Windows where chmod is not meaningful.
64
+ * Story 61.4 - F4 remediation.
65
+ */
66
+ export function setSecureFilePermissions(filePath) {
67
+ if (process.platform === 'win32')
68
+ return;
69
+ try {
70
+ chmodSync(filePath, 0o600);
71
+ }
72
+ catch {
73
+ // Non-critical: best-effort
74
+ }
75
+ }
76
+ /**
77
+ * Set restrictive directory permissions (700) on config directories.
78
+ * Skipped on Windows where chmod is not meaningful.
79
+ * Story 61.4 - F4 remediation.
80
+ */
81
+ export function setSecureDirPermissions(dirPath) {
82
+ if (process.platform === 'win32')
83
+ return;
84
+ try {
85
+ chmodSync(dirPath, 0o700);
86
+ }
87
+ catch {
88
+ // Non-critical: best-effort
89
+ }
90
+ }
91
+ // ── Config Read/Write ───────────────────────────────────────────────────
92
+ /**
93
+ * Load config from ~/.neocortex/config.json with automatic migration.
94
+ *
95
+ * If the config contains a plaintext `licenseKey` (old format), it is:
96
+ * 1. Encrypted using machine fingerprint
97
+ * 2. Stored as `encryptedLicenseKey`
98
+ * 3. Old `licenseKey` field removed
99
+ * 4. Config rewritten to disk
100
+ *
101
+ * If decryption of `encryptedLicenseKey` fails (hardware change),
102
+ * returns config with licenseKey = undefined.
103
+ */
104
+ export function loadSecureConfig() {
105
+ try {
106
+ if (!existsSync(CONFIG_FILE))
107
+ return null;
108
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
109
+ const config = JSON.parse(raw);
110
+ // Migration path: plaintext licenseKey -> encrypted
111
+ if (config.licenseKey && !config.encryptedLicenseKey) {
112
+ const encrypted = encryptLicenseKey(config.licenseKey);
113
+ const migratedConfig = { ...config };
114
+ const plainKey = migratedConfig.licenseKey;
115
+ delete migratedConfig.licenseKey;
116
+ migratedConfig.encryptedLicenseKey = encrypted;
117
+ writeSecureConfig(migratedConfig);
118
+ return {
119
+ serverUrl: config.serverUrl,
120
+ mode: config.mode,
121
+ machineId: config.machineId,
122
+ activatedAt: config.activatedAt,
123
+ tier: config.tier,
124
+ licenseKey: plainKey,
125
+ };
126
+ }
127
+ // Decrypt encrypted license key
128
+ let licenseKey;
129
+ if (config.encryptedLicenseKey) {
130
+ const decrypted = decryptLicenseKey(config.encryptedLicenseKey);
131
+ if (decrypted) {
132
+ licenseKey = decrypted;
133
+ }
134
+ // If decryption fails, licenseKey remains undefined -> requires re-activation
135
+ }
136
+ return {
137
+ serverUrl: config.serverUrl,
138
+ mode: config.mode,
139
+ machineId: config.machineId,
140
+ activatedAt: config.activatedAt,
141
+ tier: config.tier,
142
+ licenseKey,
143
+ };
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ /**
150
+ * Write config to disk with encrypted license key and secure permissions.
151
+ * The licenseKey field should already be encrypted as encryptedLicenseKey.
152
+ */
153
+ function writeSecureConfig(config) {
154
+ mkdirSync(CONFIG_DIR, { recursive: true });
155
+ setSecureDirPermissions(CONFIG_DIR);
156
+ const cacheDir = join(CONFIG_DIR, 'cache');
157
+ if (existsSync(cacheDir)) {
158
+ setSecureDirPermissions(cacheDir);
159
+ }
160
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', 'utf-8');
161
+ setSecureFilePermissions(CONFIG_FILE);
162
+ }
163
+ /**
164
+ * Save config after activation with encrypted license key.
165
+ * This is the primary write path called from activate.ts.
166
+ */
167
+ export function saveSecureConfig(config) {
168
+ const encrypted = encryptLicenseKey(config.licenseKey);
169
+ const diskConfig = {
170
+ serverUrl: config.serverUrl,
171
+ mode: config.mode,
172
+ machineId: config.machineId,
173
+ activatedAt: config.activatedAt,
174
+ tier: config.tier,
175
+ encryptedLicenseKey: encrypted,
176
+ };
177
+ writeSecureConfig(diskConfig);
178
+ }
179
+ //# sourceMappingURL=secure-config.js.map
@@ -1,4 +1,4 @@
1
- # 🧠 Neocortex v3.9.19 (Free) | OrNexus Team
1
+ # 🧠 Neocortex v3.9.22 (Free) | OrNexus Team
2
2
 
3
3
  This project uses Neocortex, a Development Orchestrator (Free).
4
4
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: neocortex
3
- description: "🧠 Neocortex v3.9.19 (Free) | OrNexus Team"
3
+ description: "🧠 Neocortex v3.9.22 (Free) | OrNexus Team"
4
4
  ---
5
5
 
6
6
  # Neocortex - Thin Client Interface
@@ -4,7 +4,7 @@ agent:
4
4
  name: 'Neocortex'
5
5
  title: 'Development Orchestrator (Free)'
6
6
  icon: '>'
7
- version: '3.9.19'
7
+ version: '3.9.22'
8
8
  architecture: 'thin-client'
9
9
  module: stand-alone
10
10
  hasSidecar: false
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: neocortex
3
- description: "🧠 Neocortex v3.9.19 (Free) | OrNexus Team"
3
+ description: "🧠 Neocortex v3.9.22 (Free) | OrNexus Team"
4
4
  model: opus
5
5
  color: blue
6
6
  tools:
@@ -56,7 +56,7 @@ SEMPRE que este agente for invocado, imprima o banner abaixo como PRIMEIRO outpu
56
56
  ┌────────────────────────────────────────────────────────────┐
57
57
  │ │
58
58
  │ ####### N E O C O R T E X │
59
- │ ### ######## v3.9.19
59
+ │ ### ######## v3.9.22
60
60
  │ ######### ##### │
61
61
  │ ## ############## Development Orchestrator │
62
62
  │ ## ### ###### ## OrNexus Team (Free) │
@@ -1,4 +1,4 @@
1
- # 🧠 Neocortex v3.9.19 (Free) | OrNexus Team
1
+ # 🧠 Neocortex v3.9.22 (Free) | OrNexus Team
2
2
 
3
3
  You are a Development Orchestrator (Free). All orchestration logic is delivered by the remote Neocortex server.
4
4
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: neocortex
3
- description: "🧠 Neocortex v3.9.19 (Free) | OrNexus Team"
3
+ description: "🧠 Neocortex v3.9.22 (Free) | OrNexus Team"
4
4
  model: fast
5
5
  readonly: false
6
6
  is_background: false
@@ -18,7 +18,7 @@ SEMPRE que este agente for invocado, imprima o banner abaixo como PRIMEIRO outpu
18
18
  ┌────────────────────────────────────────────────────────────┐
19
19
  │ │
20
20
  │ ####### N E O C O R T E X │
21
- │ ### ######## v3.9.19
21
+ │ ### ######## v3.9.22
22
22
  │ ######### ##### │
23
23
  │ ## ############## Development Orchestrator │
24
24
  │ ## ### ###### ## OrNexus Team (Free) │
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: neocortex
3
- description: "🧠 Neocortex v3.9.19 (Free) | OrNexus Team"
3
+ description: "🧠 Neocortex v3.9.22 (Free) | OrNexus Team"
4
4
  kind: local
5
5
  tools:
6
6
  - read_file
@@ -25,7 +25,7 @@ SEMPRE que este agente for invocado, imprima o banner abaixo como PRIMEIRO outpu
25
25
  ┌────────────────────────────────────────────────────────────┐
26
26
  │ │
27
27
  │ ####### N E O C O R T E X │
28
- │ ### ######## v3.9.19
28
+ │ ### ######## v3.9.22
29
29
  │ ######### ##### │
30
30
  │ ## ############## Development Orchestrator │
31
31
  │ ## ### ###### ## OrNexus Team (Free) │