fraim-hub 2.0.303 → 2.0.305
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 +2 -3
- package/dist/src/ai-hub/cert-store.js +54 -0
- package/dist/src/ai-hub/cli.js +30 -1
- package/dist/src/ai-hub/desktop-main.js +37 -18
- package/dist/src/ai-hub/hosts.js +80 -9
- package/dist/src/ai-hub/office-setup-policy.js +33 -0
- package/dist/src/ai-hub/server.js +212 -43
- package/dist/src/config/persona-capability-bundles.js +10 -10
- package/dist/src/core/quality-evidence.js +2 -2
- package/package.json +2 -2
- package/public/ai-hub/index.html +48 -0
- package/public/ai-hub/script.js +380 -30
- package/public/ai-hub/styles.css +39 -5
- package/public/first-run/error-frame.js +1 -1
- package/public/first-run/script.js +7 -4
- package/public/first-run/styles.css +6 -5
- package/public/portfolio/careena.html +4 -4
- package/public/portfolio/qasm.html +2 -2
- package/public/portfolio/sechar.html +5 -5
package/README.md
CHANGED
|
@@ -313,9 +313,8 @@ Use these defaults:
|
|
|
313
313
|
- `technical-design` after the spec is approved and you need the implementation plan, file touchpoints, and risk handling.
|
|
314
314
|
- `feature-implementation` for code changes, bug fixes, and documentation updates that should be executed and validated.
|
|
315
315
|
- `test-authoring` when you need reproduction coverage, missing tests, or stronger regression protection before implementation.
|
|
316
|
-
- `
|
|
317
|
-
- `implementation-
|
|
318
|
-
- `implementation-design-review` when you need to verify the code matches the approved technical design.
|
|
316
|
+
- `iterative-quality-improvement` or `ui-polish-validation` after user-facing UI changes or when the ask is explicitly browser validation.
|
|
317
|
+
- `independent-implementation-review` when you need an independent sign-off that the delivered behavior matches both the approved technical design and the feature spec.
|
|
319
318
|
- `issue-retrospective` after the work is complete and you want durable learnings captured.
|
|
320
319
|
|
|
321
320
|
Typical path for a larger feature:
|
|
@@ -6,9 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.certPaths = certPaths;
|
|
7
7
|
exports.loadOrCreateCert = loadOrCreateCert;
|
|
8
8
|
exports.trustCert = trustCert;
|
|
9
|
+
exports.untrustCert = untrustCert;
|
|
10
|
+
exports.computeCertFingerprint = computeCertFingerprint;
|
|
11
|
+
exports.isCertTrusted = isCertTrusted;
|
|
9
12
|
const fs_1 = __importDefault(require("fs"));
|
|
10
13
|
const path_1 = __importDefault(require("path"));
|
|
11
14
|
const os_1 = __importDefault(require("os"));
|
|
15
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
12
16
|
const child_process_1 = require("child_process");
|
|
13
17
|
function certDir() {
|
|
14
18
|
const base = process.env.APPDATA ||
|
|
@@ -66,3 +70,53 @@ function trustCert(certPath) {
|
|
|
66
70
|
(0, child_process_1.spawnSync)('security', ['add-trusted-cert', '-r', 'trustRoot', '-k', keychain, certPath], { stdio: 'ignore' });
|
|
67
71
|
}
|
|
68
72
|
}
|
|
73
|
+
/** Removes a specific cert from the trust store, by fingerprint. Test-only helper. */
|
|
74
|
+
function untrustCert(fingerprint) {
|
|
75
|
+
if (process.platform === 'win32') {
|
|
76
|
+
(0, child_process_1.spawnSync)('certutil', ['-delstore', '-user', 'Root', fingerprint], { encoding: 'utf8' });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (process.platform === 'darwin') {
|
|
80
|
+
(0, child_process_1.spawnSync)('security', ['delete-certificate', '-Z', fingerprint], { stdio: 'ignore' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* SHA-1 fingerprint of an X.509 cert's DER bytes, in the lowercase hex form
|
|
85
|
+
* both `certutil -store` (Windows) and `security find-certificate -Z` (macOS)
|
|
86
|
+
* print, so it can be searched for directly in their output.
|
|
87
|
+
*/
|
|
88
|
+
function computeCertFingerprint(pemCert) {
|
|
89
|
+
const der = Buffer.from(pemCert
|
|
90
|
+
.replace('-----BEGIN CERTIFICATE-----', '')
|
|
91
|
+
.replace('-----END CERTIFICATE-----', '')
|
|
92
|
+
.replace(/\s+/g, ''), 'base64');
|
|
93
|
+
return crypto_1.default.createHash('sha1').update(der).digest('hex');
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Live check: is this exact cert (by fingerprint, not just "a localhost cert")
|
|
97
|
+
* already present and trusted in the OS root store? Issue #1571: replaces a
|
|
98
|
+
* flag-file-based "did we already do this" record, which can silently drift
|
|
99
|
+
* from reality (a user or policy tool removes the cert from the trust store,
|
|
100
|
+
* a profile restore, etc.) with no way to recover once it does. This mirrors
|
|
101
|
+
* office-sideload.ts's isSideloaded(), which already queries the real
|
|
102
|
+
* registry/filesystem live rather than trusting a flag alone.
|
|
103
|
+
*/
|
|
104
|
+
function isCertTrusted(certPath) {
|
|
105
|
+
if (!fs_1.default.existsSync(certPath))
|
|
106
|
+
return false;
|
|
107
|
+
const fingerprint = computeCertFingerprint(fs_1.default.readFileSync(certPath, 'utf8'));
|
|
108
|
+
if (process.platform === 'win32') {
|
|
109
|
+
const result = (0, child_process_1.spawnSync)('certutil', ['-store', '-user', 'Root'], { encoding: 'utf8' });
|
|
110
|
+
if (result.status !== 0)
|
|
111
|
+
return false;
|
|
112
|
+
return result.stdout.toLowerCase().includes(fingerprint);
|
|
113
|
+
}
|
|
114
|
+
if (process.platform === 'darwin') {
|
|
115
|
+
const keychain = path_1.default.join(os_1.default.homedir(), 'Library', 'Keychains', 'login.keychain-db');
|
|
116
|
+
const result = (0, child_process_1.spawnSync)('security', ['find-certificate', '-a', '-c', 'localhost', '-Z', keychain], { encoding: 'utf8' });
|
|
117
|
+
if (result.status !== 0)
|
|
118
|
+
return false;
|
|
119
|
+
return result.stdout.toLowerCase().replace(/:/g, '').includes(fingerprint);
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
package/dist/src/ai-hub/cli.js
CHANGED
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.DESKTOP_HUB_READY_TIMEOUT_ENV = void 0;
|
|
40
40
|
exports.resolveDesktopReadyTimeoutMs = resolveDesktopReadyTimeoutMs;
|
|
41
41
|
exports.waitForDesktopHubReady = waitForDesktopHubReady;
|
|
42
|
+
exports.resolveLaunchProjectPath = resolveLaunchProjectPath;
|
|
42
43
|
exports.runHub = runHub;
|
|
43
44
|
// Hub-owned launcher used by the fraim-hub package. Keep this outside
|
|
44
45
|
// src/cli/commands so the core fraim package has no Hub command implementation.
|
|
@@ -203,10 +204,38 @@ async function waitForDesktopHubReady(child, preferredPort, options = {}) {
|
|
|
203
204
|
+ `${stillRunning}`
|
|
204
205
|
+ ` Set ${exports.DESKTOP_HUB_READY_TIMEOUT_ENV} to a larger value if this machine needs longer.`);
|
|
205
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Issue #1253: which project this launch is for, decided at the launch boundary.
|
|
209
|
+
*
|
|
210
|
+
* cwd affinity lives HERE rather than in the server, and the placement is the point.
|
|
211
|
+
* `resolveHubProjectPath` in server.ts has no access to a launch directory at all, so
|
|
212
|
+
* the #866 R2 guarantee — the server never substitutes `process.cwd()` — stays
|
|
213
|
+
* literally true rather than true by convention. What changes is only what the launch
|
|
214
|
+
* surface may hand it: an invocation directory that is already a FRAIM project is an
|
|
215
|
+
* intentional project and may be adopted; an uninitialized one never becomes one.
|
|
216
|
+
*
|
|
217
|
+
* An explicit `--project-path` is passed through verbatim. The server gates it (it is
|
|
218
|
+
* recorded only when it is a real FRAIM project), so the CLI does not second-guess a
|
|
219
|
+
* path the user typed.
|
|
220
|
+
*
|
|
221
|
+
* Exported for tests: this is the whole cwd-eligibility rule, and it is a pure
|
|
222
|
+
* function of (flag, cwd).
|
|
223
|
+
*/
|
|
224
|
+
function resolveLaunchProjectPath(explicitProjectPath, cwd = process.cwd()) {
|
|
225
|
+
if (explicitProjectPath)
|
|
226
|
+
return path_1.default.resolve(explicitProjectPath);
|
|
227
|
+
return (0, project_fraim_paths_1.workspaceFraimExists)(cwd) ? path_1.default.resolve(cwd) : undefined;
|
|
228
|
+
}
|
|
206
229
|
async function runHub(options) {
|
|
207
230
|
const { AiHubServer, findAvailablePort } = await Promise.resolve().then(() => __importStar(require('./server')));
|
|
208
231
|
const preferredPort = options.port || (0, git_utils_1.getPort)() + 100;
|
|
209
|
-
|
|
232
|
+
// #1253: covers the desktop child and the in-process server alike — both already
|
|
233
|
+
// read this one local.
|
|
234
|
+
const projectPath = resolveLaunchProjectPath(options.projectPath);
|
|
235
|
+
if (projectPath && !options.projectPath) {
|
|
236
|
+
// A surprising project selection must be traceable to the launch directory.
|
|
237
|
+
console.log(`[ai-hub] launch project adopted from cwd: ${projectPath}`);
|
|
238
|
+
}
|
|
210
239
|
const runtimeId = options.runtimeId || process.env.FRAIM_HUB_RUNTIME_ID || 'hub';
|
|
211
240
|
if (options.open) {
|
|
212
241
|
const wantDesktop = !options.browser;
|
|
@@ -11,6 +11,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
11
11
|
const server_1 = require("./server");
|
|
12
12
|
const cert_store_1 = require("./cert-store");
|
|
13
13
|
const office_sideload_1 = require("./office-sideload");
|
|
14
|
+
const office_setup_policy_1 = require("./office-setup-policy");
|
|
14
15
|
const remote_hub_gateway_1 = require("./remote-hub-gateway");
|
|
15
16
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
16
17
|
const version_utils_1 = require("../cli/utils/version-utils");
|
|
@@ -214,6 +215,28 @@ function checkForRelaunchAttemptUpdate() {
|
|
|
214
215
|
// ---------------------------------------------------------------------------
|
|
215
216
|
// Word manifest sideload (runs once on first launch)
|
|
216
217
|
// ---------------------------------------------------------------------------
|
|
218
|
+
// Trusting the loopback cert prompts a one-time OS security dialog (Windows
|
|
219
|
+
// "certutil" store-write, macOS Touch ID/password for keychain trust), so it
|
|
220
|
+
// must not run on every launch. Issue #1571 PR feedback: rather than record
|
|
221
|
+
// "already done" in a flag file that can drift from reality (the cert gets
|
|
222
|
+
// removed from the OS trust store by a user, a policy tool, a profile
|
|
223
|
+
// restore, with no way to recover once the flag lies), check the OS trust
|
|
224
|
+
// store live via isCertTrusted() - the same live-check pattern office-sideload.ts's
|
|
225
|
+
// isSideloaded() already uses instead of trusting a flag alone.
|
|
226
|
+
function ensureCertTrustedOnce(certPath) {
|
|
227
|
+
if ((0, cert_store_1.isCertTrusted)(certPath))
|
|
228
|
+
return;
|
|
229
|
+
try {
|
|
230
|
+
(0, cert_store_1.trustCert)(certPath);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
console.warn('[fraim] could not trust localhost certificate:', err);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Issue: Word add-in sideload must run once on initial install and then just be
|
|
237
|
+
// checked (not re-run, not re-prompted) on every later Hub launch. isSideloaded()
|
|
238
|
+
// plus the version+port flag below make this idempotent: unchanged state returns
|
|
239
|
+
// immediately with no registry write and no dialog.
|
|
217
240
|
function ensureWordSideload(projectPath, httpsPort) {
|
|
218
241
|
// Flag version bump: bump this string when new manifests are added so all
|
|
219
242
|
// users get re-sideloaded on their next launch.
|
|
@@ -238,12 +261,12 @@ function ensureWordSideload(projectPath, httpsPort) {
|
|
|
238
261
|
fs_1.default.writeFileSync(flagPath, expectedFlag);
|
|
239
262
|
}
|
|
240
263
|
function shouldConfigureOfficeSideload() {
|
|
241
|
-
// Office
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
264
|
+
// Office setup (cert trust + manifest sideload) runs on every real Hub
|
|
265
|
+
// launch, but ensureCertTrustedOnce() and ensureWordSideload() are each
|
|
266
|
+
// internally idempotent, so in steady state neither does any work and no
|
|
267
|
+
// dialog reappears. Decision logic (env var + platform gating) lives in
|
|
268
|
+
// shouldRunOfficeSetup() (office-setup-policy.ts) — see #1571.
|
|
269
|
+
return (0, office_setup_policy_1.shouldRunOfficeSetup)(process.env, process.platform);
|
|
247
270
|
}
|
|
248
271
|
// ---------------------------------------------------------------------------
|
|
249
272
|
// Tray setup
|
|
@@ -492,18 +515,14 @@ async function launchDesktopShell(options) {
|
|
|
492
515
|
catch (err) {
|
|
493
516
|
console.warn('[fraim] could not write hub runtime file:', err);
|
|
494
517
|
}
|
|
495
|
-
// #1110: trusting the loopback cert in the OS store is
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
catch (err) {
|
|
504
|
-
console.warn('[fraim] could not trust localhost certificate:', err);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
518
|
+
// #1110: trusting the loopback cert in the OS store is only needed so Word
|
|
519
|
+
// *Online* will render the task pane over HTTPS, and the underlying `certutil`/
|
|
520
|
+
// `security` call can prompt an OS trust dialog (Touch ID/password on macOS).
|
|
521
|
+
// ensureCertTrustedOnce() runs this exactly once per cert, not on every launch,
|
|
522
|
+
// so the one-time Office setup happens at initial install and is just checked
|
|
523
|
+
// (no dialog, no re-run) on every later Hub launch.
|
|
524
|
+
if (shouldConfigureOfficeSideload())
|
|
525
|
+
ensureCertTrustedOnce((0, cert_store_1.certPaths)().certPath);
|
|
507
526
|
if (shouldConfigureOfficeSideload())
|
|
508
527
|
ensureWordSideload(resolvedProjectPath, httpsPort);
|
|
509
528
|
const hubUrl = `http://127.0.0.1:${httpPort}/ai-hub/`;
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -2250,9 +2250,19 @@ function normalizeGeminiPromptForMatch(value) {
|
|
|
2250
2250
|
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
2251
2251
|
}
|
|
2252
2252
|
class CliHostRuntime {
|
|
2253
|
-
constructor(spawn = spawnHostProcess, killTree = (pid, signal) => (0, tree_kill_1.default)(pid, signal)
|
|
2253
|
+
constructor(spawn = spawnHostProcess, killTree = (pid, signal) => (0, tree_kill_1.default)(pid, signal),
|
|
2254
|
+
// Issue #1570: a redirect's kill was previously fire-and-forget with no
|
|
2255
|
+
// ceiling (see `killActiveChildWithEscalation`). Configurable (not just a
|
|
2256
|
+
// module constant) so tests can prove the retry/escalation behavior in
|
|
2257
|
+
// milliseconds instead of the real 5s production window.
|
|
2258
|
+
killEscalationTimeoutMs = 5000,
|
|
2259
|
+
// Issue #1583: the initial SIGTERM counts as attempt 1; retries escalate
|
|
2260
|
+
// to SIGKILL until this cap is exhausted.
|
|
2261
|
+
maxKillEscalationAttempts = 10) {
|
|
2254
2262
|
this.spawn = spawn;
|
|
2255
2263
|
this.killTree = killTree;
|
|
2264
|
+
this.killEscalationTimeoutMs = killEscalationTimeoutMs;
|
|
2265
|
+
this.maxKillEscalationAttempts = maxKillEscalationAttempts;
|
|
2256
2266
|
// Issue #1176: active continuation runs keyed by `${hostId}::${sessionId}` (R1,
|
|
2257
2267
|
// R13, R14). Lives on this long-lived singleton instance, in-memory only (R15).
|
|
2258
2268
|
// `spawn`/`killTree` are constructor-injectable so tests can prove queueing,
|
|
@@ -2307,6 +2317,8 @@ class CliHostRuntime {
|
|
|
2307
2317
|
const active = this.activeContinueRuns.get(key);
|
|
2308
2318
|
if (!active)
|
|
2309
2319
|
return false;
|
|
2320
|
+
if (active.escalationTimer != null)
|
|
2321
|
+
clearTimeout(active.escalationTimer);
|
|
2310
2322
|
active.pending.splice(0);
|
|
2311
2323
|
this.activeContinueRuns.delete(key);
|
|
2312
2324
|
if (active.child.pid == null)
|
|
@@ -2337,12 +2349,7 @@ class CliHostRuntime {
|
|
|
2337
2349
|
if (entry.deliveryIntent === 'stop') {
|
|
2338
2350
|
active.pending.unshift(entry);
|
|
2339
2351
|
if (active.child.pid != null) {
|
|
2340
|
-
|
|
2341
|
-
this.killTree(active.child.pid, 'SIGTERM');
|
|
2342
|
-
}
|
|
2343
|
-
catch {
|
|
2344
|
-
// Best-effort: `close` still fires on normal process exit either way.
|
|
2345
|
-
}
|
|
2352
|
+
this.killActiveChildWithEscalation(key, active);
|
|
2346
2353
|
}
|
|
2347
2354
|
}
|
|
2348
2355
|
else {
|
|
@@ -2357,11 +2364,75 @@ class CliHostRuntime {
|
|
|
2357
2364
|
spawnAndRegister(key, hostId, sessionId, entry) {
|
|
2358
2365
|
const plan = (0, configured_agents_1.decorateHostPlanWithConfiguredAgent)(entry.direct ? buildDirectContinuePlan(hostId, sessionId, entry.message) : buildContinuePlan(hostId, sessionId, entry.message), entry.launchContext?.agent, entry.launchContext?.env);
|
|
2359
2366
|
const child = this.spawn(hostId, plan, entry.projectPath, entry.handlers);
|
|
2360
|
-
const runEntry = { child, pending: [] };
|
|
2367
|
+
const runEntry = { child, pending: [], handlers: entry.handlers };
|
|
2361
2368
|
this.activeContinueRuns.set(key, runEntry);
|
|
2362
|
-
child.once('close', () =>
|
|
2369
|
+
child.once('close', () => {
|
|
2370
|
+
// Issue #1570: a redirect kill that eventually takes (just slower than
|
|
2371
|
+
// the escalation window) must not leave its retry timer dangling past
|
|
2372
|
+
// this point — the process it was trying to kill is already gone.
|
|
2373
|
+
if (runEntry.escalationTimer != null) {
|
|
2374
|
+
clearTimeout(runEntry.escalationTimer);
|
|
2375
|
+
runEntry.escalationTimer = undefined;
|
|
2376
|
+
}
|
|
2377
|
+
this.dequeueNext(key, hostId, sessionId, runEntry);
|
|
2378
|
+
});
|
|
2363
2379
|
return child;
|
|
2364
2380
|
}
|
|
2381
|
+
// Issue #1570 (Defect 1): the redirect kill was previously fire-and-forget
|
|
2382
|
+
// — `killTree` was called once, with no way to know whether it actually
|
|
2383
|
+
// took, and no ceiling on how long the queued correction could sit behind
|
|
2384
|
+
// a stale turn if it silently didn't (a resistant/reparented descendant, a
|
|
2385
|
+
// stuck `taskkill /T`). This bounds the wait: if the active child's `close`
|
|
2386
|
+
// event hasn't fired within `killEscalationTimeoutMs`, retry the kill and
|
|
2387
|
+
// signal the active turn's own handlers so the manager sees a visible
|
|
2388
|
+
// "still interrupting" state instead of the old turn's output silently
|
|
2389
|
+
// continuing forever. Retries are capped by `maxKillEscalationAttempts`, so
|
|
2390
|
+
// a permanently wedged process produces a final failure signal instead of an
|
|
2391
|
+
// unbounded escalation loop.
|
|
2392
|
+
killActiveChildWithEscalation(key, active, attempt = 1) {
|
|
2393
|
+
const pid = active.child.pid;
|
|
2394
|
+
if (pid == null)
|
|
2395
|
+
return;
|
|
2396
|
+
const maxAttempts = Math.max(1, Math.floor(this.maxKillEscalationAttempts));
|
|
2397
|
+
// Issue #1570: the first attempt is a normal SIGTERM; every retry escalates
|
|
2398
|
+
// to SIGKILL, matching the issue's own suggested "re-issue a forceful kill"
|
|
2399
|
+
// shape. On Windows this is a no-op (tree-kill's win32 branch always runs
|
|
2400
|
+
// `taskkill /T /F` regardless of signal), but on POSIX a resistant process
|
|
2401
|
+
// that ignored SIGTERM gets an unignorable SIGKILL from the second attempt on.
|
|
2402
|
+
try {
|
|
2403
|
+
this.killTree(pid, attempt === 1 ? 'SIGTERM' : 'SIGKILL');
|
|
2404
|
+
}
|
|
2405
|
+
catch {
|
|
2406
|
+
// Best-effort: `close` still fires on normal process exit either way.
|
|
2407
|
+
}
|
|
2408
|
+
if (active.escalationTimer != null)
|
|
2409
|
+
clearTimeout(active.escalationTimer);
|
|
2410
|
+
active.escalationTimer = setTimeout(() => {
|
|
2411
|
+
active.escalationTimer = undefined;
|
|
2412
|
+
// The child already closed (and this entry was replaced/removed via
|
|
2413
|
+
// dequeueNext) — nothing left to escalate.
|
|
2414
|
+
if (this.activeContinueRuns.get(key) !== active)
|
|
2415
|
+
return;
|
|
2416
|
+
if (attempt >= maxAttempts) {
|
|
2417
|
+
try {
|
|
2418
|
+
active.handlers.onInterruptFailed?.({ attempts: attempt, timeoutMs: this.killEscalationTimeoutMs });
|
|
2419
|
+
}
|
|
2420
|
+
catch (error) {
|
|
2421
|
+
console.warn('[ai-hub] onInterruptFailed handler threw:', error instanceof Error ? error.message : String(error));
|
|
2422
|
+
}
|
|
2423
|
+
console.warn('[ai-hub] redirect kill did not close after maximum escalation attempts:', { key, pid, attempts: attempt });
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
try {
|
|
2427
|
+
active.handlers.onInterruptStalled?.({ attempt, timeoutMs: this.killEscalationTimeoutMs });
|
|
2428
|
+
}
|
|
2429
|
+
catch (error) {
|
|
2430
|
+
console.warn('[ai-hub] onInterruptStalled handler threw:', error instanceof Error ? error.message : String(error));
|
|
2431
|
+
}
|
|
2432
|
+
console.warn('[ai-hub] redirect kill did not close within the escalation window; retrying:', { key, pid, attempt });
|
|
2433
|
+
this.killActiveChildWithEscalation(key, active, attempt + 1);
|
|
2434
|
+
}, this.killEscalationTimeoutMs);
|
|
2435
|
+
}
|
|
2365
2436
|
// R4-R6, R9, R16 (batch-delivery per issue #1176 Round 2 design feedback):
|
|
2366
2437
|
// when the active child closes, drain every pending continuation at once
|
|
2367
2438
|
// (FIFO), join their messages into a single combined turn, and spawn exactly
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure decision logic for desktop Office setup (loopback cert trust + Word/
|
|
4
|
+
* Excel/PowerPoint manifest sideload), extracted out of desktop-main.ts so it
|
|
5
|
+
* can be unit-tested without an Electron runtime (mirrors the
|
|
6
|
+
* window-open-decision.ts pure-function pattern - see architecture.md).
|
|
7
|
+
*
|
|
8
|
+
* All I/O (registry writes, certutil/security spawns) stays in desktop-main.ts
|
|
9
|
+
* and cert-store.ts; this module only decides whether to attempt it at all.
|
|
10
|
+
* Cert-trust idempotency itself is a live OS-state check (cert-store.ts's
|
|
11
|
+
* isCertTrusted()), not a flag file, so it has no decision logic to extract
|
|
12
|
+
* here - see #1571 PR feedback.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.shouldRunOfficeSetup = shouldRunOfficeSetup;
|
|
16
|
+
/**
|
|
17
|
+
* Issue #1571: a prior fix (commit 33764591c, "Avoid macOS trust prompt on
|
|
18
|
+
* normal Hub launch") gated Office setup behind FRAIM_ENABLE_OFFICE_SIDELOAD,
|
|
19
|
+
* an env var nothing else in the codebase ever set - so setup silently never
|
|
20
|
+
* ran on any platform. Office setup must run on every real launch on the
|
|
21
|
+
* platforms that support it; ensureCertTrustedOnce()/ensureWordSideload() in
|
|
22
|
+
* desktop-main.ts are each independently idempotent, so in steady state this
|
|
23
|
+
* returning true does no actual work and shows no dialog.
|
|
24
|
+
*
|
|
25
|
+
* Linux has no native Office desktop apps - office-sideload.ts and
|
|
26
|
+
* cert-store.ts both have no real implementation there - so it is excluded
|
|
27
|
+
* here rather than attempted and logged as "Unsupported platform" on every
|
|
28
|
+
* launch (FRAIM ships a Linux desktop build; see packages/fraim-hub/package.json).
|
|
29
|
+
*/
|
|
30
|
+
function shouldRunOfficeSetup(env, platform) {
|
|
31
|
+
return env.FRAIM_INSTALLER_LIFECYCLE_TEST !== '1'
|
|
32
|
+
&& (platform === 'win32' || platform === 'darwin');
|
|
33
|
+
}
|