fraim-hub 2.0.345 → 2.0.348

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.
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ownerArtifactRoot = void 0;
7
+ exports.ownerPath = ownerPath;
8
+ exports.validateOwner = validateOwner;
9
+ exports.ownerKey = ownerKey;
10
+ exports.assertSameOwner = assertSameOwner;
11
+ exports.assertLearningTargetPath = assertLearningTargetPath;
12
+ exports.resolveOwnerHome = resolveOwnerHome;
13
+ exports.ownerArtifactPath = ownerArtifactPath;
14
+ exports.configuredArtifactPath = configuredArtifactPath;
15
+ const node_fs_1 = __importDefault(require("node:fs"));
16
+ const node_path_1 = __importDefault(require("node:path"));
17
+ /** A recorded home path is immutable; moving storage requires a newly reviewed run. */
18
+ function ownerPath(root) {
19
+ const absolute = node_path_1.default.resolve(root).replace(/\\/g, '/');
20
+ return process.platform === 'win32' ? absolute.toLowerCase() : absolute;
21
+ }
22
+ function validateOwner(owner) {
23
+ if (!owner || !['Project', 'Manager', 'Org'].includes(owner.level)
24
+ || Object.keys(owner).some(key => !['level', 'path'].includes(key))
25
+ || typeof owner.path !== 'string' || !node_path_1.default.isAbsolute(owner.path)
26
+ || /[\x00-\x1f]/.test(owner.path) || owner.path !== ownerPath(owner.path)) {
27
+ throw new Error('owner-unresolved: a canonical absolute home path is required');
28
+ }
29
+ }
30
+ function ownerKey(owner) {
31
+ validateOwner(owner);
32
+ return JSON.stringify([owner.level, owner.path]);
33
+ }
34
+ function assertSameOwner(expected, actual) {
35
+ if (ownerKey(expected) !== ownerKey(actual))
36
+ throw new Error('owner-path-conflict: the approved home differs from the configured path');
37
+ }
38
+ /** Shared authored-family identity check. Handwritten entries can omit Domain metadata. */
39
+ function assertLearningTargetPath(ref, domain) {
40
+ const subject = ref.owner.level === 'Org' ? 'org' : ref.learningSubject;
41
+ if (!subject || !/^[a-zA-Z0-9@._+-]+$/.test(subject))
42
+ throw new Error('learning-subject-required');
43
+ const escaped = subject.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
44
+ const match = ref.relativePath.match(new RegExp(`^learnings/${escaped}-((?:[a-z][a-z-]*-)?)(preferences|mistake-patterns|validated-patterns|manager-coaching)(\\.cold)?\\.md$`));
45
+ if (!match || (domain !== undefined && match[1] !== (domain === 'global' ? '' : domain + '-')))
46
+ throw new Error('domain-target-mismatch: learning subject, domain and family must match its file');
47
+ }
48
+ /** Pure read: resolve existing configured homes without migration or identity configuration. */
49
+ function resolveOwnerHome(projectPath, level, options) {
50
+ const load = (relative) => options.runtimeModule(projectPath, relative);
51
+ const paths = load('core/utils/project-fraim-paths');
52
+ let home;
53
+ if (level === 'Project') {
54
+ const root = paths.getWorkspaceFraimPath(projectPath);
55
+ home = { root, artifactRoot: node_path_1.default.resolve(projectPath), contentRoot: node_path_1.default.join(root, 'personalized-employee'), requiresPublish: false, backend: 'project' };
56
+ }
57
+ else {
58
+ if (!['Manager', 'Org'].includes(level))
59
+ throw new Error('owner-unresolved: invalid owner level');
60
+ const configFile = node_path_1.default.join(paths.getUserFraimDirPath(), 'config.json');
61
+ let config;
62
+ try {
63
+ config = node_fs_1.default.existsSync(configFile) ? JSON.parse(node_fs_1.default.readFileSync(configFile, 'utf8')) : {};
64
+ }
65
+ catch {
66
+ throw new Error('home-unavailable: user configuration is invalid');
67
+ }
68
+ if (!config || typeof config !== 'object' || Array.isArray(config))
69
+ throw new Error('home-unavailable: user configuration is invalid');
70
+ const raw = level === 'Manager' ? config.managerStorage : config.organization;
71
+ if (raw !== undefined && (!raw || typeof raw !== 'object' || Array.isArray(raw)
72
+ || !['git', 'local-folder', 'fraim-cloud', 'single-machine'].includes(raw.backend)
73
+ || (raw.backend === 'git' && (typeof raw.gitUrl !== 'string' || !raw.gitUrl.trim()))
74
+ || (raw.backend === 'local-folder' && (typeof raw.localPath !== 'string' || !raw.localPath.trim())))) {
75
+ throw new Error('home-unavailable: invalid configured storage home');
76
+ }
77
+ const resolved = load('cli/utils/pack-home').resolvePackHome(level === 'Manager' ? 'manager' : 'org', { migrateLegacy: false });
78
+ home = { root: resolved.root, contentRoot: resolved.contentRoot, requiresPublish: resolved.requiresPublish, backend: resolved.backend, ...(resolved.gitUrl ? { gitUrl: resolved.gitUrl } : {}) };
79
+ }
80
+ const owner = { level, path: ownerPath(home.root) };
81
+ if (options.expectedOwner)
82
+ assertSameOwner(options.expectedOwner, owner);
83
+ if (!node_fs_1.default.existsSync(home.root) || !node_fs_1.default.statSync(home.root).isDirectory()
84
+ || (home.backend === 'git' && !node_fs_1.default.existsSync(node_path_1.default.join(home.root, '.git')))) {
85
+ throw new Error('home-unavailable: configured home is not available locally');
86
+ }
87
+ return { ...home, level, owner };
88
+ }
89
+ /** Resolve a relative artifact without permitting lexical or realpath escapes, including junctions. */
90
+ function ownerArtifactPath(home, relativePath) {
91
+ assertSameOwner(home.owner, { level: home.level, path: ownerPath(home.root) });
92
+ if (!relativePath || node_path_1.default.isAbsolute(relativePath) || relativePath.includes('\\')
93
+ || relativePath.includes(':') || relativePath.split('/').some(part => !part || part === '.' || part === '..')) {
94
+ throw new Error('invalid-artifact-path');
95
+ }
96
+ // Shared project capabilities live in the repository registry, outside its personalized pack.
97
+ const shared = home.level === 'Project' && relativePath.startsWith('registry/');
98
+ const containmentRoot = shared ? node_path_1.default.dirname(home.root) : home.root;
99
+ // The workspace configuration has one explicit reference; arbitrary parent paths stay forbidden.
100
+ const workspaceConfig = home.level === 'Project' && relativePath === 'config.json';
101
+ const contentRoot = shared ? containmentRoot : workspaceConfig ? home.root : home.contentRoot;
102
+ const target = node_path_1.default.resolve(contentRoot, relativePath);
103
+ const existingAncestor = (file) => {
104
+ let current = file;
105
+ while (!node_fs_1.default.existsSync(current)) {
106
+ const parent = node_path_1.default.dirname(current);
107
+ if (parent === current)
108
+ throw new Error('home-unavailable');
109
+ current = parent;
110
+ }
111
+ return current;
112
+ };
113
+ const realRoot = node_fs_1.default.realpathSync(containmentRoot);
114
+ const ancestor = node_fs_1.default.realpathSync(existingAncestor(target));
115
+ const relative = node_path_1.default.relative(realRoot, ancestor);
116
+ if (relative === '..' || relative.startsWith('..' + node_path_1.default.sep) || node_path_1.default.isAbsolute(relative))
117
+ throw new Error('invalid-artifact-path: escaped owner home');
118
+ // Also reject escaping a content root via a symlink to a sibling in the pack.
119
+ if (node_fs_1.default.existsSync(contentRoot)) {
120
+ const contentRelative = node_path_1.default.relative(node_fs_1.default.realpathSync(contentRoot), ancestor);
121
+ if (contentRelative === '..' || contentRelative.startsWith('..' + node_path_1.default.sep) || node_path_1.default.isAbsolute(contentRelative))
122
+ throw new Error('invalid-artifact-path: escaped content root');
123
+ }
124
+ return target;
125
+ }
126
+ /** Explicit artifact base; never infer a project from its personalized directory. */
127
+ const ownerArtifactRoot = (home) => home.artifactRoot ?? home.root;
128
+ exports.ownerArtifactRoot = ownerArtifactRoot;
129
+ /** Configured artifacts may sit outside the personalized pack, but never outside their owner. */
130
+ function configuredArtifactPath(home, file) {
131
+ assertSameOwner(home.owner, { level: home.level, path: ownerPath(home.root) });
132
+ const root = node_path_1.default.resolve((0, exports.ownerArtifactRoot)(home));
133
+ const target = node_path_1.default.resolve(file);
134
+ const relative = node_path_1.default.relative(root, target);
135
+ if (!relative || relative === '..' || relative.startsWith('..' + node_path_1.default.sep) || node_path_1.default.isAbsolute(relative))
136
+ throw new Error('invalid-artifact-path: escaped owner home');
137
+ for (let current = target; ownerPath(current) !== ownerPath(root); current = node_path_1.default.dirname(current)) {
138
+ try {
139
+ if (node_fs_1.default.lstatSync(current).isSymbolicLink())
140
+ throw new Error('invalid-artifact-path: linked artifact');
141
+ }
142
+ catch (error) {
143
+ if (error.code !== 'ENOENT')
144
+ throw error;
145
+ }
146
+ }
147
+ return target;
148
+ }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.hashContent = void 0;
7
+ exports.atomicWrite = atomicWrite;
8
+ exports.withFileLock = withFileLock;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const node_crypto_1 = require("node:crypto");
12
+ const node_os_1 = require("node:os");
13
+ const hashContent = (text) => (0, node_crypto_1.createHash)('sha256').update(text).digest('hex');
14
+ exports.hashContent = hashContent;
15
+ function atomicWrite(file, text) {
16
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
17
+ const temporary = file + '.' + (0, node_crypto_1.randomUUID)() + '.tmp';
18
+ try {
19
+ node_fs_1.default.writeFileSync(temporary, text, { encoding: 'utf8', flag: 'wx' });
20
+ const fd = node_fs_1.default.openSync(temporary, 'r+');
21
+ try {
22
+ node_fs_1.default.fsyncSync(fd);
23
+ }
24
+ finally {
25
+ node_fs_1.default.closeSync(fd);
26
+ }
27
+ node_fs_1.default.renameSync(temporary, file);
28
+ }
29
+ finally {
30
+ if (node_fs_1.default.existsSync(temporary))
31
+ node_fs_1.default.unlinkSync(temporary);
32
+ }
33
+ }
34
+ function withFileLock(file, fn) {
35
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
36
+ let fd;
37
+ try {
38
+ fd = node_fs_1.default.openSync(file + '.lock', 'wx');
39
+ }
40
+ catch {
41
+ // Serialize recovery so two contenders cannot unlink each other's new lock.
42
+ let recovery;
43
+ try {
44
+ recovery = node_fs_1.default.openSync(file + '.lock.recovery', 'wx');
45
+ }
46
+ catch {
47
+ throw new Error('busy: lock recovery is already in progress');
48
+ }
49
+ try {
50
+ const owner = JSON.parse(node_fs_1.default.readFileSync(file + '.lock', 'utf8'));
51
+ let stale = false;
52
+ if (owner.host === (0, node_os_1.hostname)() && Number.isInteger(owner.pid) && owner.pid > 0) {
53
+ try {
54
+ process.kill(owner.pid, 0);
55
+ }
56
+ catch (error) {
57
+ stale = error.code === 'ESRCH';
58
+ }
59
+ }
60
+ if (!stale)
61
+ throw new Error('busy');
62
+ node_fs_1.default.unlinkSync(file + '.lock');
63
+ fd = node_fs_1.default.openSync(file + '.lock', 'wx');
64
+ }
65
+ catch {
66
+ throw new Error('busy: reconcile outstanding lock before retry');
67
+ }
68
+ finally {
69
+ node_fs_1.default.closeSync(recovery);
70
+ node_fs_1.default.unlinkSync(file + '.lock.recovery');
71
+ }
72
+ }
73
+ node_fs_1.default.writeFileSync(fd, JSON.stringify({ pid: process.pid, host: (0, node_os_1.hostname)(), at: new Date().toISOString() }));
74
+ try {
75
+ return fn();
76
+ }
77
+ finally {
78
+ node_fs_1.default.closeSync(fd);
79
+ node_fs_1.default.unlinkSync(file + '.lock');
80
+ }
81
+ }
@@ -31,6 +31,7 @@ exports.buildDirectStartPlan = buildDirectStartPlan;
31
31
  exports.buildDirectContinuePlan = buildDirectContinuePlan;
