pi-oracle 0.7.6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +7 -5
- package/docs/ORACLE_DESIGN.md +17 -11
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +22 -21
- package/docs/platform-smoke.md +5 -5
- package/extensions/oracle/index.ts +84 -4
- package/extensions/oracle/lib/auth.ts +4 -4
- package/extensions/oracle/lib/commands.ts +48 -22
- package/extensions/oracle/lib/config.ts +63 -9
- package/extensions/oracle/lib/poller.ts +20 -5
- package/extensions/oracle/lib/runtime.ts +8 -0
- package/extensions/oracle/lib/tools.ts +18 -5
- package/extensions/oracle/shared/browser-profile-helpers.d.mts +15 -0
- package/extensions/oracle/shared/browser-profile-helpers.mjs +37 -13
- package/extensions/oracle/shared/job-observability-helpers.d.mts +3 -1
- package/extensions/oracle/shared/job-observability-helpers.mjs +14 -5
- package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +9 -0
- package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +29 -2
- package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +1 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +52 -13
- package/extensions/oracle/worker/run-job.mjs +179 -53
- package/package.json +3 -6
- package/prompts/oracle-followup.md +2 -2
- package/prompts/oracle.md +13 -5
- package/scripts/oracle-real-smoke.mjs +10 -5
- package/scripts/platform-smoke/platform-build-windows.ps1 +2 -2
- package/scripts/platform-smoke/targets.mjs +2 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { getAgentDir, hasProjectTrustInputs, ProjectTrustStore } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { isAbsolute, join, normalize } from "node:path";
|
|
11
11
|
import {
|
|
12
12
|
assertNotKnownBrowserUserDataPath,
|
|
@@ -313,27 +313,76 @@ const detectedChromeUserAgent = detectDefaultChromeUserAgent(detectedChromeExecu
|
|
|
313
313
|
const agentExtensionsDir = join(getAgentDir(), "extensions");
|
|
314
314
|
const detectedChromeProfileName = detectDefaultBrowserProfileSource(process.platform);
|
|
315
315
|
|
|
316
|
+
export interface OracleConfigLoadOptions {
|
|
317
|
+
/**
|
|
318
|
+
* Whether project-local oracle config may be loaded. Omit for the runtime
|
|
319
|
+
* policy that preserves oracle's historical project-config behavior while
|
|
320
|
+
* respecting explicit --no-approve and saved distrust decisions.
|
|
321
|
+
*/
|
|
322
|
+
projectConfigTrusted?: boolean;
|
|
323
|
+
/** Session cwd used for Pi's saved project-trust decision when config lookup is anchored to a derived project root. */
|
|
324
|
+
projectConfigTrustCwd?: string;
|
|
325
|
+
}
|
|
326
|
+
|
|
316
327
|
export interface OracleConfigLoadDetails {
|
|
317
328
|
agentDir: string;
|
|
318
329
|
agentConfigPath: string;
|
|
319
330
|
agentConfigExists: boolean;
|
|
320
331
|
projectConfigPath: string;
|
|
321
332
|
projectConfigExists: boolean;
|
|
333
|
+
projectConfigTrusted: boolean;
|
|
334
|
+
projectConfigLoaded: boolean;
|
|
335
|
+
projectConfigSkippedReason?: string;
|
|
322
336
|
effectiveAuthConfigPath: string;
|
|
323
337
|
effectiveAuthScope: "agent";
|
|
324
338
|
}
|
|
325
339
|
|
|
326
|
-
|
|
340
|
+
function getProjectTrustCliOverride(argv = process.argv): boolean | undefined {
|
|
341
|
+
let trusted: boolean | undefined;
|
|
342
|
+
for (const arg of argv.slice(2)) {
|
|
343
|
+
if (arg === "--approve" || arg === "-a") trusted = true;
|
|
344
|
+
if (arg === "--no-approve" || arg === "-na") trusted = false;
|
|
345
|
+
}
|
|
346
|
+
return trusted;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function isProjectConfigTrusted(cwd: string, agentDir: string, projectConfigExists: boolean, options?: OracleConfigLoadOptions): boolean {
|
|
350
|
+
if (options?.projectConfigTrusted !== undefined) return options.projectConfigTrusted;
|
|
351
|
+
const trustCwd = options?.projectConfigTrustCwd ?? cwd;
|
|
352
|
+
const cliOverride = getProjectTrustCliOverride();
|
|
353
|
+
if (cliOverride !== undefined) return cliOverride;
|
|
354
|
+
if (!projectConfigExists && !hasProjectTrustInputs(trustCwd)) return true;
|
|
355
|
+
try {
|
|
356
|
+
const trustStore = new ProjectTrustStore(agentDir);
|
|
357
|
+
const trustDecision = trustStore.get(trustCwd);
|
|
358
|
+
const rootDecision = trustCwd !== cwd ? trustStore.get(cwd) : null;
|
|
359
|
+
if (trustDecision !== null) return trustDecision;
|
|
360
|
+
if (rootDecision !== null) return rootDecision;
|
|
361
|
+
} catch {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function getOracleConfigLoadDetails(cwd: string, options?: OracleConfigLoadOptions): OracleConfigLoadDetails {
|
|
327
368
|
const agentDir = getAgentDir();
|
|
328
369
|
const projectRoot = getProjectId(cwd);
|
|
329
370
|
const agentConfigPath = join(agentDir, "extensions", "oracle.json");
|
|
330
371
|
const projectConfigPath = join(projectRoot, ".pi", "extensions", "oracle.json");
|
|
372
|
+
const projectConfigExists = existsSync(projectConfigPath);
|
|
373
|
+
const projectConfigTrusted = isProjectConfigTrusted(projectRoot, agentDir, projectConfigExists, options);
|
|
374
|
+
const projectConfigLoaded = projectConfigExists && projectConfigTrusted;
|
|
331
375
|
return {
|
|
332
376
|
agentDir,
|
|
333
377
|
agentConfigPath,
|
|
334
378
|
agentConfigExists: existsSync(agentConfigPath),
|
|
335
379
|
projectConfigPath,
|
|
336
|
-
projectConfigExists
|
|
380
|
+
projectConfigExists,
|
|
381
|
+
projectConfigTrusted,
|
|
382
|
+
projectConfigLoaded,
|
|
383
|
+
projectConfigSkippedReason: projectConfigExists && !projectConfigTrusted
|
|
384
|
+
? "Project oracle config is ignored because this run used --no-approve or the project has a saved untrusted decision."
|
|
385
|
+
: undefined,
|
|
337
386
|
effectiveAuthConfigPath: agentConfigPath,
|
|
338
387
|
effectiveAuthScope: "agent",
|
|
339
388
|
};
|
|
@@ -341,8 +390,11 @@ export function getOracleConfigLoadDetails(cwd: string): OracleConfigLoadDetails
|
|
|
341
390
|
|
|
342
391
|
export function formatOracleAuthConfigRemediation(details: OracleConfigLoadDetails): string {
|
|
343
392
|
const authFields = "auth.chromeProfile / auth.chromeCookiePath / auth.chromiumKeychain";
|
|
344
|
-
if (!details.
|
|
345
|
-
|
|
393
|
+
if (!details.projectConfigLoaded) {
|
|
394
|
+
const projectNote = details.projectConfigSkippedReason
|
|
395
|
+
? ` Project config at ${details.projectConfigPath} is present but not loaded because this run explicitly does not trust project-local inputs.`
|
|
396
|
+
: "";
|
|
397
|
+
return `Set ${authFields} in ${details.effectiveAuthConfigPath}.${projectNote}`;
|
|
346
398
|
}
|
|
347
399
|
return (
|
|
348
400
|
`Set ${authFields} in ${details.effectiveAuthConfigPath}. ` +
|
|
@@ -354,11 +406,13 @@ export function formatOracleAuthConfigSummary(details: OracleConfigLoadDetails):
|
|
|
354
406
|
const lines = [
|
|
355
407
|
`Effective oracle auth config: ${details.effectiveAuthConfigPath} (agent dir: ${details.agentDir}${details.agentConfigExists ? "" : "; create this file to override auth.*"})`,
|
|
356
408
|
];
|
|
357
|
-
if (details.
|
|
409
|
+
if (details.projectConfigLoaded) {
|
|
358
410
|
lines.push(
|
|
359
411
|
`Project oracle config also loaded: ${details.projectConfigPath} ` +
|
|
360
412
|
`(project scope can override ${[...PROJECT_OVERRIDE_KEYS].join("/")} only; auth.* still comes from ${details.effectiveAuthConfigPath}).`,
|
|
361
413
|
);
|
|
414
|
+
} else if (details.projectConfigSkippedReason) {
|
|
415
|
+
lines.push(`Project oracle config present but not loaded: ${details.projectConfigPath}. ${details.projectConfigSkippedReason}`);
|
|
362
416
|
}
|
|
363
417
|
return lines.join("\n");
|
|
364
418
|
}
|
|
@@ -665,9 +719,9 @@ function validateOracleConfig(value: unknown): OracleConfig {
|
|
|
665
719
|
};
|
|
666
720
|
}
|
|
667
721
|
|
|
668
|
-
export function loadOracleConfig(cwd: string): OracleConfig {
|
|
669
|
-
const details = getOracleConfigLoadDetails(cwd);
|
|
722
|
+
export function loadOracleConfig(cwd: string, options?: OracleConfigLoadOptions): OracleConfig {
|
|
723
|
+
const details = getOracleConfigLoadDetails(cwd, options);
|
|
670
724
|
const globalConfig = readJson(details.agentConfigPath);
|
|
671
|
-
const projectConfig = filterProjectConfig(readJson(details.projectConfigPath));
|
|
725
|
+
const projectConfig = details.projectConfigLoaded ? filterProjectConfig(readJson(details.projectConfigPath)) : undefined;
|
|
672
726
|
return validateOracleConfig(deepMerge(deepMerge(DEFAULT_CONFIG, globalConfig), projectConfig));
|
|
673
727
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Invariants/Assumptions: Poller scans are serialized per session key, wake-up delivery is best-effort, and terminal-job notifications always re-read durable job state before send.
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { buildOracleStatusText, buildOracleWakeupNotificationContent } from "../shared/job-observability-helpers.mjs";
|
|
8
|
+
import { buildOracleStatusText, buildOracleWakeupNotificationContent, type OracleReadinessStatus } from "../shared/job-observability-helpers.mjs";
|
|
9
9
|
import { isProcessAlive, readProcessStartedAt } from "../shared/process-helpers.mjs";
|
|
10
10
|
import { isLockTimeoutError, listLeaseMetadata, releaseLease, withGlobalReconcileLock, writeLeaseMetadata } from "./locks.js";
|
|
11
11
|
import {
|
|
@@ -46,6 +46,7 @@ interface OraclePollerLifecycle {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const activePollers = new Map<string, OracleActivePoller>();
|
|
49
|
+
const readinessBySession = new Map<string, OracleReadinessStatus>();
|
|
49
50
|
const scansInFlight = new Set<string>();
|
|
50
51
|
const POLLER_LOCK_TIMEOUT_MS = 50;
|
|
51
52
|
const WAKEUP_TARGET_LEASE_KIND = "wakeup-target";
|
|
@@ -167,19 +168,31 @@ function getJobCountsForSession(sessionFile: string | undefined, cwd: string): {
|
|
|
167
168
|
function refreshOracleStatusSnapshot(snapshot: OraclePollerContextSnapshot): void {
|
|
168
169
|
if (!snapshot.hasUI) return;
|
|
169
170
|
if (!snapshot.sessionFile) {
|
|
170
|
-
snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("
|
|
171
|
+
snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("error", "oracle: unavailable"));
|
|
171
172
|
return;
|
|
172
173
|
}
|
|
173
174
|
const counts = getJobCountsForSession(snapshot.sessionFile, snapshot.cwd);
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
175
|
+
const readiness = readinessBySession.get(getPollerSessionKey(snapshot.sessionFile, snapshot.cwd)) ?? "loaded";
|
|
176
|
+
const statusText = buildOracleStatusText(counts, readiness);
|
|
177
|
+
if (counts.active > 0) {
|
|
178
|
+
snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("success", statusText));
|
|
179
|
+
} else if (readiness === "auth_needed" || readiness === "config_error") {
|
|
180
|
+
snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("error", statusText));
|
|
181
|
+
} else {
|
|
182
|
+
snapshot.ui.setStatus("oracle", statusText);
|
|
183
|
+
}
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
export function refreshOracleStatus(ctx: ExtensionContext): void {
|
|
180
187
|
refreshOracleStatusSnapshot(snapshotPollerContext(ctx));
|
|
181
188
|
}
|
|
182
189
|
|
|
190
|
+
export function setOracleReadiness(ctx: ExtensionContext, readiness: OracleReadinessStatus): void {
|
|
191
|
+
const snapshot = snapshotPollerContext(ctx);
|
|
192
|
+
if (snapshot.sessionFile) readinessBySession.set(getPollerSessionKey(snapshot.sessionFile, snapshot.cwd), readiness);
|
|
193
|
+
refreshOracleStatusSnapshot(snapshot);
|
|
194
|
+
}
|
|
195
|
+
|
|
183
196
|
function requestWakeupTurn(pi: ExtensionAPI, job: OraclePollerJob): void {
|
|
184
197
|
pi.sendMessage(
|
|
185
198
|
{
|
|
@@ -412,6 +425,7 @@ export function stopPollerForSession(sessionFile: string | undefined, cwd: strin
|
|
|
412
425
|
if (handle.timer) clearInterval(handle.timer);
|
|
413
426
|
activePollers.delete(sessionKey);
|
|
414
427
|
}
|
|
428
|
+
readinessBySession.delete(sessionKey);
|
|
415
429
|
const wakeupTargetLeaseKey = getWakeupTargetLeaseKey(sessionKey);
|
|
416
430
|
void releaseLease(WAKEUP_TARGET_LEASE_KIND, wakeupTargetLeaseKey).catch(() => undefined);
|
|
417
431
|
}
|
|
@@ -423,6 +437,7 @@ export async function stopAllPollers(): Promise<void> {
|
|
|
423
437
|
if (handle.timer) clearInterval(handle.timer);
|
|
424
438
|
}
|
|
425
439
|
activePollers.clear();
|
|
440
|
+
readinessBySession.clear();
|
|
426
441
|
await Promise.all(handles.map(async (handle) => {
|
|
427
442
|
const wakeupTargetLeaseKey = getWakeupTargetLeaseKey(handle.sessionKey);
|
|
428
443
|
await releaseLease(WAKEUP_TARGET_LEASE_KIND, wakeupTargetLeaseKey).catch(() => undefined);
|
|
@@ -169,6 +169,10 @@ function unreadableAuthSeedProfileMessage(seedDir: string): string {
|
|
|
169
169
|
return `Oracle auth seed profile is not readable: ${seedDir}. Fix its permissions or rerun /oracle-auth.`;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
function unauthenticatedAuthSeedProfileMessage(seedDir: string): string {
|
|
173
|
+
return `Oracle auth seed profile exists but is not authenticated: ${seedDir}. Run /oracle-auth to create a verified auth seed before submitting oracle jobs.`;
|
|
174
|
+
}
|
|
175
|
+
|
|
172
176
|
function missingBrowserExecutableMessage(executablePath: string): string {
|
|
173
177
|
return `Configured oracle browser executable does not exist: ${executablePath}. Fix browser.executablePath or install Chrome there.`;
|
|
174
178
|
}
|
|
@@ -315,6 +319,10 @@ export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Pr
|
|
|
315
319
|
} catch {
|
|
316
320
|
throw new Error(unreadableAuthSeedProfileMessage(seedDir));
|
|
317
321
|
}
|
|
322
|
+
|
|
323
|
+
if (!getSeedGeneration(config)) {
|
|
324
|
+
throw new Error(unauthenticatedAuthSeedProfileMessage(seedDir));
|
|
325
|
+
}
|
|
318
326
|
}
|
|
319
327
|
|
|
320
328
|
export async function assertOracleSubmitPrerequisites(config: OracleConfig): Promise<void> {
|
|
@@ -914,6 +914,15 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
|
|
|
914
914
|
};
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
+
if (message.startsWith("Oracle auth seed profile exists but is not authenticated: ")) {
|
|
918
|
+
return {
|
|
919
|
+
code: "auth_seed_profile_unauthenticated",
|
|
920
|
+
message,
|
|
921
|
+
rejectedValue: message.replace(/^Oracle auth seed profile exists but is not authenticated: /, "").replace(/\. Run \/oracle-auth.*$/, ""),
|
|
922
|
+
suggestedNextStep: "Call oracle_auth or run /oracle-auth once to create a verified auth seed, then retry the oracle tool call.",
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
917
926
|
if (message.startsWith("Failed to parse oracle config ") || message.startsWith("Invalid oracle config:") || message.startsWith("Invalid oracle project config:")) {
|
|
918
927
|
return {
|
|
919
928
|
code: "oracle_config_invalid",
|
|
@@ -1151,6 +1160,10 @@ function formatOraclePreflightResponse(details: OraclePreflightDetails): string
|
|
|
1151
1160
|
].filter(Boolean).join("\n");
|
|
1152
1161
|
}
|
|
1153
1162
|
|
|
1163
|
+
function isProjectTrusted(ctx: ExtensionContext): boolean {
|
|
1164
|
+
return (ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() ?? true;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1154
1167
|
async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: unknown; followUpJobId?: unknown } = {}): Promise<OraclePreflightDetails> {
|
|
1155
1168
|
const sessionFile = getSessionFile(ctx);
|
|
1156
1169
|
if (!hasPersistedSessionFile(sessionFile)) {
|
|
@@ -1174,7 +1187,7 @@ async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: un
|
|
|
1174
1187
|
if (followUpJobId !== undefined && typeof followUpJobId !== "string") {
|
|
1175
1188
|
throw new Error("oracle_preflight followUpJobId must be a string");
|
|
1176
1189
|
}
|
|
1177
|
-
const baseConfig = loadOracleConfig(ctx.cwd);
|
|
1190
|
+
const baseConfig = loadOracleConfig(ctx.cwd, { projectConfigTrusted: isProjectTrusted(ctx) });
|
|
1178
1191
|
const followUp = resolveFollowUp(followUpJobId, ctx.cwd);
|
|
1179
1192
|
provider = normalizeOracleProvider(params.provider, followUp.provider ?? baseConfig.defaults.provider, "oracle_preflight");
|
|
1180
1193
|
if (followUp.provider && provider !== followUp.provider) {
|
|
@@ -1202,7 +1215,7 @@ async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: un
|
|
|
1202
1215
|
session: { persisted: true, sessionFile },
|
|
1203
1216
|
config: { ready: true },
|
|
1204
1217
|
auth: {
|
|
1205
|
-
ready: !["auth_seed_profile_missing", "auth_seed_profile_unreadable", "auth_seed_profile_invalid_type"].includes(errorDetails.code),
|
|
1218
|
+
ready: !["auth_seed_profile_missing", "auth_seed_profile_unreadable", "auth_seed_profile_invalid_type", "auth_seed_profile_unauthenticated"].includes(errorDetails.code),
|
|
1206
1219
|
seedProfileDir: config.browser.authSeedProfileDir,
|
|
1207
1220
|
},
|
|
1208
1221
|
error: errorDetails,
|
|
@@ -1264,9 +1277,9 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
|
|
|
1264
1277
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1265
1278
|
try {
|
|
1266
1279
|
const projectCwd = getProjectId(ctx.cwd);
|
|
1267
|
-
const baseConfig = loadOracleConfig(projectCwd);
|
|
1280
|
+
const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted: isProjectTrusted(ctx) });
|
|
1268
1281
|
const provider = normalizeOracleProvider(params.provider, baseConfig.defaults.provider, "oracle_auth");
|
|
1269
|
-
const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd, provider);
|
|
1282
|
+
const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd, provider, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted: isProjectTrusted(ctx) });
|
|
1270
1283
|
return {
|
|
1271
1284
|
content: [{ type: "text" as const, text: message }],
|
|
1272
1285
|
details: {
|
|
@@ -1311,7 +1324,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
|
|
|
1311
1324
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1312
1325
|
try {
|
|
1313
1326
|
const projectCwd = getProjectId(ctx.cwd);
|
|
1314
|
-
const baseConfig = loadOracleConfig(projectCwd);
|
|
1327
|
+
const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted: isProjectTrusted(ctx) });
|
|
1315
1328
|
const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
|
|
1316
1329
|
const projectId = getProjectId(projectCwd);
|
|
1317
1330
|
const sessionId = getSessionId(originSessionFile, projectId);
|
|
@@ -10,6 +10,12 @@ export interface ExecutableSearchOptions {
|
|
|
10
10
|
pathDelimiter?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export interface KnownBrowserUserDataPathMatchDetails {
|
|
14
|
+
root: string;
|
|
15
|
+
source: "knownBrowserUserDataDir" | "auth.chromeCookiePath" | "auth.chromeProfile" | "extraProtectedPath";
|
|
16
|
+
configuredPath?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
export const SWEET_COOKIE_SAFE_STORAGE_PASSWORD_ENV_NAMES: readonly [
|
|
14
20
|
"SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD",
|
|
15
21
|
"SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD",
|
|
@@ -40,6 +46,15 @@ export function knownBrowserUserDataPathMatch(
|
|
|
40
46
|
cookieSources?: { chromeProfile?: string; chromeCookiePath?: string };
|
|
41
47
|
},
|
|
42
48
|
): string | undefined;
|
|
49
|
+
export function knownBrowserUserDataPathMatchDetails(
|
|
50
|
+
pathValue: string,
|
|
51
|
+
options?: BrowserPathOptions & {
|
|
52
|
+
platform?: OraclePlatform;
|
|
53
|
+
includeUnsupported?: boolean;
|
|
54
|
+
extraProtectedPaths?: string[];
|
|
55
|
+
cookieSources?: { chromeProfile?: string; chromeCookiePath?: string };
|
|
56
|
+
},
|
|
57
|
+
): KnownBrowserUserDataPathMatchDetails | undefined;
|
|
43
58
|
export function assertNotKnownBrowserUserDataPath(
|
|
44
59
|
pathValue: string,
|
|
45
60
|
label: string,
|
|
@@ -224,23 +224,40 @@ function protectedPathsForCookieDb(cookiePath) {
|
|
|
224
224
|
* @returns {string[]}
|
|
225
225
|
*/
|
|
226
226
|
export function protectedCookieSourcePaths(cookieSources) {
|
|
227
|
+
return protectedCookieSourcePathEntries(cookieSources).map((entry) => entry.path);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function protectedCookieSourcePathEntries(cookieSources) {
|
|
227
231
|
if (!cookieSources) return [];
|
|
228
232
|
const roots = [];
|
|
229
233
|
const cookiePath = typeof cookieSources.chromeCookiePath === "string" && cookieSources.chromeCookiePath.trim()
|
|
230
234
|
? cookieSources.chromeCookiePath.trim()
|
|
231
235
|
: undefined;
|
|
232
|
-
if (cookiePath)
|
|
236
|
+
if (cookiePath) {
|
|
237
|
+
roots.push(...protectedPathsForCookieDb(cookiePath).map((path) => ({ path, source: "auth.chromeCookiePath", configuredPath: cookiePath })));
|
|
238
|
+
}
|
|
233
239
|
|
|
234
240
|
const profile = typeof cookieSources.chromeProfile === "string" && cookieSources.chromeProfile.trim()
|
|
235
241
|
? cookieSources.chromeProfile.trim()
|
|
236
242
|
: undefined;
|
|
237
243
|
if (profile && looksLikeFilesystemPath(profile)) {
|
|
238
244
|
const normalizedProfile = normalizedAbsolutePath(profile);
|
|
239
|
-
if (isCookiesDbPath(normalizedProfile))
|
|
240
|
-
|
|
245
|
+
if (isCookiesDbPath(normalizedProfile)) {
|
|
246
|
+
roots.push(...protectedPathsForCookieDb(normalizedProfile).map((path) => ({ path, source: "auth.chromeProfile", configuredPath: profile })));
|
|
247
|
+
} else {
|
|
248
|
+
roots.push({ path: normalizedProfile, source: "auth.chromeProfile", configuredPath: profile }, { path: dirname(normalizedProfile), source: "auth.chromeProfile", configuredPath: profile });
|
|
249
|
+
}
|
|
241
250
|
}
|
|
242
251
|
|
|
243
|
-
|
|
252
|
+
const seen = new Set();
|
|
253
|
+
return roots
|
|
254
|
+
.map((root) => ({ ...root, path: normalize(root.path) }))
|
|
255
|
+
.filter((root) => {
|
|
256
|
+
const key = `${root.path}\0${root.source}\0${root.configuredPath}`;
|
|
257
|
+
if (seen.has(key)) return false;
|
|
258
|
+
seen.add(key);
|
|
259
|
+
return true;
|
|
260
|
+
});
|
|
244
261
|
}
|
|
245
262
|
|
|
246
263
|
/**
|
|
@@ -249,19 +266,24 @@ export function protectedCookieSourcePaths(cookieSources) {
|
|
|
249
266
|
* @returns {string | undefined}
|
|
250
267
|
*/
|
|
251
268
|
export function knownBrowserUserDataPathMatch(pathValue, options = {}) {
|
|
269
|
+
return knownBrowserUserDataPathMatchDetails(pathValue, options)?.root;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function knownBrowserUserDataPathMatchDetails(pathValue, options = {}) {
|
|
252
273
|
const platform = options.platform ?? process.platform;
|
|
253
274
|
const normalizedPath = normalizedAbsolutePath(pathValue, options);
|
|
254
275
|
const resolvedPath = resolvePathThroughExistingAncestorsSync(normalizedPath);
|
|
255
276
|
const roots = [
|
|
256
|
-
...browserUserDataDirsForPlatform(platform, { ...options, includeUnsupported: options.includeUnsupported ?? true })
|
|
257
|
-
|
|
258
|
-
...(
|
|
277
|
+
...browserUserDataDirsForPlatform(platform, { ...options, includeUnsupported: options.includeUnsupported ?? true })
|
|
278
|
+
.map((path) => ({ path, source: "knownBrowserUserDataDir" })),
|
|
279
|
+
...protectedCookieSourcePathEntries(options.cookieSources),
|
|
280
|
+
...((options.extraProtectedPaths ?? [])).map((path) => ({ path, source: "extraProtectedPath" })),
|
|
259
281
|
];
|
|
260
282
|
for (const root of roots) {
|
|
261
|
-
const normalizedRoot = normalizedAbsolutePath(root, options);
|
|
262
|
-
if (pathInsideOrEqual(normalizedPath, normalizedRoot)) return normalizedRoot;
|
|
283
|
+
const normalizedRoot = normalizedAbsolutePath(root.path, options);
|
|
284
|
+
if (pathInsideOrEqual(normalizedPath, normalizedRoot)) return { root: normalizedRoot, source: root.source, configuredPath: root.configuredPath };
|
|
263
285
|
const resolvedRoot = resolvePathThroughExistingAncestorsSync(normalizedRoot) ?? normalizedRoot;
|
|
264
|
-
if (resolvedPath && pathInsideOrEqual(resolvedPath, resolvedRoot)) return resolvedRoot;
|
|
286
|
+
if (resolvedPath && pathInsideOrEqual(resolvedPath, resolvedRoot)) return { root: resolvedRoot, source: root.source, configuredPath: root.configuredPath };
|
|
265
287
|
}
|
|
266
288
|
return undefined;
|
|
267
289
|
}
|
|
@@ -273,10 +295,12 @@ export function knownBrowserUserDataPathMatch(pathValue, options = {}) {
|
|
|
273
295
|
* @returns {void}
|
|
274
296
|
*/
|
|
275
297
|
export function assertNotKnownBrowserUserDataPath(pathValue, label, options = {}) {
|
|
276
|
-
const match =
|
|
277
|
-
if (match)
|
|
278
|
-
|
|
298
|
+
const match = knownBrowserUserDataPathMatchDetails(pathValue, options);
|
|
299
|
+
if (!match) return;
|
|
300
|
+
if (match.source === "auth.chromeCookiePath" || match.source === "auth.chromeProfile") {
|
|
301
|
+
throw new Error(`${label} is inside the browser profile root inferred from ${match.source} (${match.configuredPath} -> ${match.root}): ${pathValue}`);
|
|
279
302
|
}
|
|
303
|
+
throw new Error(`${label} must not point into a real browser user-data directory (${match.root}): ${pathValue}`);
|
|
280
304
|
}
|
|
281
305
|
|
|
282
306
|
/**
|
|
@@ -62,6 +62,8 @@ export interface OracleStatusCounts {
|
|
|
62
62
|
queued: number;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export type OracleReadinessStatus = "unavailable" | "loaded" | "auth_needed" | "config_error" | "ready";
|
|
66
|
+
|
|
65
67
|
export declare function formatBytes(bytes: number): string;
|
|
66
68
|
export declare function formatOracleLifecycleEvent(event: OracleJobLifecycleEvent | undefined): string | undefined;
|
|
67
69
|
export declare function formatOracleCancelOutcome(job: { id: string; status: string }): string;
|
|
@@ -71,4 +73,4 @@ export declare function buildOracleWakeupNotificationContent(
|
|
|
71
73
|
options?: { responsePath?: string; responseAvailable?: boolean; artifactsPath?: string },
|
|
72
74
|
): string;
|
|
73
75
|
export declare function formatOracleSubmitResponse(job: OracleJobSummaryLike & { promptPath: string; archivePath: string }, options: OracleSubmitResponseOptions): string;
|
|
74
|
-
export declare function buildOracleStatusText(counts: OracleStatusCounts): string;
|
|
76
|
+
export declare function buildOracleStatusText(counts: OracleStatusCounts, readiness?: OracleReadinessStatus): string;
|
|
@@ -254,19 +254,28 @@ export function formatOracleSubmitResponse(job, options) {
|
|
|
254
254
|
|
|
255
255
|
/**
|
|
256
256
|
* @param {OracleStatusCounts} counts
|
|
257
|
+
* @param {"unavailable" | "loaded" | "auth_needed" | "config_error" | "ready"} [readiness]
|
|
257
258
|
* @returns {string}
|
|
258
259
|
*/
|
|
259
|
-
export function buildOracleStatusText(counts) {
|
|
260
|
+
export function buildOracleStatusText(counts, readiness = "loaded") {
|
|
261
|
+
const readinessText = {
|
|
262
|
+
unavailable: "unavailable",
|
|
263
|
+
loaded: "loaded",
|
|
264
|
+
auth_needed: "auth needed",
|
|
265
|
+
config_error: "config error",
|
|
266
|
+
ready: "ready",
|
|
267
|
+
}[readiness] ?? "loaded";
|
|
268
|
+
const readinessSuffix = readiness === "ready" ? "" : `, ${readinessText}`;
|
|
260
269
|
if (counts.active > 0 && counts.queued > 0) {
|
|
261
|
-
return `oracle: running (${counts.active}), queued (${counts.queued})`;
|
|
270
|
+
return `oracle: running (${counts.active}), queued (${counts.queued})${readinessSuffix}`;
|
|
262
271
|
}
|
|
263
272
|
if (counts.active > 0) {
|
|
264
273
|
const suffix = counts.active > 1 ? ` (${counts.active})` : "";
|
|
265
|
-
return `oracle: running${suffix}`;
|
|
274
|
+
return `oracle: running${suffix}${readinessSuffix}`;
|
|
266
275
|
}
|
|
267
276
|
if (counts.queued > 0) {
|
|
268
277
|
const suffix = counts.queued > 1 ? ` (${counts.queued})` : "";
|
|
269
|
-
return `oracle: queued${suffix}`;
|
|
278
|
+
return `oracle: queued${suffix}${readinessSuffix}`;
|
|
270
279
|
}
|
|
271
|
-
return
|
|
280
|
+
return `oracle: ${readinessText}`;
|
|
272
281
|
}
|
|
@@ -3,9 +3,18 @@ export interface OracleStableValueState {
|
|
|
3
3
|
stableCount: number;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
export interface OracleSendAcceptanceState {
|
|
7
|
+
url?: string;
|
|
8
|
+
urlKnown?: boolean;
|
|
9
|
+
assistantCount?: number;
|
|
10
|
+
stopStreaming?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export declare function assistantSnapshotSlice(snapshot: string, composerLabel: string, responseIndex: number): string | undefined;
|
|
7
14
|
export declare function stripUrlQueryAndHash(url: string | undefined): string;
|
|
8
15
|
export declare function isConversationPathUrl(url: string): boolean;
|
|
16
|
+
export declare function conversationIdFromUrl(url: string | undefined): string | undefined;
|
|
17
|
+
export declare function providerSendAccepted(before: OracleSendAcceptanceState, after: OracleSendAcceptanceState): boolean;
|
|
9
18
|
export declare function resolveStableConversationUrlCandidate(url: string, previousChatUrl?: string): string | undefined;
|
|
10
19
|
export declare function nextStableValueState(
|
|
11
20
|
state: Partial<OracleStableValueState> | undefined,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// Invariants/Assumptions: Snapshot text comes from agent-browser `snapshot -i`; URL inputs may be malformed and must fail safely.
|
|
6
6
|
|
|
7
7
|
/** @typedef {import("./chatgpt-flow-helpers.d.mts").OracleStableValueState} OracleStableValueState */
|
|
8
|
+
/** @typedef {import("./chatgpt-flow-helpers.d.mts").OracleSendAcceptanceState} OracleSendAcceptanceState */
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @param {string} snapshot
|
|
@@ -52,13 +53,39 @@ export function stripUrlQueryAndHash(url) {
|
|
|
52
53
|
* @returns {boolean}
|
|
53
54
|
*/
|
|
54
55
|
export function isConversationPathUrl(url) {
|
|
56
|
+
return Boolean(conversationIdFromUrl(url));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string | undefined} url
|
|
61
|
+
* @returns {string | undefined}
|
|
62
|
+
*/
|
|
63
|
+
export function conversationIdFromUrl(url) {
|
|
64
|
+
if (typeof url !== "string" || !url.trim()) return undefined;
|
|
55
65
|
try {
|
|
56
|
-
|
|
66
|
+
const match = new URL(url).pathname.match(/\/(?:c|chat)\/([A-Za-z0-9-]+)$/i);
|
|
67
|
+
return match?.[1];
|
|
57
68
|
} catch {
|
|
58
|
-
return
|
|
69
|
+
return undefined;
|
|
59
70
|
}
|
|
60
71
|
}
|
|
61
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @param {OracleSendAcceptanceState} before
|
|
75
|
+
* @param {OracleSendAcceptanceState} after
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
export function providerSendAccepted(before, after) {
|
|
79
|
+
const beforeUrlKnown = before.urlKnown !== false;
|
|
80
|
+
const afterUrlKnown = after.urlKnown !== false;
|
|
81
|
+
const beforeConversationId = beforeUrlKnown ? conversationIdFromUrl(before.url) : undefined;
|
|
82
|
+
const afterConversationId = afterUrlKnown ? conversationIdFromUrl(after.url) : undefined;
|
|
83
|
+
if (beforeUrlKnown && afterUrlKnown && afterConversationId && afterConversationId !== beforeConversationId) return true;
|
|
84
|
+
if ((after.assistantCount ?? 0) > (before.assistantCount ?? 0)) return true;
|
|
85
|
+
if (after.stopStreaming === true && before.stopStreaming !== true) return true;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
/**
|
|
63
90
|
* @param {string} url
|
|
64
91
|
* @param {string | undefined} previousChatUrl
|
|
@@ -10,6 +10,7 @@ export interface OracleUiSelection {
|
|
|
10
10
|
export declare const CHATGPT_CANONICAL_APP_ORIGINS: readonly string[];
|
|
11
11
|
|
|
12
12
|
export declare function buildAllowedChatGptOrigins(chatUrl: string, authUrl?: string): string[];
|
|
13
|
+
export declare function stripChatGptResponseChrome(value: string | undefined): string;
|
|
13
14
|
export declare function matchesModelFamilyLabel(label: string | undefined, family: OracleUiModelFamily): boolean;
|
|
14
15
|
export declare function matchesRequestedModelControlLabel(label: string | undefined, selection: OracleUiSelection): boolean;
|
|
15
16
|
export declare function matchesCompactIntelligenceOpenerLabel(label: string | undefined): boolean;
|