pi-agent-browser-native 0.6.10 → 0.6.11
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 +32 -0
- package/README.md +49 -6
- package/dist/extensions/agent-browser/index.js +416 -430
- package/dist/extensions/agent-browser/lib/argv-grammar.js +1 -1
- package/dist/extensions/agent-browser/lib/command-policy.js +41 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +1 -1
- package/dist/extensions/agent-browser/lib/input-modes/script.js +3 -2
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +40 -10
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +3 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +8 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +1 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +3 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +25 -14
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +56 -10
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +161 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +3 -3
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +2 -4
- package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +68 -0
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -6
- package/dist/extensions/agent-browser/lib/page-target-validation.js +9 -5
- package/dist/extensions/agent-browser/lib/playbook.js +10 -9
- package/dist/extensions/agent-browser/lib/process-environment.js +26 -8
- package/dist/extensions/agent-browser/lib/process.js +8 -5
- package/dist/extensions/agent-browser/lib/read-confirmation.js +59 -0
- package/dist/extensions/agent-browser/lib/recording-reservations.js +11 -1
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +8 -0
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +6 -5
- package/dist/extensions/agent-browser/lib/results/categories.js +4 -2
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +60 -29
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +19 -8
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +0 -20
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +40 -38
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +1 -0
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +3 -3
- package/dist/extensions/agent-browser/lib/results/presentation.js +38 -9
- package/dist/extensions/agent-browser/lib/results/recording.js +50 -0
- package/dist/extensions/agent-browser/lib/runtime.js +72 -20
- package/dist/extensions/agent-browser/lib/session-page-state.js +23 -7
- package/dist/extensions/agent-browser/lib/temp.js +4 -0
- package/docs/ARCHITECTURE.md +26 -10
- package/docs/COMMAND_REFERENCE.md +35 -15
- package/docs/SUPPORT_MATRIX.md +7 -3
- package/docs/TOOL_CONTRACT.md +72 -20
- package/package.json +1 -1
- package/scripts/prepare.mjs +2 -4
|
@@ -286,10 +286,12 @@ export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = un
|
|
|
286
286
|
clampUpstreamDefaultTimeout(childEnv);
|
|
287
287
|
return childEnv;
|
|
288
288
|
}
|
|
289
|
-
function getManagedPreSpawnPolicyError(options, currentPageUrl, pageUrlUnknown = false) {
|
|
289
|
+
function getManagedPreSpawnPolicyError(options, currentPageUrl, pageUrlUnknown = false, browserIndependentReadConfirmation = false) {
|
|
290
290
|
if (!validateManagedSessionRestoreContextForSpawn(options)) {
|
|
291
291
|
return "Managed session restore policy, storage, or checkout identity changed after planning; refusing to start agent-browser.";
|
|
292
292
|
}
|
|
293
|
+
if (browserIndependentReadConfirmation)
|
|
294
|
+
return undefined;
|
|
293
295
|
return getPageTargetValidationError({
|
|
294
296
|
args: options.args,
|
|
295
297
|
currentPageUrl,
|
|
@@ -316,7 +318,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
316
318
|
restoreState: managedSessionRestoreState,
|
|
317
319
|
stdin,
|
|
318
320
|
};
|
|
319
|
-
const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown);
|
|
321
|
+
const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, options.browserIndependentReadConfirmation);
|
|
320
322
|
if (planningPolicyError) {
|
|
321
323
|
return {
|
|
322
324
|
aborted: false,
|
|
@@ -331,7 +333,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
331
333
|
const managedSessionRestoreEnv = getManagedSessionRestoreEnv(managedSessionRestoreOptions);
|
|
332
334
|
const ownedManagedSessionCompatibilityEnv = getOwnedManagedSessionCompatibilityEnv(managedSessionRestoreOptions);
|
|
333
335
|
const processOverrides = {
|
|
334
|
-
[AGENT_BROWSER_IDLE_TIMEOUT_ENV]: String(getImplicitSessionIdleTimeoutMs()),
|
|
336
|
+
...(ownedManagedSession ? { [AGENT_BROWSER_IDLE_TIMEOUT_ENV]: String(getImplicitSessionIdleTimeoutMs()) } : {}),
|
|
335
337
|
...managedSessionRestoreEnv,
|
|
336
338
|
...env,
|
|
337
339
|
...getManagedSessionRestoreProtectedEnv(managedSessionRestoreOptions, managedSessionRestoreEnv),
|
|
@@ -340,7 +342,8 @@ export async function runAgentBrowserProcess(options) {
|
|
|
340
342
|
};
|
|
341
343
|
const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
|
|
342
344
|
let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
|
|
343
|
-
const requestedSocketDir = explicitSocketDir ?? parentEnv[PI_AGENT_BROWSER_SOCKET_DIR_ENV]
|
|
345
|
+
const requestedSocketDir = explicitSocketDir ?? parentEnv[PI_AGENT_BROWSER_SOCKET_DIR_ENV]
|
|
346
|
+
?? (!ownedManagedSession ? parentEnv[AGENT_BROWSER_SOCKET_DIR_ENV] : undefined) ?? getAgentBrowserSocketDir();
|
|
344
347
|
if (requestedSocketDir !== undefined) {
|
|
345
348
|
const socketDirError = requestedSocketDir.length > 0
|
|
346
349
|
? await getAgentBrowserSocketDirValidationError(requestedSocketDir)
|
|
@@ -463,7 +466,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
463
466
|
});
|
|
464
467
|
};
|
|
465
468
|
const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
|
|
466
|
-
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown);
|
|
469
|
+
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, options.browserIndependentReadConfirmation);
|
|
467
470
|
if (spawnPolicyError) {
|
|
468
471
|
resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
|
|
469
472
|
return;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
|
|
2
|
+
import { extractExplicitSessionName, getAgentBrowserSessionIdentityKey, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./argv-grammar.js";
|
|
3
|
+
import { getExplicitReadUrl } from "./command-policy.js";
|
|
4
|
+
import { isCloseCommand } from "./command-taxonomy.js";
|
|
5
|
+
import { isRecord } from "./parsing.js";
|
|
6
|
+
export function parseReadConfirmation(value) {
|
|
7
|
+
if (!isRecord(value) || value.source !== "native-explicit-url-read" || (value.state !== "pending" && value.state !== "cleared"))
|
|
8
|
+
return undefined;
|
|
9
|
+
if (typeof value.id !== "string" || !value.id || typeof value.sessionName !== "string" || !value.sessionName || (value.namespace !== undefined && typeof value.namespace !== "string"))
|
|
10
|
+
return undefined;
|
|
11
|
+
return { ...(isRecord(value.capabilities) && value.capabilities.readRequiresConfirmation === true ? { capabilities: { readRequiresConfirmation: true } } : {}), id: value.id, sessionName: value.sessionName, namespace: value.namespace, source: "native-explicit-url-read", state: value.state };
|
|
12
|
+
}
|
|
13
|
+
export function findReadConfirmation(args, confirmations, namespace) {
|
|
14
|
+
const tokens = extractUpstreamCommandTokens(args);
|
|
15
|
+
if (tokens.length !== 2 || !["confirm", "deny"].includes(tokens[0]))
|
|
16
|
+
return undefined;
|
|
17
|
+
const sessionName = extractExplicitSessionName(args);
|
|
18
|
+
const effectiveNamespace = resolveAgentBrowserNamespace(args, namespace);
|
|
19
|
+
const matches = [...confirmations].filter(value => value.state === "pending" && value.id === tokens[1]
|
|
20
|
+
&& (sessionName === undefined || getAgentBrowserSessionIdentityKey(sessionName, value.namespace) === getAgentBrowserSessionIdentityKey(value.sessionName, value.namespace))
|
|
21
|
+
&& (effectiveNamespace === undefined || getAgentBrowserSessionIdentityKey(value.sessionName, effectiveNamespace) === getAgentBrowserSessionIdentityKey(value.sessionName, value.namespace)));
|
|
22
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
23
|
+
}
|
|
24
|
+
export function scopeReadConfirmationArgs(args, confirmation) {
|
|
25
|
+
return [
|
|
26
|
+
...(scanUpstreamGlobalFlagOccurrences(args, "--namespace").length === 0 ? ["--namespace", confirmation.namespace ?? ""] : []),
|
|
27
|
+
...(extractExplicitSessionName(args) === undefined ? ["--session", confirmation.sessionName] : []),
|
|
28
|
+
...args,
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
export function nextReadConfirmation(options) {
|
|
32
|
+
const { commandTokens: tokens, current } = options;
|
|
33
|
+
const settlesRead = current?.state === "pending" && tokens.length === 2 && ["confirm", "deny"].includes(tokens[0]) && tokens[1] === current.id;
|
|
34
|
+
const confirmedResult = settlesRead && tokens[0] === "confirm" && isRecord(options.data) && options.data.confirmed === true && options.data.action === "read" && isRecord(options.data.result)
|
|
35
|
+
? options.data.result.data : options.data;
|
|
36
|
+
// Native control fields only. Never parse response content, snapshot text or nested page JSON as provenance.
|
|
37
|
+
if (isRecord(confirmedResult) && confirmedResult.confirmation_required === true && typeof confirmedResult.confirmation_id === "string" && confirmedResult.confirmation_id && !("content" in confirmedResult)) {
|
|
38
|
+
if (confirmedResult.action === "read" && (typeof getExplicitReadUrl(tokens) === "string" || settlesRead)) {
|
|
39
|
+
return { ...(isRecord(confirmedResult.capabilities) && confirmedResult.capabilities.readRequiresConfirmation === true ? { capabilities: { readRequiresConfirmation: true } } : {}), id: confirmedResult.confirmation_id, namespace: options.namespace, sessionName: options.sessionName, source: "native-explicit-url-read", state: "pending" };
|
|
40
|
+
}
|
|
41
|
+
if (current?.state === "pending")
|
|
42
|
+
return { ...current, state: "cleared" };
|
|
43
|
+
}
|
|
44
|
+
return options.succeeded && current?.state === "pending" && (settlesRead || isCloseCommand(tokens[0])) ? { ...current, state: "cleared" } : undefined;
|
|
45
|
+
}
|
|
46
|
+
export function buildReadConfirmationNextActions(confirmation, pendingResponse) {
|
|
47
|
+
if (confirmation.state === "cleared")
|
|
48
|
+
return [];
|
|
49
|
+
const prefix = ["--namespace", confirmation.namespace ?? "", "--session", confirmation.sessionName];
|
|
50
|
+
if (!pendingResponse)
|
|
51
|
+
return [{ id: "inspect-read-confirmation-session", tool: "agent_browser", params: { args: [...prefix, "session", "info"] }, reason: "Inspect the exact native session after the read confirmation failed; rerun the original URL read if its ID expired.", safety: "Read-only status, without browser launch or tab changes. Do not substitute a different pending confirmation ID." }];
|
|
52
|
+
return ["confirm", "deny"].map(command => ({
|
|
53
|
+
id: command === "confirm" ? "approve-confirmation" : "deny-confirmation", tool: "agent_browser", params: { args: [...prefix, command, confirmation.id] },
|
|
54
|
+
reason: `${command === "confirm" ? "Approve" : "Deny"} the native confirmation for this explicit URL read.`,
|
|
55
|
+
safety: confirmation.capabilities?.readRequiresConfirmation === true
|
|
56
|
+
? "Review the requested read first. The native capability proves ID matching; no DOM confirmation is implied."
|
|
57
|
+
: "Native ID matching/browser independence is unproven. The exact native session is preserved, but this confirmation retains normal page checks.",
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
@@ -14,6 +14,8 @@ function getArtifactReservation(artifact) {
|
|
|
14
14
|
cwd: artifact.cwd ?? process.cwd(),
|
|
15
15
|
namespace: artifact.namespace,
|
|
16
16
|
path: artifact.path,
|
|
17
|
+
...(artifact.recording?.recordingId ? { recordingId: artifact.recording.recordingId } : {}),
|
|
18
|
+
...(artifact.recordingStartedAtMs !== undefined ? { startedAtMs: artifact.recordingStartedAtMs } : {}),
|
|
17
19
|
sessionName: artifact.session,
|
|
18
20
|
};
|
|
19
21
|
}
|
|
@@ -43,7 +45,7 @@ export function applyRecordingArtifactsToReservations(reservations, artifacts) {
|
|
|
43
45
|
for (const [key, pending] of pendingBySession) {
|
|
44
46
|
const existing = reservations.get(key);
|
|
45
47
|
reservations.set(key, pending);
|
|
46
|
-
if (!existing || existing.absolutePath !== pending.absolutePath || existing.cwd !== pending.cwd) {
|
|
48
|
+
if (!existing || existing.absolutePath !== pending.absolutePath || existing.cwd !== pending.cwd || existing.recordingId !== pending.recordingId || existing.startedAtMs !== pending.startedAtMs) {
|
|
47
49
|
transitions.push({ reservation: pending, state: "active" });
|
|
48
50
|
}
|
|
49
51
|
}
|
|
@@ -63,6 +65,8 @@ export function appendRecordingReservationTransition(pi, transition) {
|
|
|
63
65
|
cwd: state === "active" ? reservation.cwd : undefined,
|
|
64
66
|
namespace: reservation.namespace,
|
|
65
67
|
path: state === "active" ? reservation.path : undefined,
|
|
68
|
+
recordingId: state === "active" ? reservation.recordingId : undefined,
|
|
69
|
+
startedAtMs: state === "active" ? reservation.startedAtMs : undefined,
|
|
66
70
|
sessionName: reservation.sessionName,
|
|
67
71
|
state,
|
|
68
72
|
version: 1,
|
|
@@ -81,6 +85,10 @@ function parseReservationTransition(data) {
|
|
|
81
85
|
state: "closed",
|
|
82
86
|
};
|
|
83
87
|
}
|
|
88
|
+
if (data.recordingId !== undefined && (typeof data.recordingId !== "string" || !data.recordingId))
|
|
89
|
+
return undefined;
|
|
90
|
+
if (data.startedAtMs !== undefined && (typeof data.startedAtMs !== "number" || !Number.isFinite(data.startedAtMs)))
|
|
91
|
+
return undefined;
|
|
84
92
|
if (typeof data.absolutePath !== "string" || !isAbsolute(data.absolutePath)
|
|
85
93
|
|| typeof data.cwd !== "string" || !isAbsolute(data.cwd) || typeof data.path !== "string")
|
|
86
94
|
return undefined;
|
|
@@ -90,6 +98,8 @@ function parseReservationTransition(data) {
|
|
|
90
98
|
cwd: data.cwd,
|
|
91
99
|
namespace: data.namespace,
|
|
92
100
|
path: data.path,
|
|
101
|
+
...(typeof data.recordingId === "string" ? { recordingId: data.recordingId } : {}),
|
|
102
|
+
...(typeof data.startedAtMs === "number" ? { startedAtMs: data.startedAtMs } : {}),
|
|
93
103
|
sessionName: data.sessionName,
|
|
94
104
|
},
|
|
95
105
|
state: "active",
|
|
@@ -197,6 +197,14 @@ export function buildAgentBrowserNextActions(options) {
|
|
|
197
197
|
break;
|
|
198
198
|
case "timeout":
|
|
199
199
|
{
|
|
200
|
+
if (options.command === "session" && options.subcommand === "info") {
|
|
201
|
+
actions.push(buildNextToolAction({
|
|
202
|
+
args: ["session", "info"],
|
|
203
|
+
id: "retry-session-info",
|
|
204
|
+
reason: "Retry the same session status check without opening a browser or inspecting its page.",
|
|
205
|
+
}));
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
200
208
|
const textAssertion = options.command === "wait" && options.args?.includes("--text") === true;
|
|
201
209
|
const urlAssertion = options.command === "wait" && options.args?.includes("--url") === true;
|
|
202
210
|
actions.push(buildNextToolAction({
|
|
@@ -4,7 +4,8 @@ export function isPendingRecordingCommand(command, subcommand, kind) {
|
|
|
4
4
|
return command === "record" && (subcommand === "start" || subcommand === "restart") && kind === "video";
|
|
5
5
|
}
|
|
6
6
|
export function isPendingRecordingArtifact(artifact) {
|
|
7
|
-
return
|
|
7
|
+
return artifact.recordingState === "openRecording" || artifact.status === "pending"
|
|
8
|
+
|| (artifact.status === undefined && isPendingRecordingCommand(artifact.command, artifact.subcommand, artifact.kind));
|
|
8
9
|
}
|
|
9
10
|
export const SESSION_ARTIFACT_MANIFEST_VERSION = 1;
|
|
10
11
|
export const SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES";
|
|
@@ -98,10 +99,10 @@ export function retirePendingRecordingManifestEntries(manifest, sessionName, nam
|
|
|
98
99
|
|| !entry.session
|
|
99
100
|
|| getAgentBrowserSessionIdentityKey(entry.session, entry.namespace) !== sessionKey
|
|
100
101
|
|| entry.kind !== "video"
|
|
101
|
-
|| !
|
|
102
|
+
|| !isPendingRecordingArtifact(entry))
|
|
102
103
|
return entry;
|
|
103
104
|
changed = true;
|
|
104
|
-
return { ...entry,
|
|
105
|
+
return { ...entry, recordingState: undefined, status: "unverified", subcommand: "close-abandoned" };
|
|
105
106
|
});
|
|
106
107
|
if (!changed)
|
|
107
108
|
return manifest;
|
|
@@ -135,7 +136,7 @@ export function mergeSessionArtifactManifest(options) {
|
|
|
135
136
|
if (candidateKey !== key
|
|
136
137
|
&& sameRecordingSession
|
|
137
138
|
&& candidate.kind === "video"
|
|
138
|
-
&&
|
|
139
|
+
&& isPendingRecordingArtifact(candidate)) {
|
|
139
140
|
byPath.delete(candidateKey);
|
|
140
141
|
}
|
|
141
142
|
}
|
|
@@ -155,7 +156,7 @@ export function mergeSessionArtifactManifest(options) {
|
|
|
155
156
|
const leftTime = left.evictedAtMs ?? left.createdAtMs;
|
|
156
157
|
const rightTime = right.evictedAtMs ?? right.createdAtMs;
|
|
157
158
|
return rightTime - leftTime
|
|
158
|
-
|| Number(
|
|
159
|
+
|| Number(isPendingRecordingArtifact(right)) - Number(isPendingRecordingArtifact(left))
|
|
159
160
|
|| left.path.localeCompare(right.path);
|
|
160
161
|
})
|
|
161
162
|
.slice(0, maxEntries);
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { isPendingRecordingArtifact } from "./artifact-manifest.js";
|
|
2
2
|
function hasUnverifiedFileArtifact(artifacts) {
|
|
3
|
-
return (artifacts ?? []).some((artifact) => !isPendingRecordingArtifact(artifact) && artifact.exists !== true);
|
|
3
|
+
return (artifacts ?? []).some((artifact) => !isPendingRecordingArtifact(artifact) && (artifact.exists !== true || ["failed", "stale", "unverified"].includes(artifact.status ?? "")));
|
|
4
4
|
}
|
|
5
5
|
export function classifyAgentBrowserSuccessCategory(options) {
|
|
6
6
|
if (options.inspection)
|
|
7
7
|
return "inspection";
|
|
8
|
+
if (hasUnverifiedFileArtifact(options.artifacts))
|
|
9
|
+
return "artifact-unverified";
|
|
8
10
|
if ((options.artifacts ?? []).some(isPendingRecordingArtifact))
|
|
9
11
|
return "artifact-pending";
|
|
10
12
|
if ((options.artifacts ?? []).length > 0)
|
|
11
|
-
return
|
|
13
|
+
return "artifact-saved";
|
|
12
14
|
if (options.savedFile)
|
|
13
15
|
return "artifact-saved";
|
|
14
16
|
return "completed";
|
|
@@ -5,6 +5,7 @@ import { getExplicitArtifactDestination } from "../../orchestration/browser-run/
|
|
|
5
5
|
import { isRecord, parsePositiveInteger } from "../../parsing.js";
|
|
6
6
|
import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingArtifact, isPendingRecordingCommand, mergeSessionArtifactManifest, } from "../artifact-manifest.js";
|
|
7
7
|
import { classifyAgentBrowserSuccessCategory } from "../categories.js";
|
|
8
|
+
import { formatRecordingReceipt, getRecordingReceipt } from "../recording.js";
|
|
8
9
|
const PNG_HEADER = Buffer.from("89504e470d0a1a0a0000000d49484452", "hex");
|
|
9
10
|
const INLINE_IMAGE_MAX_BYTES_ENV = "PI_AGENT_BROWSER_INLINE_IMAGE_MAX_BYTES";
|
|
10
11
|
const DEFAULT_INLINE_IMAGE_MAX_BYTES = 5 * 1_024 * 1_024;
|
|
@@ -159,24 +160,26 @@ async function buildFileArtifactMetadata(options) {
|
|
|
159
160
|
const absolutePath = options.artifactRequest?.absolutePath ?? resolve(options.cwd, options.path);
|
|
160
161
|
const displayPath = options.artifactRequest?.path ?? options.path;
|
|
161
162
|
const extension = extname(absolutePath || options.path).toLowerCase() || undefined;
|
|
162
|
-
const pendingRecording = isPendingRecordingCommand(options.commandInfo.command, options.commandInfo.subcommand, kind);
|
|
163
|
+
const pendingRecording = options.recordingPending === true || (isPendingRecordingCommand(options.commandInfo.command, options.commandInfo.subcommand, kind) && options.recordingOutcome !== false && options.recording?.success !== false);
|
|
164
|
+
const captureStartedAtMs = options.recording?.capture.startedAt ? Date.parse(options.recording.capture.startedAt) : NaN;
|
|
165
|
+
const recordingStartedAtMs = Number.isFinite(captureStartedAtMs) ? captureStartedAtMs : options.artifactMinUpdatedAtMs;
|
|
163
166
|
let exists;
|
|
164
167
|
let sizeBytes;
|
|
165
168
|
let mediaType;
|
|
166
169
|
let stale = false;
|
|
167
170
|
let updatedAtMs;
|
|
168
|
-
if (!pendingRecording) {
|
|
171
|
+
if (!pendingRecording || options.commandInfo.subcommand === "stop") {
|
|
169
172
|
try {
|
|
170
173
|
const fileStats = await stat(absolutePath);
|
|
171
|
-
exists =
|
|
174
|
+
exists = fileStats.isFile();
|
|
172
175
|
sizeBytes = fileStats.size;
|
|
173
176
|
updatedAtMs = fileStats.mtimeMs;
|
|
174
177
|
mediaType = fileStats.isFile() ? await getFileImageMimeType(absolutePath) : undefined;
|
|
175
178
|
const commandCreatesArtifact = !(options.commandInfo.command === "wait" && isDownloadWaitSubcommand(options.commandInfo.subcommand));
|
|
176
|
-
stale = commandCreatesArtifact && artifactMtimeIsOutsideCommandWindow(updatedAtMs, options.artifactMinUpdatedAtMs, options.artifactMaxUpdatedAtMs);
|
|
179
|
+
stale = commandCreatesArtifact && artifactMtimeIsOutsideCommandWindow(updatedAtMs, kind === "video" ? recordingStartedAtMs : options.artifactMinUpdatedAtMs, options.artifactMaxUpdatedAtMs);
|
|
177
180
|
}
|
|
178
|
-
catch {
|
|
179
|
-
exists = false;
|
|
181
|
+
catch (error) {
|
|
182
|
+
exists = ["ENOENT", "ENOTDIR"].includes(error.code ?? "") ? false : undefined;
|
|
180
183
|
}
|
|
181
184
|
}
|
|
182
185
|
return {
|
|
@@ -190,11 +193,17 @@ async function buildFileArtifactMetadata(options) {
|
|
|
190
193
|
mediaType,
|
|
191
194
|
namespace: options.namespace,
|
|
192
195
|
path: displayPath,
|
|
196
|
+
recording: options.recording,
|
|
197
|
+
recordingStartedAtMs: kind === "video" ? recordingStartedAtMs : undefined,
|
|
193
198
|
recordingState: pendingRecording ? "openRecording" : undefined,
|
|
194
199
|
requestedPath: options.artifactRequest?.path ?? getExplicitArtifactDestination(options.commandInfo.commandTokens ?? []),
|
|
195
200
|
session: options.sessionName,
|
|
196
201
|
sizeBytes,
|
|
197
|
-
status: pendingRecording ? "pending" : exists === false ? "missing" :
|
|
202
|
+
status: pendingRecording ? "pending" : exists !== true ? exists === false ? "missing" : "unverified" : stale ? "stale"
|
|
203
|
+
: kind === "video" && (options.recording?.success === false || options.recording?.output.encoderSucceeded === false || (options.recordingOutcome === false && options.recording?.success !== null)) ? "failed"
|
|
204
|
+
: kind === "video" && ((options.recording?.success !== true && options.recordingOutcome !== true)
|
|
205
|
+
|| (options.recording?.file.sizeBytes != null && options.recording.file.sizeBytes !== sizeBytes)) ? "unverified"
|
|
206
|
+
: options.artifactRequest?.status ?? "saved",
|
|
198
207
|
subcommand: options.commandInfo.subcommand,
|
|
199
208
|
tempPath: options.artifactRequest?.tempPath,
|
|
200
209
|
updatedAtMs,
|
|
@@ -204,6 +213,10 @@ async function buildFileArtifactMetadata(options) {
|
|
|
204
213
|
async function buildPreviousRestartRecordingArtifact(options) {
|
|
205
214
|
if (options.commandInfo.command !== "record" || options.commandInfo.subcommand !== "restart")
|
|
206
215
|
return undefined;
|
|
216
|
+
if (isRecord(options.data) && "previousRecording" in options.data) {
|
|
217
|
+
const recording = getRecordingReceipt(options.data.previousRecording);
|
|
218
|
+
return recording ? buildFileArtifactMetadata({ ...options, commandInfo: { command: "record", subcommand: "restart-previous" }, path: recording.path, recording }) : undefined;
|
|
219
|
+
}
|
|
207
220
|
const sessionKey = options.sessionName ? getAgentBrowserSessionIdentityKey(options.sessionName, options.namespace) : undefined;
|
|
208
221
|
const previousRecording = options.artifactManifest?.entries.find((entry) => (entry.command === "record" &&
|
|
209
222
|
(entry.subcommand === "start" || entry.subcommand === "restart") &&
|
|
@@ -233,18 +246,21 @@ async function buildPreviousRestartRecordingArtifact(options) {
|
|
|
233
246
|
exists: true,
|
|
234
247
|
mediaType: fileStats.isFile() ? await getFileImageMimeType(absolutePath) : undefined,
|
|
235
248
|
sizeBytes: fileStats.size,
|
|
236
|
-
status: stale ? "stale" : "
|
|
249
|
+
status: stale ? "stale" : "unverified",
|
|
237
250
|
updatedAtMs: fileStats.mtimeMs,
|
|
238
251
|
};
|
|
239
252
|
}
|
|
240
|
-
catch {
|
|
241
|
-
|
|
253
|
+
catch (error) {
|
|
254
|
+
const missing = ["ENOENT", "ENOTDIR"].includes(error.code ?? "");
|
|
255
|
+
return { ...base, exists: missing ? false : undefined, status: missing ? "missing" : "unverified" };
|
|
242
256
|
}
|
|
243
257
|
}
|
|
244
258
|
export async function extractFileArtifacts(options) {
|
|
245
259
|
const candidates = extractPathStrings(options.data);
|
|
246
|
-
const
|
|
247
|
-
const
|
|
260
|
+
const recording = options.commandInfo.command === "record" ? getRecordingReceipt(options.data, options.commandInfo.subcommand === "stop" ? options.recordingOutcome : undefined) : undefined;
|
|
261
|
+
const recordingPending = options.recordingPending ?? (recording?.success === null && isRecord(options.data) && isRecord(options.data.capture) && recording.capture.endedAt === null);
|
|
262
|
+
const currentArtifacts = (await Promise.all(candidates.map((path) => buildFileArtifactMetadata({ ...options, path, recording, recordingPending })))).filter((artifact) => artifact !== undefined);
|
|
263
|
+
const previousRestartRecordingArtifact = await buildPreviousRestartRecordingArtifact(options);
|
|
248
264
|
return previousRestartRecordingArtifact ? [previousRestartRecordingArtifact, ...currentArtifacts] : currentArtifacts;
|
|
249
265
|
}
|
|
250
266
|
export function buildManifestEntriesForFileArtifacts(artifacts, nowMs = Date.now()) {
|
|
@@ -259,6 +275,10 @@ export function buildManifestEntriesForFileArtifacts(artifacts, nowMs = Date.now
|
|
|
259
275
|
mediaType: artifact.mediaType,
|
|
260
276
|
namespace: artifact.namespace,
|
|
261
277
|
path: artifact.path,
|
|
278
|
+
recording: artifact.recording,
|
|
279
|
+
recordingStartedAtMs: artifact.recordingStartedAtMs,
|
|
280
|
+
recordingState: artifact.recordingState,
|
|
281
|
+
status: artifact.status,
|
|
262
282
|
requestedPath: artifact.requestedPath,
|
|
263
283
|
retentionState: artifact.exists === false || artifact.status === "stale" ? "missing" : "live",
|
|
264
284
|
session: artifact.session,
|
|
@@ -268,10 +288,7 @@ export function buildManifestEntriesForFileArtifacts(artifacts, nowMs = Date.now
|
|
|
268
288
|
}));
|
|
269
289
|
}
|
|
270
290
|
export function isManifestFileArtifact(artifact) {
|
|
271
|
-
|
|
272
|
-
return artifact.kind === "video" && artifact.command === "record" && artifact.subcommand === "restart-previous";
|
|
273
|
-
}
|
|
274
|
-
return artifact.kind === "video" && artifact.command === "record" ? true : !isPendingRecordingArtifact(artifact);
|
|
291
|
+
return artifact.kind === "video" && artifact.command === "record" ? true : artifact.status !== "stale" && !isPendingRecordingArtifact(artifact);
|
|
275
292
|
}
|
|
276
293
|
function getArtifactVerificationEntry(artifact) {
|
|
277
294
|
if (isPendingRecordingArtifact(artifact)) {
|
|
@@ -279,7 +296,9 @@ function getArtifactVerificationEntry(artifact) {
|
|
|
279
296
|
absolutePath: artifact.absolutePath,
|
|
280
297
|
exists: artifact.exists,
|
|
281
298
|
kind: artifact.kind,
|
|
282
|
-
limitation: "Recording output is pending until
|
|
299
|
+
limitation: "Recording output is pending until native finalization succeeds and the file is verified.",
|
|
300
|
+
recording: artifact.recording,
|
|
301
|
+
recordingStartedAtMs: artifact.recordingStartedAtMs,
|
|
283
302
|
mediaType: artifact.mediaType,
|
|
284
303
|
path: artifact.path,
|
|
285
304
|
recordingState: artifact.recordingState ?? "openRecording",
|
|
@@ -292,7 +311,7 @@ function getArtifactVerificationEntry(artifact) {
|
|
|
292
311
|
willExistOnStop: artifact.willExistOnStop ?? true,
|
|
293
312
|
};
|
|
294
313
|
}
|
|
295
|
-
const state = artifact.status
|
|
314
|
+
const state = ["failed", "stale", "unverified"].includes(artifact.status ?? "")
|
|
296
315
|
? "unverified"
|
|
297
316
|
: artifact.exists === true
|
|
298
317
|
? "verified"
|
|
@@ -303,13 +322,17 @@ function getArtifactVerificationEntry(artifact) {
|
|
|
303
322
|
absolutePath: artifact.absolutePath,
|
|
304
323
|
exists: artifact.exists,
|
|
305
324
|
kind: artifact.kind,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
325
|
+
recording: artifact.recording,
|
|
326
|
+
recordingStartedAtMs: artifact.recordingStartedAtMs,
|
|
327
|
+
limitation: artifact.status === "failed" || artifact.status === "unverified"
|
|
328
|
+
? "File presence does not prove successful native recording finalization or encoding. Inspect the receipt and original failure."
|
|
329
|
+
: artifact.status === "stale"
|
|
330
|
+
? "The reported path's modification time fell outside this command's bounded artifact window. Treat the artifact as stale until regenerated."
|
|
331
|
+
: state === "missing"
|
|
332
|
+
? "The wrapper did not find the reported artifact at absolutePath. Treat the path as unverified until recovered or regenerated."
|
|
333
|
+
: state === "unverified"
|
|
334
|
+
? "The wrapper could not prove local filesystem existence for this artifact."
|
|
335
|
+
: undefined,
|
|
313
336
|
mediaType: artifact.mediaType,
|
|
314
337
|
path: artifact.path,
|
|
315
338
|
requestedPath: artifact.requestedPath,
|
|
@@ -415,6 +438,10 @@ function formatArtifactLabel(artifact) {
|
|
|
415
438
|
case "trace":
|
|
416
439
|
return "Saved trace";
|
|
417
440
|
case "video":
|
|
441
|
+
if (artifact.status === "failed")
|
|
442
|
+
return artifact.subcommand === "restart-previous" ? "Previous recording failed" : "Recording failed";
|
|
443
|
+
if (artifact.status === "unverified")
|
|
444
|
+
return artifact.subcommand === "restart-previous" ? "Previous recording unverified" : "Recording unverified";
|
|
418
445
|
if (artifact.command === "record" && artifact.subcommand === "restart-previous") {
|
|
419
446
|
if (artifact.status === "stale")
|
|
420
447
|
return "Previous recording stale";
|
|
@@ -423,7 +450,9 @@ function formatArtifactLabel(artifact) {
|
|
|
423
450
|
return "Previous recording saved";
|
|
424
451
|
}
|
|
425
452
|
if (!isPendingRecordingArtifact(artifact))
|
|
426
|
-
return "Saved recording";
|
|
453
|
+
return artifact.status === "saved" ? "Saved recording" : "Recording reported; file not verified";
|
|
454
|
+
if (artifact.subcommand === "stop")
|
|
455
|
+
return "Recording finalization pending";
|
|
427
456
|
return artifact.subcommand === "restart" ? "Recording restarted; output will be written on stop" : "Recording started; output will be written on stop";
|
|
428
457
|
}
|
|
429
458
|
}
|
|
@@ -440,7 +469,7 @@ export function formatArtifactSummary(artifacts) {
|
|
|
440
469
|
if (restartArtifact && previousRecordingArtifacts.length > 0) {
|
|
441
470
|
return [...previousRecordingArtifacts, restartArtifact].map((artifact) => `${formatArtifactLabel(artifact)}: ${artifact.path}`).join("\n");
|
|
442
471
|
}
|
|
443
|
-
return
|
|
472
|
+
return `${artifacts.every((artifact) => artifact.status === "saved") ? "Saved" : "Reported"} ${artifacts.length} artifacts: ${artifacts.map((artifact) => `${artifact.kind} ${artifact.path}`).join(", ")}`;
|
|
444
473
|
}
|
|
445
474
|
export function formatArtifactMetadataLines(artifacts) {
|
|
446
475
|
return artifacts.map((artifact, index) => {
|
|
@@ -450,12 +479,13 @@ export function formatArtifactMetadataLines(artifacts) {
|
|
|
450
479
|
`Artifact type: ${artifact.kind}`,
|
|
451
480
|
artifact.requestedPath ? `Requested path: ${artifact.requestedPath}` : undefined,
|
|
452
481
|
`Absolute path: ${artifact.absolutePath}`,
|
|
453
|
-
|
|
482
|
+
`Exists: ${artifact.exists ?? "pending until record stop"}`,
|
|
454
483
|
`Status: ${artifact.status ?? "pending"}`,
|
|
455
484
|
`Recording state: ${artifact.recordingState ?? "openRecording"}`,
|
|
456
485
|
`Will exist on stop: ${artifact.willExistOnStop !== false}`,
|
|
457
486
|
artifact.session ? `Session: ${artifact.session}` : undefined,
|
|
458
487
|
artifact.cwd ? `CWD: ${artifact.cwd}` : undefined,
|
|
488
|
+
artifact.recording ? formatRecordingReceipt(artifact.recording) : undefined,
|
|
459
489
|
`Machine data: details.artifacts[${index}]`,
|
|
460
490
|
].filter((item) => item !== undefined).join("\n");
|
|
461
491
|
}
|
|
@@ -464,7 +494,7 @@ export function formatArtifactMetadataLines(artifacts) {
|
|
|
464
494
|
`Artifact type: ${artifact.kind}`,
|
|
465
495
|
artifact.requestedPath ? `Requested path: ${artifact.requestedPath}` : undefined,
|
|
466
496
|
`Absolute path: ${artifact.absolutePath}`,
|
|
467
|
-
`Exists: ${artifact.exists
|
|
497
|
+
`Exists: ${artifact.exists ?? "unknown"}`,
|
|
468
498
|
artifact.exists === false ? "not found on disk" : undefined,
|
|
469
499
|
typeof artifact.sizeBytes === "number" ? `Size: ${formatByteCount(artifact.sizeBytes)}` : undefined,
|
|
470
500
|
typeof artifact.sizeBytes === "number" ? `Size bytes: ${artifact.sizeBytes}` : undefined,
|
|
@@ -473,6 +503,7 @@ export function formatArtifactMetadataLines(artifacts) {
|
|
|
473
503
|
artifact.mediaType ? `Media type: ${artifact.mediaType}` : undefined,
|
|
474
504
|
artifact.session ? `Session: ${artifact.session}` : undefined,
|
|
475
505
|
artifact.cwd ? `CWD: ${artifact.cwd}` : undefined,
|
|
506
|
+
artifact.recording ? formatRecordingReceipt(artifact.recording) : undefined,
|
|
476
507
|
`Machine data: details.artifacts[${index}]`,
|
|
477
508
|
].filter((item) => item !== undefined).join("\n");
|
|
478
509
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
1
2
|
import { isCloseCommand } from "../../command-taxonomy.js";
|
|
2
3
|
import { isRecord } from "../../parsing.js";
|
|
3
4
|
import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
|
|
@@ -194,7 +195,7 @@ async function buildBatchStepPresentation(options) {
|
|
|
194
195
|
const redactedCommand = command ? redactInvocationArgs(command) : undefined;
|
|
195
196
|
const commandText = formatBatchStepCommand(hasModelFacingArgRedaction(redactedCommand) ? redactedCommand : command, index);
|
|
196
197
|
const lifecycle = extractAgentBrowserLifecycle(item.result);
|
|
197
|
-
if (item.success === false) {
|
|
198
|
+
if (item.success === false && command?.[0] !== "record") {
|
|
198
199
|
const redactedErrorData = redactBatchStepErrorData(command, item.error);
|
|
199
200
|
const errorText = formatBatchStepError(redactedErrorData);
|
|
200
201
|
const failureCategory = classifyAgentBrowserFailureCategory({
|
|
@@ -252,7 +253,9 @@ async function buildBatchStepPresentation(options) {
|
|
|
252
253
|
commandInfo: commandInfoWithTokens,
|
|
253
254
|
cwd,
|
|
254
255
|
args: command,
|
|
255
|
-
envelope: { data: item.result, success:
|
|
256
|
+
envelope: { data: item.result, success: item.success !== false, error: item.error },
|
|
257
|
+
errorText: item.success === false ? formatBatchStepError(redactBatchStepErrorData(command, item.error)) : undefined,
|
|
258
|
+
piCleanupOwnership: options.piCleanupOwnership,
|
|
256
259
|
networkRouteDiagnostics,
|
|
257
260
|
namespace,
|
|
258
261
|
persistentArtifactStore,
|
|
@@ -320,11 +323,18 @@ async function buildBatchStepPresentation(options) {
|
|
|
320
323
|
presentation,
|
|
321
324
|
};
|
|
322
325
|
}
|
|
323
|
-
function abandonedRecordingArtifact(artifact) {
|
|
326
|
+
async function abandonedRecordingArtifact(artifact) {
|
|
324
327
|
const { recordingState: _recordingState, willExistOnStop: _willExistOnStop, ...terminal } = artifact;
|
|
325
|
-
|
|
328
|
+
try {
|
|
329
|
+
const file = await stat(artifact.absolutePath);
|
|
330
|
+
return { ...terminal, exists: file.isFile(), sizeBytes: file.size, status: file.isFile() ? "unverified" : "missing", subcommand: "close-abandoned" };
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
const missing = error.code === "ENOENT";
|
|
334
|
+
return { ...terminal, exists: missing ? false : undefined, status: missing ? "missing" : "unverified", subcommand: "close-abandoned" };
|
|
335
|
+
}
|
|
326
336
|
}
|
|
327
|
-
function coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace) {
|
|
337
|
+
async function coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace) {
|
|
328
338
|
const artifacts = [];
|
|
329
339
|
const pendingIndexesBySession = new Map();
|
|
330
340
|
const removedPendingIndexes = new Set();
|
|
@@ -351,7 +361,7 @@ function coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace)
|
|
|
351
361
|
for (const pendingIndex of pendingIndexesBySession.get(session) ?? []) {
|
|
352
362
|
const pending = artifacts[pendingIndex];
|
|
353
363
|
if (pending && !removedPendingIndexes.has(pendingIndex))
|
|
354
|
-
artifacts[pendingIndex] = abandonedRecordingArtifact(pending);
|
|
364
|
+
artifacts[pendingIndex] = await abandonedRecordingArtifact(pending);
|
|
355
365
|
}
|
|
356
366
|
}
|
|
357
367
|
return artifacts.filter((_, index) => !removedPendingIndexes.has(index));
|
|
@@ -372,6 +382,7 @@ export async function buildBatchPresentation(options) {
|
|
|
372
382
|
cwd,
|
|
373
383
|
index,
|
|
374
384
|
item,
|
|
385
|
+
piCleanupOwnership: options.piCleanupOwnership,
|
|
375
386
|
namespace,
|
|
376
387
|
networkRoutes: currentNetworkRoutes,
|
|
377
388
|
persistentArtifactStore: persistentArtifactStore ? { ...persistentArtifactStore, protectedPaths: protectedPersistentPaths } : undefined,
|
|
@@ -387,7 +398,7 @@ export async function buildBatchPresentation(options) {
|
|
|
387
398
|
}
|
|
388
399
|
const batchFailure = getBatchFailureDetails(steps);
|
|
389
400
|
const images = steps.flatMap((step) => getPresentationImages(step.presentation));
|
|
390
|
-
const artifacts = coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace);
|
|
401
|
+
const artifacts = await coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace);
|
|
391
402
|
const artifactVerification = buildArtifactVerificationSummary(artifacts);
|
|
392
403
|
const fullOutputPaths = steps.flatMap((step) => getPresentationPaths({
|
|
393
404
|
primaryPath: step.presentation.fullOutputPath,
|
|
@@ -399,7 +410,7 @@ export async function buildBatchPresentation(options) {
|
|
|
399
410
|
}));
|
|
400
411
|
const redactedBatchData = steps.map(({ details }) => (details.success
|
|
401
412
|
? { command: details.command, result: details.data, success: true }
|
|
402
|
-
: { command: details.command, error: details.text, success: false }));
|
|
413
|
+
: { command: details.command, error: details.text, ...(details.command?.[0] === "record" ? { result: details.data } : {}), success: false }));
|
|
403
414
|
const unverifiedMutationCount = steps.filter((step) => step.details.pageChangeSummary?.changeType === "mutation" && step.details.pageChangeSummary.observed === false).length;
|
|
404
415
|
const mutationEvidenceText = unverifiedMutationCount > 0
|
|
405
416
|
? `Mutation evidence: ${unverifiedMutationCount} action result${unverifiedMutationCount === 1 ? " proves" : "s prove"} dispatch only, not application state change. Use explicit later assertions or external receipts as postconditions; fixed waits are not postconditions.`
|
|
@@ -5,29 +5,9 @@ const UNTITLED_PAGE_SUMMARY = "(untitled page)";
|
|
|
5
5
|
export function stringifyModelFacing(value) {
|
|
6
6
|
return stringifyUnknown(redactSensitiveValue(value));
|
|
7
7
|
}
|
|
8
|
-
export function parseJsonPreviewString(value) {
|
|
9
|
-
const trimmed = value.trim();
|
|
10
|
-
if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
|
|
11
|
-
return value;
|
|
12
|
-
try {
|
|
13
|
-
return JSON.parse(trimmed);
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return value;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
8
|
export function redactModelFacingText(text) {
|
|
20
|
-
const parsed = parseJsonPreviewString(text);
|
|
21
|
-
if (parsed !== text) {
|
|
22
|
-
return stringifyModelFacing(parsed);
|
|
23
|
-
}
|
|
24
9
|
return redactSensitiveText(text);
|
|
25
10
|
}
|
|
26
|
-
export function redactModelFacingTextIfSensitive(text) {
|
|
27
|
-
return /(?:@|\b(?:access[_-]?key|api[_-]?key|auth|authorization|basic|bearer|connection[_-]?string|cookie|database[_-]?url|db[_-]?url|mongo(?:db)?[_-]?uri|pass(?:word)?|private[_-]?key|redis[_-]?url|secret|session[_-]?id|token)\b)/i.test(text)
|
|
28
|
-
? redactModelFacingText(text)
|
|
29
|
-
: text;
|
|
30
|
-
}
|
|
31
11
|
export function getArrayField(data, key) {
|
|
32
12
|
return Array.isArray(data[key]) ? data[key] : undefined;
|
|
33
13
|
}
|