@agwab/pi-workflow 0.1.2 → 0.2.0
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 +7 -13
- package/dist/compiler.d.ts +2 -0
- package/dist/compiler.js +27 -2
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js +3 -2
- package/dist/extension.js +201 -16
- package/dist/store.js +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/workflow-progress-health.d.ts +37 -0
- package/dist/workflow-progress-health.js +296 -0
- package/dist/workflow-runtime.d.ts +6 -0
- package/dist/workflow-runtime.js +33 -10
- package/dist/workflow-view.d.ts +2 -0
- package/dist/workflow-view.js +97 -18
- package/dist/workflow-web-source.js +32 -14
- package/docs/usage.md +1 -1
- package/package.json +6 -6
- package/src/compiler.ts +41 -2
- package/src/engine.ts +7 -16
- package/src/extension.ts +254 -22
- package/src/store.ts +1 -0
- package/src/types.ts +4 -0
- package/src/workflow-progress-health.ts +461 -0
- package/src/workflow-runtime.ts +50 -13
- package/src/workflow-view.ts +186 -41
- package/src/workflow-web-source.ts +192 -69
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +111 -37
- package/workflows/deep-research/helpers/final-audit-packet.mjs +191 -14
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +159 -50
- package/workflows/deep-research/helpers/render-executive.mjs +671 -37
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +624 -0
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +2 -0
- package/workflows/deep-research/schemas/deep-research-final-synthesis-control.schema.json +110 -0
- package/workflows/deep-research/spec.json +41 -11
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
appendFile,
|
|
4
|
+
mkdir,
|
|
5
|
+
readFile,
|
|
6
|
+
readdir,
|
|
7
|
+
rename,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from "node:fs/promises";
|
|
3
10
|
import { isIP } from "node:net";
|
|
4
11
|
import { dirname, resolve } from "node:path";
|
|
5
12
|
|
|
@@ -171,7 +178,9 @@ export function normalizeWorkflowWebSecurityPolicy(
|
|
|
171
178
|
};
|
|
172
179
|
}
|
|
173
180
|
|
|
174
|
-
export function isWorkflowWebSourceTool(
|
|
181
|
+
export function isWorkflowWebSourceTool(
|
|
182
|
+
tool: string,
|
|
183
|
+
): tool is WorkflowWebSourceTool {
|
|
175
184
|
return (WORKFLOW_WEB_SOURCE_TOOLS as readonly string[]).includes(tool);
|
|
176
185
|
}
|
|
177
186
|
|
|
@@ -202,7 +211,9 @@ export function consumeWorkflowWebVisibleBudget(
|
|
|
202
211
|
export function validateWorkflowWebUrl(
|
|
203
212
|
url: string,
|
|
204
213
|
security: WorkflowWebSecurityPolicy = DEFAULT_WORKFLOW_WEB_SECURITY_POLICY,
|
|
205
|
-
):
|
|
214
|
+
):
|
|
215
|
+
| { ok: true; normalizedUrl: string; domain: string }
|
|
216
|
+
| { ok: false; reason: string } {
|
|
206
217
|
let parsed: URL;
|
|
207
218
|
try {
|
|
208
219
|
parsed = new URL(url);
|
|
@@ -261,8 +272,8 @@ function sourceUrlDisplayCacheKey(url: string): string {
|
|
|
261
272
|
if (parsed.pathname.length > 1 && parsed.pathname.endsWith("/")) {
|
|
262
273
|
parsed.pathname = parsed.pathname.slice(0, -1);
|
|
263
274
|
}
|
|
264
|
-
const sortedParams = [...parsed.searchParams.entries()].sort(
|
|
265
|
-
left.localeCompare(right),
|
|
275
|
+
const sortedParams = [...parsed.searchParams.entries()].sort(
|
|
276
|
+
([left], [right]) => left.localeCompare(right),
|
|
266
277
|
);
|
|
267
278
|
parsed.search = "";
|
|
268
279
|
for (const [key, value] of sortedParams) {
|
|
@@ -283,8 +294,8 @@ function canonicalUrlForCache(url: string): string {
|
|
|
283
294
|
if (parsed.pathname.length > 1 && parsed.pathname.endsWith("/")) {
|
|
284
295
|
parsed.pathname = parsed.pathname.slice(0, -1);
|
|
285
296
|
}
|
|
286
|
-
const sortedParams = [...parsed.searchParams.entries()].sort(
|
|
287
|
-
left.localeCompare(right),
|
|
297
|
+
const sortedParams = [...parsed.searchParams.entries()].sort(
|
|
298
|
+
([left], [right]) => left.localeCompare(right),
|
|
288
299
|
);
|
|
289
300
|
parsed.search = "";
|
|
290
301
|
for (const [key, value] of sortedParams) {
|
|
@@ -392,13 +403,25 @@ export async function findWorkflowWebSourceByUrl(
|
|
|
392
403
|
const targetDisplayKey = sourceUrlDisplayCacheKey(redactedUrl);
|
|
393
404
|
const index = await readWorkflowWebSourceIndex(config);
|
|
394
405
|
const existing = [...index.sources].reverse().find((entry) => {
|
|
395
|
-
return sourceIndexEntryMatchesUrl(
|
|
406
|
+
return sourceIndexEntryMatchesUrl(
|
|
407
|
+
entry,
|
|
408
|
+
url,
|
|
409
|
+
redactedUrl,
|
|
410
|
+
targetKey,
|
|
411
|
+
targetDisplayKey,
|
|
412
|
+
);
|
|
396
413
|
});
|
|
397
414
|
if (existing) {
|
|
398
415
|
const source = await readWorkflowWebSource(config, existing.sourceRef);
|
|
399
416
|
if (source) return source;
|
|
400
417
|
}
|
|
401
|
-
return findWorkflowWebSourceByUrlFromSources(
|
|
418
|
+
return findWorkflowWebSourceByUrlFromSources(
|
|
419
|
+
config,
|
|
420
|
+
url,
|
|
421
|
+
redactedUrl,
|
|
422
|
+
targetKey,
|
|
423
|
+
targetDisplayKey,
|
|
424
|
+
);
|
|
402
425
|
}
|
|
403
426
|
|
|
404
427
|
function sourceIndexEntryMatchesUrl(
|
|
@@ -409,7 +432,11 @@ function sourceIndexEntryMatchesUrl(
|
|
|
409
432
|
targetDisplayKey: string,
|
|
410
433
|
): boolean {
|
|
411
434
|
if (entry.urlKey) return entry.urlKey === targetKey;
|
|
412
|
-
if (
|
|
435
|
+
if (
|
|
436
|
+
redactedUrlIdentityUnsafe(redactedUrl) ||
|
|
437
|
+
redactedUrlIdentityUnsafe(entry.redactedUrl) ||
|
|
438
|
+
redactedUrlIdentityUnsafe(entry.url)
|
|
439
|
+
) {
|
|
413
440
|
return false;
|
|
414
441
|
}
|
|
415
442
|
return (
|
|
@@ -421,7 +448,12 @@ function sourceIndexEntryMatchesUrl(
|
|
|
421
448
|
}
|
|
422
449
|
|
|
423
450
|
function redactedUrlIdentityUnsafe(url: string): boolean {
|
|
424
|
-
return
|
|
451
|
+
return (
|
|
452
|
+
/REDACTED/.test(url) ||
|
|
453
|
+
/[?&#][^=]*(?:token|secret|password|signature|sig|key|auth|session|credential)[^=]*=/i.test(
|
|
454
|
+
url,
|
|
455
|
+
)
|
|
456
|
+
);
|
|
425
457
|
}
|
|
426
458
|
|
|
427
459
|
async function findWorkflowWebSourceByUrlFromSources(
|
|
@@ -446,7 +478,11 @@ async function findWorkflowWebSourceByUrlFromSources(
|
|
|
446
478
|
if (source.urlKey === targetKey) return source;
|
|
447
479
|
continue;
|
|
448
480
|
}
|
|
449
|
-
if (
|
|
481
|
+
if (
|
|
482
|
+
redactedUrlIdentityUnsafe(redactedUrl) ||
|
|
483
|
+
redactedUrlIdentityUnsafe(source.redactedUrl) ||
|
|
484
|
+
redactedUrlIdentityUnsafe(source.url)
|
|
485
|
+
) {
|
|
450
486
|
continue;
|
|
451
487
|
}
|
|
452
488
|
if (
|
|
@@ -522,19 +558,21 @@ export function readWorkflowWebSourceSnippet(options: {
|
|
|
522
558
|
maxChars: number;
|
|
523
559
|
budget: WorkflowWebVisibleBudget;
|
|
524
560
|
}): WorkflowWebSourceReadResult {
|
|
525
|
-
return
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
561
|
+
return (
|
|
562
|
+
readWorkflowWebSourceSnippets({
|
|
563
|
+
source: options.source,
|
|
564
|
+
requests: [
|
|
565
|
+
{
|
|
566
|
+
query: options.query,
|
|
567
|
+
claim: options.claim,
|
|
568
|
+
terms: options.terms,
|
|
569
|
+
maxChars: options.maxChars,
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
maxChars: options.maxChars,
|
|
573
|
+
budget: options.budget,
|
|
574
|
+
})[0] ?? { status: "not_found", visibleChars: 0 }
|
|
575
|
+
);
|
|
538
576
|
}
|
|
539
577
|
|
|
540
578
|
export function readWorkflowWebSourceSnippets(options: {
|
|
@@ -573,10 +611,13 @@ export function extractTextFromToolResult(result: unknown): string {
|
|
|
573
611
|
.join("\n\n");
|
|
574
612
|
}
|
|
575
613
|
|
|
576
|
-
export function extractTitleFromToolResult(
|
|
614
|
+
export function extractTitleFromToolResult(
|
|
615
|
+
result: unknown,
|
|
616
|
+
): string | undefined {
|
|
577
617
|
if (!isRecord(result)) return undefined;
|
|
578
618
|
const details = result.details;
|
|
579
|
-
if (isRecord(details) && typeof details.title === "string")
|
|
619
|
+
if (isRecord(details) && typeof details.title === "string")
|
|
620
|
+
return details.title;
|
|
580
621
|
const text = extractTextFromToolResult(result);
|
|
581
622
|
const heading = text.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
582
623
|
return heading ? heading.slice(0, 200) : undefined;
|
|
@@ -660,7 +701,9 @@ function shouldKeepFragmentForCache(hash: string): boolean {
|
|
|
660
701
|
return raw.startsWith("/") || raw.startsWith("!") || raw.includes("?");
|
|
661
702
|
}
|
|
662
703
|
|
|
663
|
-
function sourceToIndexEntry(
|
|
704
|
+
function sourceToIndexEntry(
|
|
705
|
+
source: WorkflowWebSource,
|
|
706
|
+
): WorkflowWebSourceIndexEntry {
|
|
664
707
|
return {
|
|
665
708
|
sourceRef: source.sourceRef,
|
|
666
709
|
createdAt: source.createdAt,
|
|
@@ -717,7 +760,10 @@ function readWorkflowWebSourceSnippetWithCache(options: {
|
|
|
717
760
|
});
|
|
718
761
|
}
|
|
719
762
|
}
|
|
720
|
-
const termNeedles = prepareTermNeedles(
|
|
763
|
+
const termNeedles = prepareTermNeedles(
|
|
764
|
+
options.request.terms,
|
|
765
|
+
options.request.claim,
|
|
766
|
+
);
|
|
721
767
|
if (termNeedles.length === 0) return { status: "not_found", visibleChars: 0 };
|
|
722
768
|
return snippetForTerms({
|
|
723
769
|
text: options.source.text,
|
|
@@ -736,10 +782,19 @@ function snippetForTerms(options: {
|
|
|
736
782
|
budget: WorkflowWebVisibleBudget;
|
|
737
783
|
}): WorkflowWebSourceReadResult {
|
|
738
784
|
const needles = options.terms
|
|
739
|
-
.map((term) => ({
|
|
785
|
+
.map((term) => ({
|
|
786
|
+
raw: term,
|
|
787
|
+
normalized: normalizeForSearch(term).normalized,
|
|
788
|
+
}))
|
|
740
789
|
.filter((term) => term.normalized.length > 0);
|
|
741
790
|
if (needles.length === 0) return { status: "not_found", visibleChars: 0 };
|
|
742
|
-
const candidates: Array<{
|
|
791
|
+
const candidates: Array<{
|
|
792
|
+
start: number;
|
|
793
|
+
end: number;
|
|
794
|
+
matchedTerms: string[];
|
|
795
|
+
missingTerms: string[];
|
|
796
|
+
score: number;
|
|
797
|
+
}> = [];
|
|
743
798
|
for (const needle of needles) {
|
|
744
799
|
let fromIndex = 0;
|
|
745
800
|
let occurrenceCount = 0;
|
|
@@ -755,7 +810,9 @@ function snippetForTerms(options: {
|
|
|
755
810
|
normalizedIndex + Math.max(0, needle.normalized.length - 1),
|
|
756
811
|
);
|
|
757
812
|
const end = (options.normalizedSource.map[endMapIndex] ?? start) + 1;
|
|
758
|
-
candidates.push(
|
|
813
|
+
candidates.push(
|
|
814
|
+
scoreTermWindow(options.text, start, end, options.maxChars, needles),
|
|
815
|
+
);
|
|
759
816
|
fromIndex = normalizedIndex + Math.max(1, needle.normalized.length);
|
|
760
817
|
occurrenceCount += 1;
|
|
761
818
|
}
|
|
@@ -766,7 +823,11 @@ function snippetForTerms(options: {
|
|
|
766
823
|
return right.matchedTerms.length - left.matchedTerms.length;
|
|
767
824
|
})[0]!;
|
|
768
825
|
const raw = redactInlineSecrets(options.text.slice(best.start, best.end));
|
|
769
|
-
const consumed = consumeWorkflowWebVisibleBudget(
|
|
826
|
+
const consumed = consumeWorkflowWebVisibleBudget(
|
|
827
|
+
options.budget,
|
|
828
|
+
raw,
|
|
829
|
+
options.maxChars,
|
|
830
|
+
);
|
|
770
831
|
return {
|
|
771
832
|
status: "matched",
|
|
772
833
|
matchType: "terms",
|
|
@@ -787,7 +848,13 @@ function scoreTermWindow(
|
|
|
787
848
|
matchEnd: number,
|
|
788
849
|
maxChars: number,
|
|
789
850
|
terms: Array<{ raw: string; normalized: string }>,
|
|
790
|
-
): {
|
|
851
|
+
): {
|
|
852
|
+
start: number;
|
|
853
|
+
end: number;
|
|
854
|
+
matchedTerms: string[];
|
|
855
|
+
missingTerms: string[];
|
|
856
|
+
score: number;
|
|
857
|
+
} {
|
|
791
858
|
const center = Math.floor((matchStart + matchEnd) / 2);
|
|
792
859
|
const start = Math.max(0, center - Math.floor(maxChars / 2));
|
|
793
860
|
const end = Math.min(text.length, start + maxChars);
|
|
@@ -799,7 +866,10 @@ function scoreTermWindow(
|
|
|
799
866
|
.filter((term) => !windowNorm.includes(term.normalized))
|
|
800
867
|
.map((term) => term.raw);
|
|
801
868
|
const occurrenceScore = terms.reduce((score, term) => {
|
|
802
|
-
return
|
|
869
|
+
return (
|
|
870
|
+
score +
|
|
871
|
+
(windowNorm.includes(term.normalized) ? term.normalized.length : 0)
|
|
872
|
+
);
|
|
803
873
|
}, 0);
|
|
804
874
|
return {
|
|
805
875
|
start,
|
|
@@ -810,19 +880,27 @@ function scoreTermWindow(
|
|
|
810
880
|
};
|
|
811
881
|
}
|
|
812
882
|
|
|
813
|
-
function prepareTermNeedles(
|
|
814
|
-
|
|
883
|
+
function prepareTermNeedles(
|
|
884
|
+
terms: string[] | undefined,
|
|
885
|
+
claim: string | undefined,
|
|
886
|
+
): string[] {
|
|
887
|
+
const explicitTerms = dedupeStrings(
|
|
888
|
+
(terms ?? []).map((term) => term.trim()).filter(Boolean),
|
|
889
|
+
);
|
|
815
890
|
if (explicitTerms.length > 0) return explicitTerms.slice(0, 16);
|
|
816
891
|
if (!claim?.trim()) return [];
|
|
817
892
|
return extractClaimTerms(claim).slice(0, 16);
|
|
818
893
|
}
|
|
819
894
|
|
|
820
895
|
function extractClaimTerms(claim: string): string[] {
|
|
821
|
-
const tokens =
|
|
822
|
-
|
|
823
|
-
|
|
896
|
+
const tokens =
|
|
897
|
+
claim
|
|
898
|
+
.match(/[\p{L}\p{N}][\p{L}\p{N}._/-]{2,}/gu)
|
|
899
|
+
?.map((token) => token.toLowerCase()) ?? [];
|
|
824
900
|
const filtered = tokens.filter((token) => !SOURCE_READ_STOPWORDS.has(token));
|
|
825
|
-
return dedupeStrings(filtered).sort(
|
|
901
|
+
return dedupeStrings(filtered).sort(
|
|
902
|
+
(left, right) => right.length - left.length,
|
|
903
|
+
);
|
|
826
904
|
}
|
|
827
905
|
|
|
828
906
|
function dedupeStrings(values: string[]): string[] {
|
|
@@ -889,7 +967,10 @@ function snippetForMatch(options: {
|
|
|
889
967
|
const slack = Math.max(0, options.maxChars - matchLength);
|
|
890
968
|
const before = Math.floor(slack / 2);
|
|
891
969
|
const snippetStart = Math.max(0, options.start - before);
|
|
892
|
-
const snippetEnd = Math.min(
|
|
970
|
+
const snippetEnd = Math.min(
|
|
971
|
+
options.text.length,
|
|
972
|
+
snippetStart + options.maxChars,
|
|
973
|
+
);
|
|
893
974
|
const raw = redactInlineSecrets(options.text.slice(snippetStart, snippetEnd));
|
|
894
975
|
const consumed = consumeWorkflowWebVisibleBudget(
|
|
895
976
|
options.budget,
|
|
@@ -906,7 +987,10 @@ function snippetForMatch(options: {
|
|
|
906
987
|
};
|
|
907
988
|
}
|
|
908
989
|
|
|
909
|
-
function normalizeForSearch(text: string): {
|
|
990
|
+
function normalizeForSearch(text: string): {
|
|
991
|
+
normalized: string;
|
|
992
|
+
map: number[];
|
|
993
|
+
} {
|
|
910
994
|
let normalized = "";
|
|
911
995
|
const map: number[] = [];
|
|
912
996
|
let previousWhitespace = false;
|
|
@@ -945,8 +1029,13 @@ async function readWorkflowWebSourceIndexFile(
|
|
|
945
1029
|
config: WorkflowWebSourceCacheConfig,
|
|
946
1030
|
): Promise<WorkflowWebSourceIndex> {
|
|
947
1031
|
try {
|
|
948
|
-
const parsed = JSON.parse(
|
|
949
|
-
|
|
1032
|
+
const parsed = JSON.parse(
|
|
1033
|
+
await readFile(indexPath(config), "utf8"),
|
|
1034
|
+
) as unknown;
|
|
1035
|
+
if (
|
|
1036
|
+
!isRecord(parsed) ||
|
|
1037
|
+
parsed.schema !== WORKFLOW_WEB_SOURCE_INDEX_SCHEMA
|
|
1038
|
+
) {
|
|
950
1039
|
throw new Error("invalid index");
|
|
951
1040
|
}
|
|
952
1041
|
const sources = Array.isArray(parsed.sources)
|
|
@@ -957,7 +1046,10 @@ async function readWorkflowWebSourceIndexFile(
|
|
|
957
1046
|
: [];
|
|
958
1047
|
return {
|
|
959
1048
|
schema: WORKFLOW_WEB_SOURCE_INDEX_SCHEMA,
|
|
960
|
-
updatedAt:
|
|
1049
|
+
updatedAt:
|
|
1050
|
+
typeof parsed.updatedAt === "string"
|
|
1051
|
+
? parsed.updatedAt
|
|
1052
|
+
: new Date().toISOString(),
|
|
961
1053
|
runId: typeof parsed.runId === "string" ? parsed.runId : config.runId,
|
|
962
1054
|
sources: mergeSourceIndexEntries(sources),
|
|
963
1055
|
};
|
|
@@ -998,7 +1090,11 @@ async function readWorkflowWebSourceIndexLedger(
|
|
|
998
1090
|
if (!line.trim()) continue;
|
|
999
1091
|
try {
|
|
1000
1092
|
const parsed = JSON.parse(line) as unknown;
|
|
1001
|
-
if (
|
|
1093
|
+
if (
|
|
1094
|
+
!isRecord(parsed) ||
|
|
1095
|
+
parsed.schema !== WORKFLOW_WEB_SOURCE_INDEX_EVENT_SCHEMA
|
|
1096
|
+
)
|
|
1097
|
+
continue;
|
|
1002
1098
|
const entry = sourceIndexEntryFromUnknown(parsed.entry);
|
|
1003
1099
|
if (entry) entries.push(entry);
|
|
1004
1100
|
} catch {
|
|
@@ -1008,9 +1104,15 @@ async function readWorkflowWebSourceIndexLedger(
|
|
|
1008
1104
|
return entries;
|
|
1009
1105
|
}
|
|
1010
1106
|
|
|
1011
|
-
function sourceIndexEntryFromUnknown(
|
|
1107
|
+
function sourceIndexEntryFromUnknown(
|
|
1108
|
+
value: unknown,
|
|
1109
|
+
): WorkflowWebSourceIndexEntry | undefined {
|
|
1012
1110
|
if (!isRecord(value)) return undefined;
|
|
1013
|
-
if (
|
|
1111
|
+
if (
|
|
1112
|
+
typeof value.sourceRef !== "string" ||
|
|
1113
|
+
!isWorkflowWebSourceRef(value.sourceRef)
|
|
1114
|
+
)
|
|
1115
|
+
return undefined;
|
|
1014
1116
|
if (typeof value.createdAt !== "string") return undefined;
|
|
1015
1117
|
if (typeof value.url !== "string") return undefined;
|
|
1016
1118
|
if (typeof value.redactedUrl !== "string") return undefined;
|
|
@@ -1036,7 +1138,9 @@ function mergeSourceIndexEntries(
|
|
|
1036
1138
|
): WorkflowWebSourceIndexEntry[] {
|
|
1037
1139
|
const bySourceRef = new Map<string, WorkflowWebSourceIndexEntry>();
|
|
1038
1140
|
for (const entry of entries) bySourceRef.set(entry.sourceRef, entry);
|
|
1039
|
-
return [...bySourceRef.values()].sort((left, right) =>
|
|
1141
|
+
return [...bySourceRef.values()].sort((left, right) =>
|
|
1142
|
+
left.createdAt.localeCompare(right.createdAt),
|
|
1143
|
+
);
|
|
1040
1144
|
}
|
|
1041
1145
|
|
|
1042
1146
|
function emptyWorkflowWebSourceIndex(
|
|
@@ -1101,27 +1205,38 @@ function nonPublicIpReason(address: string): string | undefined {
|
|
|
1101
1205
|
if (hexMapped) {
|
|
1102
1206
|
const high = Number.parseInt(hexMapped[1]!, 16);
|
|
1103
1207
|
const low = Number.parseInt(hexMapped[2]!, 16);
|
|
1104
|
-
return nonPublicIpReason(
|
|
1208
|
+
return nonPublicIpReason(
|
|
1209
|
+
`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`,
|
|
1210
|
+
);
|
|
1105
1211
|
}
|
|
1106
1212
|
if (isIP(lower) === 4) {
|
|
1107
1213
|
const parts = lower.split(".").map((part) => Number(part));
|
|
1108
|
-
if (
|
|
1214
|
+
if (
|
|
1215
|
+
parts.length !== 4 ||
|
|
1216
|
+
parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
|
|
1217
|
+
)
|
|
1218
|
+
return "non_public_ip_blocked";
|
|
1109
1219
|
const [a, b, c, d] = parts as [number, number, number, number];
|
|
1110
|
-
if (a === 0 || a === 10 || a === 127 || a >= 224)
|
|
1220
|
+
if (a === 0 || a === 10 || a === 127 || a >= 224)
|
|
1221
|
+
return "non_public_ip_blocked";
|
|
1111
1222
|
if (a === 100 && b >= 64 && b <= 127) return "non_public_ip_blocked";
|
|
1112
1223
|
if (a === 169 && b === 254) return "non_public_ip_blocked";
|
|
1113
1224
|
if (a === 172 && b >= 16 && b <= 31) return "non_public_ip_blocked";
|
|
1114
1225
|
if (a === 192 && b === 168) return "non_public_ip_blocked";
|
|
1115
|
-
if (a === 192 && b === 0 && (c === 0 || c === 2))
|
|
1226
|
+
if (a === 192 && b === 0 && (c === 0 || c === 2))
|
|
1227
|
+
return "non_public_ip_blocked";
|
|
1116
1228
|
if (a === 198 && (b === 18 || b === 19)) return "non_public_ip_blocked";
|
|
1117
1229
|
if (a === 198 && b === 51 && c === 100) return "non_public_ip_blocked";
|
|
1118
1230
|
if (a === 203 && b === 0 && c === 113) return "non_public_ip_blocked";
|
|
1119
|
-
if (a === 255 && b === 255 && c === 255 && d === 255)
|
|
1231
|
+
if (a === 255 && b === 255 && c === 255 && d === 255)
|
|
1232
|
+
return "non_public_ip_blocked";
|
|
1120
1233
|
}
|
|
1121
1234
|
if (isIP(lower) === 6) {
|
|
1122
1235
|
if (lower === "::" || lower === "::1") return "non_public_ip_blocked";
|
|
1123
|
-
if (lower.startsWith("fc") || lower.startsWith("fd"))
|
|
1124
|
-
|
|
1236
|
+
if (lower.startsWith("fc") || lower.startsWith("fd"))
|
|
1237
|
+
return "non_public_ip_blocked";
|
|
1238
|
+
if (lower.startsWith("fe80") || lower.startsWith("ff"))
|
|
1239
|
+
return "non_public_ip_blocked";
|
|
1125
1240
|
if (lower.startsWith("2001:db8")) return "non_public_ip_blocked";
|
|
1126
1241
|
}
|
|
1127
1242
|
return undefined;
|
|
@@ -1131,13 +1246,18 @@ function redactRecordForModel(
|
|
|
1131
1246
|
value: Record<string, unknown>,
|
|
1132
1247
|
): Record<string, unknown> {
|
|
1133
1248
|
return Object.fromEntries(
|
|
1134
|
-
Object.entries(value).map(([key, item]) => [
|
|
1249
|
+
Object.entries(value).map(([key, item]) => [
|
|
1250
|
+
key,
|
|
1251
|
+
redactValueForModel(item),
|
|
1252
|
+
]),
|
|
1135
1253
|
);
|
|
1136
1254
|
}
|
|
1137
1255
|
|
|
1138
1256
|
function redactValueForModel(value: unknown): unknown {
|
|
1139
|
-
if (typeof value === "string")
|
|
1140
|
-
|
|
1257
|
+
if (typeof value === "string")
|
|
1258
|
+
return redactInlineSecrets(sanitizeUrlMaybe(value));
|
|
1259
|
+
if (Array.isArray(value))
|
|
1260
|
+
return value.map((item) => redactValueForModel(item));
|
|
1141
1261
|
if (!isRecord(value)) return value;
|
|
1142
1262
|
return redactRecordForModel(value);
|
|
1143
1263
|
}
|
|
@@ -1147,15 +1267,18 @@ function sanitizeUrlMaybe(value: string): string {
|
|
|
1147
1267
|
}
|
|
1148
1268
|
|
|
1149
1269
|
function redactInlineSecrets(value: string): string {
|
|
1150
|
-
const withSanitizedUrls = value.replace(
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1270
|
+
const withSanitizedUrls = value.replace(
|
|
1271
|
+
/https?:\/\/[^\s)\]}>"']+/gi,
|
|
1272
|
+
(match) => {
|
|
1273
|
+
const trailing = match.match(/[.,;:!?]+$/)?.[0] ?? "";
|
|
1274
|
+
const core = trailing ? match.slice(0, -trailing.length) : match;
|
|
1275
|
+
try {
|
|
1276
|
+
return `${sanitizeParsedUrlForModel(new URL(core))}${trailing}`;
|
|
1277
|
+
} catch {
|
|
1278
|
+
return match;
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
);
|
|
1159
1282
|
return redactInlineSecretsNoUrls(withSanitizedUrls);
|
|
1160
1283
|
}
|
|
1161
1284
|
|