32
32
  exports.parseHostLine = parseHostLine;
33
33
  exports.detectCodexCompactTaskFailure = detectCodexCompactTaskFailure;
34
+ exports.wireHostProcess = wireHostProcess;
34
35
  exports.describeSpawnError = describeSpawnError;
35
36
  exports.hostDefaultUnsetEnv = hostDefaultUnsetEnv;
36
37
  exports.buildHostSpawnEnv = buildHostSpawnEnv;
@@ -2398,6 +2399,32 @@ function detectCodexCompactTaskFailure(line) {
2398
2399
  return null;
2399
2400
  }
2400
2401
  function wireHostProcess(hostId, child, handlers, spawnCwd) {
2402
+ // Exec's JSON error has no auth code. Require independent provider stderr
2403
+ // provenance AND the same terminal protocol error; never scan tool/prose text.
2404
+ const codexRefreshFailure = 'Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.';
2405
+ let codexAuthDiagnostic = false;
2406
+ let codexTerminalAuthFailure = false;
2407
+ const observeCodexAuth = (line, channel) => {
2408
+ if (hostId !== 'codex')
2409
+ return;
2410
+ if (channel === 'stderr') {
2411
+ const clean = line.replace(/\x1b\[[0-9;]*m/g, '').trim();
2412
+ const diagnostic = clean.match(/^(?:\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+)?ERROR\s+codex_login::auth::manager:\s+Failed to refresh token: (.+)$/);
2413
+ if (diagnostic?.[1] === codexRefreshFailure)
2414
+ codexAuthDiagnostic = true;
2415
+ return;
2416
+ }
2417
+ try {
2418
+ const event = JSON.parse(line);
2419
+ if (event.type === 'turn.failed' || event.type === 'error') {
2420
+ codexTerminalAuthFailure = (event.type === 'turn.failed' ? event.error?.message : event.message) === codexRefreshFailure;
2421
+ }
2422
+ else if (event.type === 'turn.started' || event.type === 'turn.completed') {
2423
+ codexTerminalAuthFailure = false;
2424
+ }
2425
+ }
2426
+ catch { /* Plain stdout cannot establish authentication failure. */ }
2427
+ };
2401
2428
  const wire = (buffer, channel) => {
2402
2429
  let pending = '';
2403
2430
  let suppressCodexModelRefreshStderr = false;
@@ -2406,6 +2433,7 @@ function wireHostProcess(hostId, child, handlers, spawnCwd) {
2406
2433
  const lines = pending.split(/\r?\n/);
2407
2434
  pending = lines.pop() || '';
2408
2435
  for (const line of lines) {
2436
+ observeCodexAuth(line, channel);
2409
2437
  if (hostId === 'codex' && channel === 'stderr') {
2410
2438
  if (line.includes('codex_models_manager::manager: failed to refresh available models:')) {
2411
2439
  suppressCodexModelRefreshStderr = true;
@@ -2431,6 +2459,7 @@ function wireHostProcess(hostId, child, handlers, spawnCwd) {
2431
2459
  });
2432
2460
  buffer.on('end', () => {
2433
2461
  if (pending.trim().length > 0) {
2462
+ observeCodexAuth(pending, channel);
2434
2463
  const parsed = parseHostLine(hostId, pending);
2435
2464
  if (parsed.message || parsed.sessionId || parsed.raw) {
2436
2465
  handlers.onEvent(parsed, channel);
@@ -2444,7 +2473,12 @@ function wireHostProcess(hostId, child, handlers, spawnCwd) {
2444
2473
  // signal='SIGSEGV') are NOT treated as clean exits by classifyExit's
2445
2474
  // `exitCode ?? 0` guard. Pass -1 for any signal-terminated child so the
2446
2475
  // non-zero branch fires and recovery is attempted.
2447
- child.on('close', (code, signal) => handlers.onExit(signal ? -1 : code));
2476
+ child.on('close', (code, signal) => {
2477
+ if (!signal && code !== null && code !== 0 && codexAuthDiagnostic && codexTerminalAuthFailure) {
2478
+ handlers.onEvent({ hostAuthenticationRequired: true, hostError: codexRefreshFailure, raw: codexRefreshFailure }, 'system');
2479
+ }
2480
+ handlers.onExit(signal ? -1 : code);
2481
+ });
2448
2482
  // Issue #1327: without this listener, an OS-level spawn failure (EAGAIN,
2449
2483
  // ENOENT, EACCES) fires an 'error' event on the ChildProcess. An unhandled
2450
2484
  // EventEmitter 'error' throws synchronously and crashes the Hub process.
@@ -10,6 +10,7 @@ exports.resolveHubAppCacheDir = resolveHubAppCacheDir;
10
10
  exports.isHubAppCacheComplete = isHubAppCacheComplete;
11
11
  exports.listCachedHubAppVersions = listCachedHubAppVersions;
12
12
  exports.runNpmInstall = runNpmInstall;
13
+ exports.pruneHubAppCache = pruneHubAppCache;
13
14
  exports.ensureMaterializedHubApp = ensureMaterializedHubApp;
14
15
  exports.resolveHubAppEntry = resolveHubAppEntry;
15
16
  const fs_1 = __importDefault(require("fs"));
@@ -209,6 +210,24 @@ const defaultInstallPackage = async (version, destDir) => {
209
210
  }
210
211
  fs_1.default.rmSync(installedRoot, { recursive: true, force: true });
211
212
  };
213
+ const HUB_APP_CACHE_MAX_VERSIONS = 3;
214
+ /** Prune the hub-app cache to at most HUB_APP_CACHE_MAX_VERSIONS entries, always preserving keepVersion. Non-fatal. */
215
+ function pruneHubAppCache(fraimDir, keepVersion) {
216
+ try {
217
+ const all = listCachedHubAppVersions(fraimDir);
218
+ const sorted = all.map((e) => e.version).sort((a, b) => (semver_1.default.gt(a, b) ? -1 : 1));
219
+ const toKeep = new Set(sorted.slice(0, HUB_APP_CACHE_MAX_VERSIONS));
220
+ toKeep.add(keepVersion);
221
+ for (const v of sorted) {
222
+ if (!toKeep.has(v)) {
223
+ fs_1.default.rmSync(resolveHubAppCacheDir(v, fraimDir), { recursive: true, force: true });
224
+ }
225
+ }
226
+ }
227
+ catch {
228
+ // Prune failure must never break a launch.
229
+ }
230
+ }
212
231
  /**
213
232
  * Return the entry path for `version`, materializing it first if it is not already a complete
214
233
  * cache entry. Returns immediately (no npm invocation) when a complete entry already exists.
@@ -248,6 +267,7 @@ async function ensureMaterializedHubApp(version, options = {}) {
248
267
  finally {
249
268
  fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
250
269
  }
270
+ pruneHubAppCache(fraimDir, version);
251
271
  return entryPath;
252
272
  }
253
273
  /**
@@ -2596,7 +2596,9 @@ function summarizeHostErrorForThread(error) {
2596
2596
  // noise that the system handled on its own.
2597
2597
  function pushTerminalFailureToThread(run, exitCode) {
2598
2598
  if (exitCode !== 0 && run.lastHostError) {
2599
- run.messages.push((0, hosts_1.createHubMessage)('system', `This run stopped: ${summarizeHostErrorForThread(run.lastHostError)}`));
2599
+ const detail = detectLoginFailureSignature(run, exitCode)
2600
+ ? run.lastHostError : summarizeHostErrorForThread(run.lastHostError);
2601
+ run.messages.push((0, hosts_1.createHubMessage)('system', `This run stopped: ${detail}`));
2600
2602
  }
2601
2603
  }
2602
2604
  function createHubRecoveryEvent(run, exitCode, attempt) {
@@ -3017,6 +3019,7 @@ class AiHubServer {
3017
3019
  this.latestVersionPollTimer = null;
3018
3020
  this.catalogSyncChild = null;
3019
3021
  this.attachmentPolicy = (0, attachment_store_1.attachmentPolicy)();
3022
+ this.configureAgentSurface = options.configureAgentSurface ?? configureFraimForHubAgent;
3020
3023
  this.openTerminal = options.openTerminal ?? hubOpenTerminal;
3021
3024
  this.preferencesStore = options.preferencesStore || new preferences_1.AiHubPreferencesStore();
3022
3025
  const launchResolution = options.projectPath
@@ -4254,7 +4257,10 @@ class AiHubServer {
4254
4257
  const option = hubAgentOption(run.hostId);
4255
4258
  run.status = 'failed';
4256
4259
  run.pauseReason = 'awaiting_user';
4257
- run.loginRequired = option ? { agentLabel: option.label } : undefined;
4260
+ run.loginRequired = option ? {
4261
+ agentLabel: option.label,
4262
+ ...(run.hostId === 'codex' && run.lastHostError ? { remediation: run.lastHostError } : {}),
4263
+ } : undefined;
4258
4264
  }
4259
4265
  if (run.status !== 'running') {
4260
4266
  run.eventLogRefs = (run.eventLogRefs || []).map((ref) => {
@@ -5856,6 +5862,7 @@ class AiHubServer {
5856
5862
  }
5857
5863
  try {
5858
5864
  const result = (0, learning_context_builder_1.applyLearningEntryChange)(resolveProjectPath(req), userEmail, ref, b.action, {
5865
+ originalId: b.originalId,
5859
5866
  originalTitle: b.originalTitle,
5860
5867
  severity: b.severity,
5861
5868
  title: b.title,
@@ -7012,7 +7019,7 @@ class AiHubServer {
7012
7019
  return res.json({ ok: true, configured: [], message: 'No ready CLI agents to configure.' });
7013
7020
  }
7014
7021
  const results = await Promise.all(readyIds.map(async (hubId) => {
7015
- const result = await configureFraimForHubAgent(hubId);
7022
+ const result = await this.configureAgentSurface(hubId);
7016
7023
  return { hubId, ...result };
7017
7024
  }));
7018
7025
  const failed = results.filter((r) => !r.configured);