@erichll/pi-auto-review 0.4.0 → 0.5.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/CHANGELOG.md +23 -0
- package/README.md +182 -177
- package/package.json +1 -1
- package/src/broker/broker.ts +5 -5
- package/src/config.json +2 -1
- package/src/index.ts +1144 -113
- package/src/policy.ts +1074 -74
package/src/policy.ts
CHANGED
|
@@ -47,16 +47,105 @@ export type TranscriptConfig = {
|
|
|
47
47
|
|
|
48
48
|
export type TranscriptResult = {
|
|
49
49
|
text: string;
|
|
50
|
+
surfaceProfile: EvidenceSurfaceProfile;
|
|
51
|
+
reviewerEvidence: {
|
|
52
|
+
userMessages: ReviewerEvidenceItem[];
|
|
53
|
+
toolCalls: ReviewerEvidenceItem[];
|
|
54
|
+
relevantResults: ReviewerEvidenceItem[];
|
|
55
|
+
};
|
|
56
|
+
budgetRemovals: ReviewerBudgetRemoval[];
|
|
50
57
|
userCharacters: number;
|
|
51
58
|
toolCharacters: number;
|
|
52
59
|
relevantResultCharacters: number;
|
|
53
60
|
truncated: boolean;
|
|
61
|
+
selectedCandidates: EvidenceCandidateMetadata[];
|
|
62
|
+
failureCode?:
|
|
63
|
+
| "critical_evidence_overflow"
|
|
64
|
+
| "required_profile_overflow"
|
|
65
|
+
| "required_user_constraint_overflow"
|
|
66
|
+
| "reviewer_input_budget_exceeded";
|
|
67
|
+
userAuthorizationCeiling: "unknown" | "low" | "medium" | "high";
|
|
68
|
+
userConstraint: "none" | "narrowed" | "revoked";
|
|
69
|
+
compactionState: "none" | "summary-present" | "authorization-unavailable";
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type ReviewerBudgetRemoval = {
|
|
73
|
+
reason:
|
|
74
|
+
| "secondary-reasons"
|
|
75
|
+
| "older-structured-tool"
|
|
76
|
+
| "optional-result";
|
|
77
|
+
count: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type ReviewerEvidenceItem = {
|
|
81
|
+
id: string;
|
|
82
|
+
reason: EvidenceSelectionReason;
|
|
83
|
+
secondaryReasons: EvidenceSelectionReason[];
|
|
84
|
+
toolCallId?: string;
|
|
85
|
+
content: string;
|
|
54
86
|
};
|
|
55
87
|
|
|
56
88
|
type Evidence = {
|
|
89
|
+
id: string;
|
|
57
90
|
index: number;
|
|
91
|
+
entryIndex: number;
|
|
58
92
|
kind: "user" | "tool";
|
|
59
93
|
text: string;
|
|
94
|
+
toolCallId?: string;
|
|
95
|
+
reason: EvidenceSelectionReason;
|
|
96
|
+
secondaryReasons: EvidenceSelectionReason[];
|
|
97
|
+
sensitivity: EvidenceSensitivity;
|
|
98
|
+
originalCharacters: number;
|
|
99
|
+
preTruncated: boolean;
|
|
100
|
+
representationCharacters?: number;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type EvidenceSensitivity =
|
|
104
|
+
| "user-intent"
|
|
105
|
+
| "tool-input"
|
|
106
|
+
| "tool-output"
|
|
107
|
+
| "boundary"
|
|
108
|
+
| "credential"
|
|
109
|
+
| "permission-control"
|
|
110
|
+
| "authorization-persistence"
|
|
111
|
+
| "transport-weakening"
|
|
112
|
+
| "audit-control"
|
|
113
|
+
| "security-control";
|
|
114
|
+
|
|
115
|
+
export type EvidenceSelectionReason =
|
|
116
|
+
| "latest-user"
|
|
117
|
+
| "exact-request-reference"
|
|
118
|
+
| "active-narrowing-constraint"
|
|
119
|
+
| "trusted-retry-user-message"
|
|
120
|
+
| "exact-tool-call"
|
|
121
|
+
| "structured-request-match"
|
|
122
|
+
| "security-combination"
|
|
123
|
+
| "same-tool"
|
|
124
|
+
| "delete-precheck"
|
|
125
|
+
| "git-push-context"
|
|
126
|
+
| "provider-branch-protection"
|
|
127
|
+
| "sandbox-trap";
|
|
128
|
+
|
|
129
|
+
export type EvidenceSurfaceProfile =
|
|
130
|
+
| "network"
|
|
131
|
+
| "delete"
|
|
132
|
+
| "git-push"
|
|
133
|
+
| "forwarded"
|
|
134
|
+
| "generic";
|
|
135
|
+
|
|
136
|
+
export type EvidenceCandidateMetadata = {
|
|
137
|
+
id: string;
|
|
138
|
+
entryIndex: number;
|
|
139
|
+
kind: "user" | "tool-call" | "tool-result" | "sandbox-trap";
|
|
140
|
+
toolCallId?: string;
|
|
141
|
+
reason: EvidenceSelectionReason;
|
|
142
|
+
secondaryReasons: EvidenceSelectionReason[];
|
|
143
|
+
surfaceProfile: EvidenceSurfaceProfile;
|
|
144
|
+
originalCharacters: number;
|
|
145
|
+
selectedCharacters: number;
|
|
146
|
+
estimatedTokens: number;
|
|
147
|
+
truncated: boolean;
|
|
148
|
+
sensitivity: EvidenceSensitivity;
|
|
60
149
|
};
|
|
61
150
|
|
|
62
151
|
export type RelevantBoundaryRequest = {
|
|
@@ -64,16 +153,40 @@ export type RelevantBoundaryRequest = {
|
|
|
64
153
|
source?: string;
|
|
65
154
|
surface?: string;
|
|
66
155
|
operation?: string;
|
|
156
|
+
cwd?: string;
|
|
67
157
|
command?: string;
|
|
68
158
|
path?: string;
|
|
69
159
|
resolvedPath?: string;
|
|
70
160
|
destination?: string;
|
|
71
161
|
toolCallId?: string;
|
|
72
162
|
toolName?: string;
|
|
163
|
+
toolInputPreview?: string;
|
|
164
|
+
agentName?: string;
|
|
165
|
+
requesterSessionId?: string;
|
|
166
|
+
accessIntent?: {
|
|
167
|
+
surface: string;
|
|
168
|
+
matchValues: readonly string[];
|
|
169
|
+
boundaryValue?: string;
|
|
170
|
+
};
|
|
171
|
+
trustedRetryOriginalRequestId?: string;
|
|
73
172
|
};
|
|
74
173
|
|
|
75
174
|
const MAX_EVIDENCE_ITEM_CHARACTERS = 4_000;
|
|
76
175
|
|
|
176
|
+
/** Stable compact JSON for reviewer-only representations. */
|
|
177
|
+
export function canonicalReviewerJson(value: unknown): string {
|
|
178
|
+
return JSON.stringify(value, (_key, current: unknown) => {
|
|
179
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
180
|
+
return current;
|
|
181
|
+
}
|
|
182
|
+
return Object.fromEntries(
|
|
183
|
+
Object.keys(current as Record<string, unknown>)
|
|
184
|
+
.sort()
|
|
185
|
+
.map((key) => [key, (current as Record<string, unknown>)[key]]),
|
|
186
|
+
);
|
|
187
|
+
}) ?? "null";
|
|
188
|
+
}
|
|
189
|
+
|
|
77
190
|
function exactKeys(
|
|
78
191
|
record: Record<string, unknown>,
|
|
79
192
|
expected: readonly string[],
|
|
@@ -492,7 +605,9 @@ function userText(message: Record<string, unknown>): string {
|
|
|
492
605
|
.trim();
|
|
493
606
|
}
|
|
494
607
|
|
|
495
|
-
function toolTexts(
|
|
608
|
+
function toolTexts(
|
|
609
|
+
message: Record<string, unknown>,
|
|
610
|
+
): Array<{ id?: string; name: string; text: string }> {
|
|
496
611
|
if (message.role !== "assistant" || !Array.isArray(message.content)) {
|
|
497
612
|
return [];
|
|
498
613
|
}
|
|
@@ -502,13 +617,23 @@ function toolTexts(message: Record<string, unknown>): string[] {
|
|
|
502
617
|
if (record.type !== "toolCall" || typeof record.name !== "string") {
|
|
503
618
|
return [];
|
|
504
619
|
}
|
|
505
|
-
|
|
620
|
+
const id =
|
|
621
|
+
typeof record.id === "string"
|
|
622
|
+
? record.id
|
|
623
|
+
: typeof record.toolCallId === "string"
|
|
624
|
+
? record.toolCallId
|
|
625
|
+
: undefined;
|
|
626
|
+
return [{
|
|
627
|
+
id,
|
|
628
|
+
name: record.name,
|
|
629
|
+
text: `${record.name} ${boundedString(record.arguments ?? {})}`,
|
|
630
|
+
}];
|
|
506
631
|
});
|
|
507
632
|
}
|
|
508
633
|
|
|
509
634
|
function extractEvidence(entries: readonly unknown[]): Evidence[] {
|
|
510
635
|
const evidence: Evidence[] = [];
|
|
511
|
-
entries.forEach((entry) => {
|
|
636
|
+
entries.forEach((entry, entryIndex) => {
|
|
512
637
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return;
|
|
513
638
|
const message = (entry as Record<string, unknown>).message;
|
|
514
639
|
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
@@ -518,13 +643,34 @@ function extractEvidence(entries: readonly unknown[]): Evidence[] {
|
|
|
518
643
|
const user = userText(record);
|
|
519
644
|
if (user) {
|
|
520
645
|
evidence.push({
|
|
646
|
+
id: `${stableEntryId(entry, entryIndex)}:user`,
|
|
521
647
|
index: evidence.length,
|
|
648
|
+
entryIndex,
|
|
522
649
|
kind: "user",
|
|
523
650
|
text: boundedString(user),
|
|
651
|
+
reason: "latest-user",
|
|
652
|
+
secondaryReasons: [],
|
|
653
|
+
sensitivity: "user-intent",
|
|
654
|
+
originalCharacters: user.length,
|
|
655
|
+
preTruncated: boundedString(user).length < user.length,
|
|
524
656
|
});
|
|
525
657
|
}
|
|
526
658
|
for (const tool of toolTexts(record)) {
|
|
527
|
-
evidence.push({
|
|
659
|
+
evidence.push({
|
|
660
|
+
id: tool.id
|
|
661
|
+
? `tool-call:${tool.id}`
|
|
662
|
+
: `${stableEntryId(entry, entryIndex)}:tool-${evidence.length}`,
|
|
663
|
+
index: evidence.length,
|
|
664
|
+
entryIndex,
|
|
665
|
+
kind: "tool",
|
|
666
|
+
text: tool.text,
|
|
667
|
+
toolCallId: tool.id,
|
|
668
|
+
reason: "structured-request-match",
|
|
669
|
+
secondaryReasons: [],
|
|
670
|
+
sensitivity: "tool-input",
|
|
671
|
+
originalCharacters: tool.text.length,
|
|
672
|
+
preTruncated: false,
|
|
673
|
+
});
|
|
528
674
|
}
|
|
529
675
|
});
|
|
530
676
|
return evidence;
|
|
@@ -535,6 +681,8 @@ type ToolCallRecord = {
|
|
|
535
681
|
name: string;
|
|
536
682
|
arguments: unknown;
|
|
537
683
|
rendered: string;
|
|
684
|
+
entryIndex: number;
|
|
685
|
+
candidateId: string;
|
|
538
686
|
};
|
|
539
687
|
|
|
540
688
|
function messageRecord(entry: unknown): Record<string, unknown> | undefined {
|
|
@@ -544,6 +692,14 @@ function messageRecord(entry: unknown): Record<string, unknown> | undefined {
|
|
|
544
692
|
return message as Record<string, unknown>;
|
|
545
693
|
}
|
|
546
694
|
|
|
695
|
+
function stableEntryId(entry: unknown, index: number): string {
|
|
696
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
697
|
+
const id = (entry as Record<string, unknown>).id;
|
|
698
|
+
if (typeof id === "string" && id.trim()) return `entry:${id}`;
|
|
699
|
+
}
|
|
700
|
+
return `entry-index:${index}`;
|
|
701
|
+
}
|
|
702
|
+
|
|
547
703
|
function redactSensitiveResult(value: string): string {
|
|
548
704
|
return value
|
|
549
705
|
.replace(
|
|
@@ -668,20 +824,6 @@ function relevanceReason(
|
|
|
668
824
|
| "provider-branch-protection"
|
|
669
825
|
| undefined {
|
|
670
826
|
if (request.toolCallId && call.id === request.toolCallId) return "same-tool";
|
|
671
|
-
const callText = call.rendered.toLowerCase();
|
|
672
|
-
const needles = [
|
|
673
|
-
request.path,
|
|
674
|
-
request.resolvedPath,
|
|
675
|
-
request.destination,
|
|
676
|
-
request.command,
|
|
677
|
-
]
|
|
678
|
-
.filter((value): value is string => Boolean(value && value.length >= 3))
|
|
679
|
-
.map((value) => value.toLowerCase());
|
|
680
|
-
const namesMatch =
|
|
681
|
-
Boolean(request.toolName) &&
|
|
682
|
-
(call.name === request.toolName ||
|
|
683
|
-
call.name.endsWith(`/${request.toolName}`) ||
|
|
684
|
-
request.toolName?.endsWith(`/${call.name}`));
|
|
685
827
|
const currentCommand = request.command || "";
|
|
686
828
|
const priorCommand = commandArgument(call);
|
|
687
829
|
const pushedBranch = explicitPushBranch(currentCommand);
|
|
@@ -690,13 +832,19 @@ function relevanceReason(
|
|
|
690
832
|
/\b(?:rm|rmdir|unlink|trash|delete)\b/i.test(currentCommand);
|
|
691
833
|
const readOnlyCheck =
|
|
692
834
|
/^\s*(?:stat|ls|find|test|readlink|realpath)\b/i.test(priorCommand);
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
835
|
+
const priorWords = /[\n;&|`<>]|\$\(/.test(priorCommand)
|
|
836
|
+
? []
|
|
837
|
+
: priorCommand
|
|
838
|
+
.trim()
|
|
839
|
+
.split(/\s+/)
|
|
840
|
+
.map((word) => word.replace(/^(["'])(.*)\1$/, "$2"));
|
|
841
|
+
const targets = [request.path, request.resolvedPath].filter(
|
|
842
|
+
(value): value is string => Boolean(value),
|
|
843
|
+
);
|
|
696
844
|
if (
|
|
697
845
|
destructive &&
|
|
698
846
|
readOnlyCheck &&
|
|
699
|
-
targets.some((target) =>
|
|
847
|
+
targets.some((target) => priorWords.includes(target))
|
|
700
848
|
) {
|
|
701
849
|
return "delete-precheck";
|
|
702
850
|
}
|
|
@@ -715,21 +863,40 @@ function relevanceReason(
|
|
|
715
863
|
) {
|
|
716
864
|
return "provider-branch-protection";
|
|
717
865
|
}
|
|
718
|
-
if (
|
|
719
|
-
(namesMatch || needles.length > 0) &&
|
|
720
|
-
needles.some((value) => callText.includes(value))
|
|
721
|
-
) {
|
|
722
|
-
return "same-tool";
|
|
723
|
-
}
|
|
724
866
|
return undefined;
|
|
725
867
|
}
|
|
726
868
|
|
|
727
869
|
function relevantResultEvidence(
|
|
728
870
|
entries: readonly unknown[],
|
|
729
871
|
request: RelevantBoundaryRequest,
|
|
730
|
-
|
|
872
|
+
currentTurnStart: number,
|
|
873
|
+
): Array<{
|
|
874
|
+
index: number;
|
|
875
|
+
reason: Exclude<
|
|
876
|
+
EvidenceSelectionReason,
|
|
877
|
+
| "latest-user"
|
|
878
|
+
| "exact-request-reference"
|
|
879
|
+
| "active-narrowing-constraint"
|
|
880
|
+
| "trusted-retry-user-message"
|
|
881
|
+
| "exact-tool-call"
|
|
882
|
+
| "structured-request-match"
|
|
883
|
+
| "security-combination"
|
|
884
|
+
| "sandbox-trap"
|
|
885
|
+
>;
|
|
886
|
+
call: ToolCallRecord;
|
|
887
|
+
resultId: string;
|
|
888
|
+
resultText: string;
|
|
889
|
+
renderedResult: string;
|
|
890
|
+
}> {
|
|
731
891
|
const calls = new Map<string, ToolCallRecord>();
|
|
732
|
-
const results: Array<{
|
|
892
|
+
const results: Array<{
|
|
893
|
+
index: number;
|
|
894
|
+
reason: "same-tool" | "delete-precheck" | "git-push-context" | "provider-branch-protection";
|
|
895
|
+
call: ToolCallRecord;
|
|
896
|
+
resultId: string;
|
|
897
|
+
resultText: string;
|
|
898
|
+
renderedResult: string;
|
|
899
|
+
}> = [];
|
|
733
900
|
entries.forEach((entry, index) => {
|
|
734
901
|
const message = messageRecord(entry);
|
|
735
902
|
if (!message) return;
|
|
@@ -750,6 +917,8 @@ function relevantResultEvidence(
|
|
|
750
917
|
name: record.name,
|
|
751
918
|
arguments: record.arguments,
|
|
752
919
|
rendered: `${record.name} ${boundedString(record.arguments ?? {})}`,
|
|
920
|
+
entryIndex: index,
|
|
921
|
+
candidateId: `tool-call:${id}`,
|
|
753
922
|
});
|
|
754
923
|
}
|
|
755
924
|
return;
|
|
@@ -764,58 +933,728 @@ function relevantResultEvidence(
|
|
|
764
933
|
if (!call) return;
|
|
765
934
|
const reason = relevanceReason(call, request);
|
|
766
935
|
const text = resultText(message);
|
|
767
|
-
|
|
936
|
+
const crossTurnAllowed = reason === "same-tool";
|
|
937
|
+
if (reason && text && (crossTurnAllowed || call.entryIndex >= currentTurnStart)) {
|
|
768
938
|
results.push({
|
|
769
939
|
index,
|
|
770
|
-
|
|
940
|
+
reason,
|
|
941
|
+
call,
|
|
942
|
+
resultId: `${stableEntryId(entry, index)}:tool-result:${message.toolCallId}`,
|
|
943
|
+
resultText: text,
|
|
944
|
+
renderedResult: `<tool-result id="${escapeEvidenceMarkup(message.toolCallId)}" reason="${reason}" tool="${escapeEvidenceMarkup(call.name)}">\n${escapeEvidenceMarkup(text)}\n</tool-result>`,
|
|
771
945
|
});
|
|
772
946
|
}
|
|
773
947
|
});
|
|
774
948
|
return results;
|
|
775
949
|
}
|
|
776
950
|
|
|
777
|
-
function
|
|
951
|
+
function sandboxTrapSupplement(
|
|
778
952
|
request: RelevantBoundaryRequest,
|
|
779
953
|
): string | undefined {
|
|
780
954
|
if (request.source !== "sandbox-runtime") return;
|
|
955
|
+
const supplement = {
|
|
956
|
+
...(request.toolName ? { process: request.toolName } : {}),
|
|
957
|
+
};
|
|
958
|
+
if (Object.keys(supplement).length === 0) return;
|
|
959
|
+
return canonicalReviewerJson(supplement);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function sandboxTrapEvidence(
|
|
963
|
+
request: RelevantBoundaryRequest,
|
|
964
|
+
): string | undefined {
|
|
965
|
+
const supplement = sandboxTrapSupplement(request);
|
|
966
|
+
if (!supplement) return;
|
|
781
967
|
return `<sandbox-trap>
|
|
782
|
-
${
|
|
783
|
-
|
|
784
|
-
|
|
968
|
+
${supplement}
|
|
969
|
+
</sandbox-trap>`;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function currentTurnStart(entries: readonly unknown[]): number {
|
|
973
|
+
let start = 0;
|
|
974
|
+
entries.forEach((entry, index) => {
|
|
975
|
+
if (messageRecord(entry)?.role === "user") start = index;
|
|
976
|
+
});
|
|
977
|
+
return start;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function surfaceProfile(
|
|
981
|
+
request: RelevantBoundaryRequest,
|
|
982
|
+
): EvidenceSurfaceProfile {
|
|
983
|
+
if (request.agentName || request.requesterSessionId) return "forwarded";
|
|
984
|
+
if (request.surface === "network" || request.destination) return "network";
|
|
985
|
+
if (/\bgit\b[\s\S]*\bpush\b/i.test(request.command ?? "")) return "git-push";
|
|
986
|
+
if (/\b(?:rm|rmdir|unlink|trash|delete)\b/i.test(request.command ?? "")) {
|
|
987
|
+
return "delete";
|
|
988
|
+
}
|
|
989
|
+
return "generic";
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function collectToolCalls(entries: readonly unknown[]): ToolCallRecord[] {
|
|
993
|
+
const calls: ToolCallRecord[] = [];
|
|
994
|
+
entries.forEach((entry, entryIndex) => {
|
|
995
|
+
const message = messageRecord(entry);
|
|
996
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) return;
|
|
997
|
+
for (const part of message.content) {
|
|
998
|
+
if (!part || typeof part !== "object" || Array.isArray(part)) continue;
|
|
999
|
+
const value = part as Record<string, unknown>;
|
|
1000
|
+
if (value.type !== "toolCall" || typeof value.name !== "string") continue;
|
|
1001
|
+
const id =
|
|
1002
|
+
typeof value.id === "string"
|
|
1003
|
+
? value.id
|
|
1004
|
+
: typeof value.toolCallId === "string"
|
|
1005
|
+
? value.toolCallId
|
|
1006
|
+
: undefined;
|
|
1007
|
+
calls.push({
|
|
1008
|
+
id,
|
|
1009
|
+
name: value.name,
|
|
1010
|
+
arguments: value.arguments,
|
|
1011
|
+
rendered: `${value.name} ${boundedString(value.arguments ?? {})}`,
|
|
1012
|
+
entryIndex,
|
|
1013
|
+
candidateId: id
|
|
1014
|
+
? `tool-call:${id}`
|
|
1015
|
+
: `${stableEntryId(entry, entryIndex)}:tool-${calls.length}`,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
return calls;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function exactStructuredMatch(
|
|
1023
|
+
call: ToolCallRecord,
|
|
1024
|
+
request: RelevantBoundaryRequest,
|
|
1025
|
+
): boolean {
|
|
1026
|
+
if (!call.arguments || typeof call.arguments !== "object" || Array.isArray(call.arguments)) {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
const args = call.arguments as Record<string, unknown>;
|
|
1030
|
+
const pairs: Array<[unknown, unknown]> = [
|
|
1031
|
+
[args.command, request.command],
|
|
1032
|
+
[args.path, request.path],
|
|
1033
|
+
[args.resolvedPath, request.resolvedPath],
|
|
1034
|
+
[args.destination, request.destination],
|
|
1035
|
+
[args.agentName, request.agentName],
|
|
1036
|
+
[args.requesterSessionId, request.requesterSessionId],
|
|
1037
|
+
[args.target, request.destination ?? request.path],
|
|
1038
|
+
[args.value, request.destination ?? request.path ?? request.command],
|
|
1039
|
+
];
|
|
1040
|
+
if (pairs.some(
|
|
1041
|
+
([actual, expected]) =>
|
|
1042
|
+
typeof actual === "string" &&
|
|
1043
|
+
typeof expected === "string" &&
|
|
1044
|
+
actual === expected,
|
|
1045
|
+
)) return true;
|
|
1046
|
+
const forwarding =
|
|
1047
|
+
args.forwarding && typeof args.forwarding === "object" && !Array.isArray(args.forwarding)
|
|
1048
|
+
? args.forwarding as Record<string, unknown>
|
|
1049
|
+
: undefined;
|
|
1050
|
+
if (
|
|
1051
|
+
forwarding &&
|
|
1052
|
+
((typeof forwarding.requesterAgentName === "string" &&
|
|
1053
|
+
forwarding.requesterAgentName === request.agentName) ||
|
|
1054
|
+
(typeof forwarding.requesterSessionId === "string" &&
|
|
1055
|
+
forwarding.requesterSessionId === request.requesterSessionId))
|
|
1056
|
+
) return true;
|
|
1057
|
+
const intent =
|
|
1058
|
+
args.accessIntent && typeof args.accessIntent === "object" && !Array.isArray(args.accessIntent)
|
|
1059
|
+
? args.accessIntent as Record<string, unknown>
|
|
1060
|
+
: undefined;
|
|
1061
|
+
return Boolean(
|
|
1062
|
+
intent &&
|
|
1063
|
+
request.accessIntent &&
|
|
1064
|
+
intent.surface === request.accessIntent.surface &&
|
|
1065
|
+
Array.isArray(intent.matchValues) &&
|
|
1066
|
+
intent.matchValues.length === request.accessIntent.matchValues.length &&
|
|
1067
|
+
intent.matchValues.every(
|
|
1068
|
+
(value, index) => value === request.accessIntent!.matchValues[index],
|
|
1069
|
+
) &&
|
|
1070
|
+
intent.boundaryValue === request.accessIntent.boundaryValue,
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function sameReviewerField(actual: unknown, expected: unknown): boolean {
|
|
1075
|
+
return expected !== undefined &&
|
|
1076
|
+
canonicalReviewerJson(actual) === canonicalReviewerJson(expected);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function toolArgumentCoveredByRequest(
|
|
1080
|
+
key: string,
|
|
1081
|
+
actual: unknown,
|
|
1082
|
+
request: RelevantBoundaryRequest,
|
|
1083
|
+
): boolean {
|
|
1084
|
+
if (key === "target") {
|
|
1085
|
+
return sameReviewerField(actual, request.destination ?? request.path);
|
|
1086
|
+
}
|
|
1087
|
+
if (key === "value") {
|
|
1088
|
+
return [request.command, request.path, request.destination].some(
|
|
1089
|
+
(expected) => sameReviewerField(actual, expected),
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
if (key === "forwarding") {
|
|
1093
|
+
if (!request.agentName && !request.requesterSessionId) return false;
|
|
1094
|
+
return sameReviewerField(actual, {
|
|
1095
|
+
requesterAgentName: request.agentName,
|
|
1096
|
+
requesterSessionId: request.requesterSessionId,
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
const expected = ({
|
|
1100
|
+
command: request.command,
|
|
785
1101
|
path: request.path,
|
|
786
1102
|
resolvedPath: request.resolvedPath,
|
|
787
1103
|
destination: request.destination,
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1104
|
+
cwd: request.cwd,
|
|
1105
|
+
agentName: request.agentName,
|
|
1106
|
+
requesterSessionId: request.requesterSessionId,
|
|
1107
|
+
accessIntent: request.accessIntent,
|
|
1108
|
+
} as Record<string, unknown>)[key];
|
|
1109
|
+
return sameReviewerField(actual, expected);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function exactToolCallReviewerRepresentation(
|
|
1113
|
+
call: ToolCallRecord,
|
|
1114
|
+
request: RelevantBoundaryRequest,
|
|
1115
|
+
reason: EvidenceSelectionReason,
|
|
1116
|
+
): string | undefined {
|
|
1117
|
+
if (
|
|
1118
|
+
!call.id ||
|
|
1119
|
+
call.id !== request.toolCallId ||
|
|
1120
|
+
!call.arguments ||
|
|
1121
|
+
typeof call.arguments !== "object" ||
|
|
1122
|
+
Array.isArray(call.arguments)
|
|
1123
|
+
) {
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const args = call.arguments as Record<string, unknown>;
|
|
1127
|
+
const serializedArgs = canonicalReviewerJson(args);
|
|
1128
|
+
const previewCoversAll =
|
|
1129
|
+
request.toolInputPreview === serializedArgs ||
|
|
1130
|
+
request.toolInputPreview === `input ${serializedArgs}`;
|
|
1131
|
+
const supplement = previewCoversAll
|
|
1132
|
+
? {}
|
|
1133
|
+
: Object.fromEntries(
|
|
1134
|
+
Object.entries(args).filter(
|
|
1135
|
+
([key, actual]) =>
|
|
1136
|
+
!toolArgumentCoveredByRequest(key, actual, request),
|
|
1137
|
+
),
|
|
1138
|
+
);
|
|
1139
|
+
return canonicalReviewerJson({
|
|
1140
|
+
id: call.id,
|
|
1141
|
+
name: call.name,
|
|
1142
|
+
reason,
|
|
1143
|
+
...(Object.keys(supplement).length > 0 ? { supplement } : {}),
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function securityEvidenceCategory(
|
|
1148
|
+
call: ToolCallRecord,
|
|
1149
|
+
):
|
|
1150
|
+
| "credential"
|
|
1151
|
+
| "permission"
|
|
1152
|
+
| "authorization"
|
|
1153
|
+
| "transport"
|
|
1154
|
+
| "audit"
|
|
1155
|
+
| "security-control"
|
|
1156
|
+
| undefined {
|
|
1157
|
+
const command = commandArgument(call);
|
|
1158
|
+
if (!command) return;
|
|
1159
|
+
if (
|
|
1160
|
+
/\b(?:cat|sed|awk|base64|openssl|head|tail|grep|dd|xxd|tar|zip)\b[\s\S]*(?:\.env|\.ssh|\.aws\/credentials|\.kube\/config|\.npmrc|auth\.json)/i.test(
|
|
1161
|
+
command,
|
|
1162
|
+
)
|
|
1163
|
+
) return "credential";
|
|
1164
|
+
if (/\b(?:chmod|chown|chgrp|setfacl)\b/i.test(command)) return "permission";
|
|
1165
|
+
if (/(?:authorized_keys|\/etc\/sudoers|access[_-]?grant)/i.test(command)) {
|
|
1166
|
+
return "authorization";
|
|
1167
|
+
}
|
|
1168
|
+
if (
|
|
1169
|
+
/(?:--insecure|--no-check-certificate|sslverify\s+false|strict-ssl\s+false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*0|https?_proxy)/i.test(
|
|
1170
|
+
command,
|
|
1171
|
+
)
|
|
1172
|
+
) return "transport";
|
|
1173
|
+
if (/\b(?:disable|truncate|delete|rm)\b[\s\S]*\b(?:audit|log)/i.test(command)) {
|
|
1174
|
+
return "audit";
|
|
1175
|
+
}
|
|
1176
|
+
if (/(?:pi-auto-review|pi-sandbox|permission-system|permissions\.json|sandbox\.json)/i.test(command)) {
|
|
1177
|
+
return "security-control";
|
|
1178
|
+
}
|
|
1179
|
+
return undefined;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function selectRequestAwareTools(
|
|
1183
|
+
entries: readonly unknown[],
|
|
1184
|
+
evidence: Evidence[],
|
|
1185
|
+
request: RelevantBoundaryRequest,
|
|
1186
|
+
relevant: ReturnType<typeof relevantResultEvidence>,
|
|
1187
|
+
turnStart: number,
|
|
1188
|
+
): { selected: Evidence[]; criticalEvidenceOverflow: boolean } {
|
|
1189
|
+
const calls = collectToolCalls(entries);
|
|
1190
|
+
const reasons = new Map<string, EvidenceSelectionReason[]>();
|
|
1191
|
+
const addReason = (call: ToolCallRecord, reason: EvidenceSelectionReason) => {
|
|
1192
|
+
const current = reasons.get(call.candidateId) ?? [];
|
|
1193
|
+
if (!current.includes(reason)) current.push(reason);
|
|
1194
|
+
reasons.set(call.candidateId, current);
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
const exact = request.toolCallId
|
|
1198
|
+
? calls.find((call) => call.id === request.toolCallId)
|
|
1199
|
+
: undefined;
|
|
1200
|
+
if (exact) addReason(exact, "exact-tool-call");
|
|
1201
|
+
for (const unit of relevant) addReason(unit.call, unit.reason);
|
|
1202
|
+
for (const call of calls) {
|
|
1203
|
+
if (call.entryIndex < turnStart || call === exact) continue;
|
|
1204
|
+
if (exactStructuredMatch(call, request)) {
|
|
1205
|
+
addReason(call, "structured-request-match");
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const profile = surfaceProfile(request);
|
|
1209
|
+
const securityCalls = calls.filter(
|
|
1210
|
+
(call) =>
|
|
1211
|
+
call.entryIndex >= turnStart &&
|
|
1212
|
+
securityEvidenceCategory(call) !== undefined &&
|
|
1213
|
+
["network", "git-push", "forwarded"].includes(profile),
|
|
1214
|
+
);
|
|
1215
|
+
for (const call of securityCalls.slice(-4)) {
|
|
1216
|
+
addReason(call, "security-combination");
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const priority: EvidenceSelectionReason[] = [
|
|
1220
|
+
"exact-tool-call",
|
|
1221
|
+
"same-tool",
|
|
1222
|
+
"delete-precheck",
|
|
1223
|
+
"git-push-context",
|
|
1224
|
+
"provider-branch-protection",
|
|
1225
|
+
"security-combination",
|
|
1226
|
+
"structured-request-match",
|
|
1227
|
+
];
|
|
1228
|
+
const selected = evidence
|
|
1229
|
+
.filter((item) => item.kind === "tool" && reasons.has(item.id))
|
|
1230
|
+
.map((item) => {
|
|
1231
|
+
const matched = reasons.get(item.id)!;
|
|
1232
|
+
const ordered = [...matched].sort(
|
|
1233
|
+
(left, right) => priority.indexOf(left) - priority.indexOf(right),
|
|
1234
|
+
);
|
|
1235
|
+
const call = calls.find((candidate) => candidate.candidateId === item.id)!;
|
|
1236
|
+
const linkage = exactToolCallReviewerRepresentation(
|
|
1237
|
+
call,
|
|
1238
|
+
request,
|
|
1239
|
+
ordered[0],
|
|
1240
|
+
);
|
|
1241
|
+
return {
|
|
1242
|
+
...item,
|
|
1243
|
+
...(linkage
|
|
1244
|
+
? { text: linkage, representationCharacters: linkage.length }
|
|
1245
|
+
: {}),
|
|
1246
|
+
reason: ordered[0],
|
|
1247
|
+
secondaryReasons: ordered.slice(1),
|
|
1248
|
+
sensitivity:
|
|
1249
|
+
ordered.includes("security-combination")
|
|
1250
|
+
? ({
|
|
1251
|
+
credential: "credential",
|
|
1252
|
+
permission: "permission-control",
|
|
1253
|
+
authorization: "authorization-persistence",
|
|
1254
|
+
transport: "transport-weakening",
|
|
1255
|
+
audit: "audit-control",
|
|
1256
|
+
"security-control": "security-control",
|
|
1257
|
+
} as const)[
|
|
1258
|
+
securityEvidenceCategory(
|
|
1259
|
+
calls.find((call) => call.candidateId === item.id)!,
|
|
1260
|
+
)!
|
|
1261
|
+
]
|
|
1262
|
+
: item.sensitivity,
|
|
1263
|
+
};
|
|
1264
|
+
});
|
|
1265
|
+
return {
|
|
1266
|
+
selected,
|
|
1267
|
+
criticalEvidenceOverflow: securityCalls.length > 4,
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function capRelevantUnits(
|
|
1272
|
+
units: ReturnType<typeof relevantResultEvidence>,
|
|
1273
|
+
profile: EvidenceSurfaceProfile,
|
|
1274
|
+
): ReturnType<typeof relevantResultEvidence> {
|
|
1275
|
+
const newest = [...units].sort((left, right) => right.index - left.index);
|
|
1276
|
+
const selected: typeof units = [];
|
|
1277
|
+
const take = (
|
|
1278
|
+
reason: (typeof units)[number]["reason"],
|
|
1279
|
+
limit: number,
|
|
1280
|
+
predicate: (unit: (typeof units)[number]) => boolean = () => true,
|
|
1281
|
+
) => {
|
|
1282
|
+
for (const unit of newest) {
|
|
1283
|
+
if (
|
|
1284
|
+
selected.length >= units.length ||
|
|
1285
|
+
selected.includes(unit) ||
|
|
1286
|
+
unit.reason !== reason ||
|
|
1287
|
+
!predicate(unit) ||
|
|
1288
|
+
selected.filter(
|
|
1289
|
+
(candidate) => candidate.reason === reason && predicate(candidate),
|
|
1290
|
+
).length >= limit
|
|
1291
|
+
) {
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
selected.push(unit);
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
take("same-tool", 1);
|
|
1298
|
+
if (profile === "delete") take("delete-precheck", 2);
|
|
1299
|
+
if (profile === "git-push") {
|
|
1300
|
+
take("git-push-context", 1, (unit) =>
|
|
1301
|
+
/^\s*git\s+(?:remote|config\s+--get\s+remote)/i.test(
|
|
1302
|
+
commandArgument(unit.call),
|
|
1303
|
+
),
|
|
1304
|
+
);
|
|
1305
|
+
take("git-push-context", 1, (unit) =>
|
|
1306
|
+
/^\s*git\s+(?:branch|status|rev-parse)/i.test(
|
|
1307
|
+
commandArgument(unit.call),
|
|
1308
|
+
),
|
|
1309
|
+
);
|
|
1310
|
+
take("provider-branch-protection", 1);
|
|
1311
|
+
}
|
|
1312
|
+
if (profile === "generic") {
|
|
1313
|
+
for (const unit of newest) {
|
|
1314
|
+
if (!selected.includes(unit) && selected.length < 4) selected.push(unit);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return selected.sort((left, right) => left.index - right.index);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function selectedMetadata(
|
|
1321
|
+
item: Evidence,
|
|
1322
|
+
original: Evidence,
|
|
1323
|
+
profile: EvidenceSurfaceProfile,
|
|
1324
|
+
): EvidenceCandidateMetadata {
|
|
1325
|
+
return {
|
|
1326
|
+
id: item.id,
|
|
1327
|
+
entryIndex: item.entryIndex,
|
|
1328
|
+
kind: item.kind === "user" ? "user" : "tool-call",
|
|
1329
|
+
...(item.toolCallId ? { toolCallId: item.toolCallId } : {}),
|
|
1330
|
+
reason: item.reason,
|
|
1331
|
+
secondaryReasons: [...item.secondaryReasons],
|
|
1332
|
+
surfaceProfile: profile,
|
|
1333
|
+
originalCharacters: original.originalCharacters,
|
|
1334
|
+
selectedCharacters: item.text.length,
|
|
1335
|
+
estimatedTokens: Buffer.byteLength(item.text, "utf8"),
|
|
1336
|
+
truncated:
|
|
1337
|
+
original.preTruncated ||
|
|
1338
|
+
item.text.length <
|
|
1339
|
+
(item.representationCharacters ?? original.originalCharacters),
|
|
1340
|
+
sensitivity: item.sensitivity,
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
type UserSelection = {
|
|
1345
|
+
selected: Evidence[];
|
|
1346
|
+
truncated: boolean;
|
|
1347
|
+
authorizationCeiling: TranscriptResult["userAuthorizationCeiling"];
|
|
1348
|
+
constraint: TranscriptResult["userConstraint"];
|
|
1349
|
+
compactionState: TranscriptResult["compactionState"];
|
|
1350
|
+
constraintOverflow: boolean;
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
function utf8Prefix(value: string, maxBytes: number): string {
|
|
1354
|
+
if (maxBytes <= 0) return "";
|
|
1355
|
+
let bytes = 0;
|
|
1356
|
+
let result = "";
|
|
1357
|
+
for (const character of value) {
|
|
1358
|
+
const size = Buffer.byteLength(character, "utf8");
|
|
1359
|
+
if (bytes + size > maxBytes) break;
|
|
1360
|
+
result += character;
|
|
1361
|
+
bytes += size;
|
|
1362
|
+
}
|
|
1363
|
+
return result;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function utf8Suffix(value: string, maxBytes: number): string {
|
|
1367
|
+
if (maxBytes <= 0) return "";
|
|
1368
|
+
let bytes = 0;
|
|
1369
|
+
const characters = [...value];
|
|
1370
|
+
let result = "";
|
|
1371
|
+
for (let index = characters.length - 1; index >= 0; index--) {
|
|
1372
|
+
const character = characters[index];
|
|
1373
|
+
const size = Buffer.byteLength(character, "utf8");
|
|
1374
|
+
if (bytes + size > maxBytes) break;
|
|
1375
|
+
result = character + result;
|
|
1376
|
+
bytes += size;
|
|
1377
|
+
}
|
|
1378
|
+
return result;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function headTail(value: string, limit: number): string {
|
|
1382
|
+
if (Buffer.byteLength(value, "utf8") <= limit) return value;
|
|
1383
|
+
const marker = "\n…[middle truncated]…\n";
|
|
1384
|
+
const markerBytes = Buffer.byteLength(marker, "utf8");
|
|
1385
|
+
if (limit <= markerBytes) return utf8Prefix(marker, limit);
|
|
1386
|
+
const available = limit - markerBytes;
|
|
1387
|
+
const head = Math.ceil(available / 2);
|
|
1388
|
+
return `${utf8Prefix(value, head)}${marker}${utf8Suffix(value, available - head)}`;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function summaryEntryIndices(entries: readonly unknown[]): number[] {
|
|
1392
|
+
const indices: number[] = [];
|
|
1393
|
+
entries.forEach((entry, index) => {
|
|
1394
|
+
const role = messageRecord(entry)?.role;
|
|
1395
|
+
if (role === "compactionSummary" || role === "branchSummary") {
|
|
1396
|
+
indices.push(index);
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
return indices;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function containsExactIdentifier(text: string, value: string): boolean {
|
|
1403
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1404
|
+
return new RegExp(`(^|[^A-Za-z0-9_-])${escaped}(?=$|[^A-Za-z0-9_-])`).test(
|
|
1405
|
+
text,
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function exactUserReference(
|
|
1410
|
+
text: string,
|
|
1411
|
+
request: RelevantBoundaryRequest,
|
|
1412
|
+
): boolean {
|
|
1413
|
+
return [
|
|
1414
|
+
request.id,
|
|
1415
|
+
request.toolCallId,
|
|
1416
|
+
request.requesterSessionId,
|
|
1417
|
+
request.agentName,
|
|
1418
|
+
].some((value) => {
|
|
1419
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
1420
|
+
return containsExactIdentifier(text, value);
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function constraintEffect(text: string): "narrowed" | "revoked" | undefined {
|
|
1425
|
+
if (
|
|
1426
|
+
/(?:\b(?:do not|don't|never|must not|forbid|forbidden|revoke|cancel)\b|不要|不得|禁止|撤销|取消|不能|别)/i.test(
|
|
1427
|
+
text,
|
|
1428
|
+
)
|
|
1429
|
+
) return "revoked";
|
|
1430
|
+
if (/(?:\b(?:only|except|staging only|not main)\b|只允许|仅允许|仅限|只能)/i.test(text)) {
|
|
1431
|
+
return "narrowed";
|
|
1432
|
+
}
|
|
1433
|
+
return undefined;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function vagueContinuation(text: string): boolean {
|
|
1437
|
+
return /^\s*(?:continue|proceed|go ahead|do it|yes|ok(?:ay)?|继续|照做|可以|好的?|行)[.!。!\s]*$/i.test(
|
|
1438
|
+
text,
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function constraintMatchesProfile(
|
|
1443
|
+
text: string,
|
|
1444
|
+
profile: EvidenceSurfaceProfile,
|
|
1445
|
+
): boolean {
|
|
1446
|
+
const patterns: Record<EvidenceSurfaceProfile, RegExp> = {
|
|
1447
|
+
network: /(?:\b(?:network|connect|domain|upload|download|http)\b|网络|联网|域名|上传|下载)/i,
|
|
1448
|
+
delete: /(?:\b(?:delete|remove|unlink|rmdir|\brm\b)\b|删除|移除)/i,
|
|
1449
|
+
"git-push": /(?:\b(?:git\s+push|push|branch|main)\b|推送|分支|主分支)/i,
|
|
1450
|
+
forwarded: /(?:\b(?:agent|subagent|forward|worker)\b|代理|子代理|转发)/i,
|
|
1451
|
+
generic: /(?:\b(?:execute|run|write|file|path|permission|deploy)\b|执行|运行|写入|文件|路径|权限|部署)/i,
|
|
1452
|
+
};
|
|
1453
|
+
return patterns[profile].test(text);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function constraintConflictsOrIsUncertain(
|
|
1457
|
+
text: string,
|
|
1458
|
+
effect: "narrowed" | "revoked",
|
|
1459
|
+
request: RelevantBoundaryRequest,
|
|
1460
|
+
): boolean {
|
|
1461
|
+
const representation = [
|
|
1462
|
+
request.command,
|
|
1463
|
+
request.path,
|
|
1464
|
+
request.resolvedPath,
|
|
1465
|
+
request.destination,
|
|
1466
|
+
request.agentName,
|
|
1467
|
+
]
|
|
1468
|
+
.filter((value): value is string => Boolean(value))
|
|
1469
|
+
.join(" ")
|
|
1470
|
+
.toLowerCase();
|
|
1471
|
+
if (/\bmain\b|主分支/i.test(text)) {
|
|
1472
|
+
return effect === "revoked"
|
|
1473
|
+
? /\bmain\b/.test(representation)
|
|
1474
|
+
: /\bmain\b/.test(representation);
|
|
1475
|
+
}
|
|
1476
|
+
if (/\bstaging\b|预发布|测试环境/i.test(text)) {
|
|
1477
|
+
const targetsStaging = /\bstaging\b/.test(representation);
|
|
1478
|
+
return effect === "revoked" ? targetsStaging : !targetsStaging;
|
|
1479
|
+
}
|
|
1480
|
+
return true;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function selectUserEvidence(
|
|
1484
|
+
entries: readonly unknown[],
|
|
1485
|
+
evidence: Evidence[],
|
|
1486
|
+
request: RelevantBoundaryRequest,
|
|
1487
|
+
profile: EvidenceSurfaceProfile,
|
|
1488
|
+
budgetTokens: number,
|
|
1489
|
+
): UserSelection {
|
|
1490
|
+
const users = evidence.filter((item) => item.kind === "user");
|
|
1491
|
+
const summaries = summaryEntryIndices(entries);
|
|
1492
|
+
const lastSummary = summaries.at(-1) ?? -1;
|
|
1493
|
+
const rawAfterSummary = users.filter((item) => item.entryIndex > lastSummary);
|
|
1494
|
+
const eligibleUsers = lastSummary >= 0 ? rawAfterSummary : users;
|
|
1495
|
+
const latest = eligibleUsers.at(-1);
|
|
1496
|
+
const compactionState: TranscriptResult["compactionState"] =
|
|
1497
|
+
summaries.length === 0
|
|
1498
|
+
? "none"
|
|
1499
|
+
: latest
|
|
1500
|
+
? "summary-present"
|
|
1501
|
+
: "authorization-unavailable";
|
|
1502
|
+
if (!latest || budgetTokens <= 0) {
|
|
1503
|
+
return {
|
|
1504
|
+
selected: [],
|
|
1505
|
+
truncated: users.length > 0,
|
|
1506
|
+
authorizationCeiling: request.trustedRetryOriginalRequestId
|
|
1507
|
+
? "high"
|
|
1508
|
+
: "unknown",
|
|
1509
|
+
constraint: "none",
|
|
1510
|
+
compactionState,
|
|
1511
|
+
constraintOverflow: false,
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
let remaining = budgetTokens;
|
|
1516
|
+
const latestText = headTail(latest.text, remaining);
|
|
1517
|
+
const latestTrustedRetry = Boolean(
|
|
1518
|
+
request.trustedRetryOriginalRequestId &&
|
|
1519
|
+
latest.text.includes("I approved one reviewer retry") &&
|
|
1520
|
+
containsExactIdentifier(
|
|
1521
|
+
latest.text,
|
|
1522
|
+
request.trustedRetryOriginalRequestId,
|
|
1523
|
+
),
|
|
1524
|
+
);
|
|
1525
|
+
const latestExactReference = exactUserReference(latest.text, request);
|
|
1526
|
+
const latestEffect = constraintEffect(latest.text);
|
|
1527
|
+
const latestActiveConstraint = Boolean(
|
|
1528
|
+
latestEffect && constraintMatchesProfile(latest.text, profile),
|
|
1529
|
+
);
|
|
1530
|
+
const latestConstraintConflict = Boolean(
|
|
1531
|
+
latestEffect &&
|
|
1532
|
+
latestActiveConstraint &&
|
|
1533
|
+
constraintConflictsOrIsUncertain(latest.text, latestEffect, request),
|
|
1534
|
+
);
|
|
1535
|
+
const selected: Evidence[] = [{
|
|
1536
|
+
...latest,
|
|
1537
|
+
text: latestText,
|
|
1538
|
+
reason: "latest-user",
|
|
1539
|
+
secondaryReasons: [
|
|
1540
|
+
...(latestTrustedRetry ? ["trusted-retry-user-message" as const] : []),
|
|
1541
|
+
...(latestExactReference ? ["exact-request-reference" as const] : []),
|
|
1542
|
+
...(latestActiveConstraint ? ["active-narrowing-constraint" as const] : []),
|
|
1543
|
+
],
|
|
1544
|
+
}];
|
|
1545
|
+
remaining -= Buffer.byteLength(latestText, "utf8");
|
|
1546
|
+
let constraint: TranscriptResult["userConstraint"] =
|
|
1547
|
+
latestConstraintConflict ? latestEffect! : "none";
|
|
1548
|
+
|
|
1549
|
+
const older = eligibleUsers.slice(0, -1).reverse();
|
|
1550
|
+
for (const candidate of older) {
|
|
1551
|
+
if (remaining <= 0) break;
|
|
1552
|
+
const trustedRetry =
|
|
1553
|
+
request.trustedRetryOriginalRequestId &&
|
|
1554
|
+
candidate.text.includes("I approved one reviewer retry") &&
|
|
1555
|
+
containsExactIdentifier(
|
|
1556
|
+
candidate.text,
|
|
1557
|
+
request.trustedRetryOriginalRequestId,
|
|
1558
|
+
);
|
|
1559
|
+
const exactReference = exactUserReference(candidate.text, request);
|
|
1560
|
+
const effect = constraintEffect(candidate.text);
|
|
1561
|
+
const activeConstraint = effect && constraintMatchesProfile(candidate.text, profile);
|
|
1562
|
+
if (!trustedRetry && !exactReference && !activeConstraint) continue;
|
|
1563
|
+
const reason: EvidenceSelectionReason = trustedRetry
|
|
1564
|
+
? "trusted-retry-user-message"
|
|
1565
|
+
: exactReference
|
|
1566
|
+
? "exact-request-reference"
|
|
1567
|
+
: "active-narrowing-constraint";
|
|
1568
|
+
const text = headTail(candidate.text, remaining);
|
|
1569
|
+
if (!text) continue;
|
|
1570
|
+
selected.push({
|
|
1571
|
+
...candidate,
|
|
1572
|
+
text,
|
|
1573
|
+
reason,
|
|
1574
|
+
secondaryReasons: [
|
|
1575
|
+
...(trustedRetry && exactReference ? ["exact-request-reference" as const] : []),
|
|
1576
|
+
...(activeConstraint && reason !== "active-narrowing-constraint"
|
|
1577
|
+
? ["active-narrowing-constraint" as const]
|
|
1578
|
+
: []),
|
|
1579
|
+
],
|
|
1580
|
+
});
|
|
1581
|
+
remaining -= Buffer.byteLength(text, "utf8");
|
|
1582
|
+
if (
|
|
1583
|
+
activeConstraint &&
|
|
1584
|
+
constraintConflictsOrIsUncertain(candidate.text, effect, request)
|
|
1585
|
+
) {
|
|
1586
|
+
constraint = effect === "revoked" ? "revoked" : constraint === "none" ? "narrowed" : constraint;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
selected.sort((left, right) => left.entryIndex - right.entryIndex);
|
|
1590
|
+
const selectedIds = new Set(selected.map((item) => item.id));
|
|
1591
|
+
const constraintOverflow = older.some((candidate) => {
|
|
1592
|
+
const effect = constraintEffect(candidate.text);
|
|
1593
|
+
if (!effect || !constraintMatchesProfile(candidate.text, profile)) return false;
|
|
1594
|
+
const chosen = selected.find((item) => item.id === candidate.id);
|
|
1595
|
+
return (
|
|
1596
|
+
!selectedIds.has(candidate.id) ||
|
|
1597
|
+
!chosen ||
|
|
1598
|
+
chosen.text.length < candidate.originalCharacters ||
|
|
1599
|
+
candidate.preTruncated
|
|
1600
|
+
);
|
|
1601
|
+
});
|
|
1602
|
+
const latestTruncated = latestText.length < latest.originalCharacters || latest.preTruncated;
|
|
1603
|
+
const hasExactAuthorizationLink = selected.some(
|
|
1604
|
+
(item) =>
|
|
1605
|
+
item.reason === "exact-request-reference" ||
|
|
1606
|
+
item.reason === "trusted-retry-user-message" ||
|
|
1607
|
+
item.secondaryReasons.includes("exact-request-reference") ||
|
|
1608
|
+
item.secondaryReasons.includes("trusted-retry-user-message"),
|
|
1609
|
+
);
|
|
1610
|
+
const authorizationCeiling: TranscriptResult["userAuthorizationCeiling"] =
|
|
1611
|
+
request.trustedRetryOriginalRequestId && constraint !== "revoked"
|
|
1612
|
+
? "high"
|
|
1613
|
+
: compactionState === "authorization-unavailable" || constraint === "revoked"
|
|
1614
|
+
? "unknown"
|
|
1615
|
+
: latestTruncated
|
|
1616
|
+
? "medium"
|
|
1617
|
+
: vagueContinuation(latest.text) && !hasExactAuthorizationLink
|
|
1618
|
+
? "unknown"
|
|
1619
|
+
: "high";
|
|
1620
|
+
return {
|
|
1621
|
+
selected,
|
|
1622
|
+
truncated:
|
|
1623
|
+
latestTruncated ||
|
|
1624
|
+
selected.some((item) => item.text.length < item.originalCharacters),
|
|
1625
|
+
authorizationCeiling,
|
|
1626
|
+
constraint,
|
|
1627
|
+
compactionState,
|
|
1628
|
+
constraintOverflow,
|
|
1629
|
+
};
|
|
791
1630
|
}
|
|
792
1631
|
|
|
793
1632
|
function selectEvidence(
|
|
794
1633
|
evidence: Evidence[],
|
|
795
1634
|
kind: Evidence["kind"],
|
|
796
|
-
|
|
1635
|
+
budgetTokens: number,
|
|
797
1636
|
): { selected: Evidence[]; truncated: boolean } {
|
|
798
1637
|
const candidates = evidence.filter((item) => item.kind === kind);
|
|
799
|
-
if (candidates.length === 0 ||
|
|
1638
|
+
if (candidates.length === 0 || budgetTokens <= 0) {
|
|
800
1639
|
return { selected: [], truncated: candidates.length > 0 };
|
|
801
1640
|
}
|
|
802
1641
|
|
|
803
1642
|
const selected = new Map<number, Evidence>();
|
|
804
|
-
let remaining =
|
|
1643
|
+
let remaining = budgetTokens;
|
|
805
1644
|
const add = (item: Evidence, limit = remaining): void => {
|
|
806
1645
|
if (selected.has(item.index) || remaining <= 0) return;
|
|
807
|
-
const text = item.text
|
|
1646
|
+
const text = utf8Prefix(item.text, Math.min(remaining, limit));
|
|
808
1647
|
if (!text) return;
|
|
809
1648
|
selected.set(item.index, { ...item, text });
|
|
810
|
-
remaining -= text
|
|
1649
|
+
remaining -= Buffer.byteLength(text, "utf8");
|
|
811
1650
|
};
|
|
812
1651
|
|
|
813
1652
|
// Keep the original user intent as an anchor, then fill from newest to oldest.
|
|
814
1653
|
if (kind === "user") {
|
|
815
1654
|
const firstBudget =
|
|
816
1655
|
candidates.length > 1
|
|
817
|
-
? Math.max(1, Math.floor(
|
|
818
|
-
:
|
|
1656
|
+
? Math.max(1, Math.floor(budgetTokens / 2))
|
|
1657
|
+
: budgetTokens;
|
|
819
1658
|
add(candidates[0], firstBudget);
|
|
820
1659
|
if (candidates.length > 1) add(candidates[candidates.length - 1]);
|
|
821
1660
|
}
|
|
@@ -841,68 +1680,229 @@ export function buildClassifierTranscript(
|
|
|
841
1680
|
request: RelevantBoundaryRequest = {},
|
|
842
1681
|
): TranscriptResult {
|
|
843
1682
|
const evidence = extractEvidence(entries);
|
|
844
|
-
const
|
|
1683
|
+
const profile = surfaceProfile(request);
|
|
1684
|
+
const turnStart = currentTurnStart(entries);
|
|
1685
|
+
const relevantUnits = capRelevantUnits(
|
|
1686
|
+
relevantResultEvidence(entries, request, turnStart),
|
|
1687
|
+
profile,
|
|
1688
|
+
);
|
|
1689
|
+
const users = selectUserEvidence(
|
|
1690
|
+
entries,
|
|
845
1691
|
evidence,
|
|
846
|
-
|
|
847
|
-
|
|
1692
|
+
request,
|
|
1693
|
+
profile,
|
|
1694
|
+
config.maxUserTranscriptTokens,
|
|
848
1695
|
);
|
|
849
|
-
const
|
|
1696
|
+
const requestAwareTools = selectRequestAwareTools(
|
|
1697
|
+
entries,
|
|
850
1698
|
evidence,
|
|
851
|
-
|
|
852
|
-
|
|
1699
|
+
request,
|
|
1700
|
+
relevantUnits,
|
|
1701
|
+
turnStart,
|
|
853
1702
|
);
|
|
854
|
-
const
|
|
855
|
-
|
|
1703
|
+
const tools = selectEvidence(
|
|
1704
|
+
requestAwareTools.selected,
|
|
1705
|
+
"tool",
|
|
1706
|
+
config.maxToolTranscriptTokens,
|
|
856
1707
|
);
|
|
857
|
-
const baseRendered = selected
|
|
858
|
-
.map(
|
|
859
|
-
(item) =>
|
|
860
|
-
`<${item.kind}>\n${escapeEvidenceMarkup(item.text)}\n</${item.kind}>`,
|
|
861
|
-
)
|
|
862
|
-
.join("\n\n");
|
|
863
1708
|
const relevantBudget =
|
|
864
|
-
|
|
1709
|
+
config.maxRelevantResultTokens ?? config.maxToolTranscriptTokens;
|
|
1710
|
+
const sandboxSupplement = sandboxTrapSupplement(request);
|
|
1711
|
+
const sandboxEvidence = sandboxTrapEvidence(request);
|
|
865
1712
|
const relevantCandidates = [
|
|
866
|
-
...(
|
|
867
|
-
? [{
|
|
1713
|
+
...(sandboxEvidence && sandboxSupplement
|
|
1714
|
+
? [{
|
|
1715
|
+
index: Number.MAX_SAFE_INTEGER - 1,
|
|
1716
|
+
id: `sandbox-trap:${request.id ?? "request"}`,
|
|
1717
|
+
reason: "sandbox-trap" as const,
|
|
1718
|
+
toolCallId: undefined,
|
|
1719
|
+
text: sandboxEvidence,
|
|
1720
|
+
content: sandboxSupplement,
|
|
1721
|
+
rawCharacters: sandboxEvidence.length,
|
|
1722
|
+
sensitivity: "boundary" as const,
|
|
1723
|
+
}]
|
|
868
1724
|
: []),
|
|
869
|
-
...
|
|
1725
|
+
...relevantUnits
|
|
1726
|
+
.filter((unit) =>
|
|
1727
|
+
tools.selected.some((tool) => tool.id === unit.call.candidateId),
|
|
1728
|
+
)
|
|
1729
|
+
.map((unit) => ({
|
|
1730
|
+
index: unit.index,
|
|
1731
|
+
id: unit.resultId,
|
|
1732
|
+
reason: unit.reason,
|
|
1733
|
+
toolCallId: unit.call.id,
|
|
1734
|
+
text: unit.renderedResult,
|
|
1735
|
+
content: unit.resultText,
|
|
1736
|
+
rawCharacters: unit.renderedResult.length,
|
|
1737
|
+
sensitivity: "tool-output" as const,
|
|
1738
|
+
})),
|
|
870
1739
|
];
|
|
871
1740
|
let relevantRemaining = relevantBudget;
|
|
872
|
-
|
|
873
|
-
|
|
1741
|
+
let requiredProfileOverflow = false;
|
|
1742
|
+
const relevantSelected: Array<
|
|
1743
|
+
(typeof relevantCandidates)[number] & { selectedText: string }
|
|
1744
|
+
> = [];
|
|
1745
|
+
const relevantSelectionOrder = [
|
|
1746
|
+
...relevantCandidates.filter((candidate) => candidate.reason === "sandbox-trap"),
|
|
1747
|
+
...relevantCandidates
|
|
1748
|
+
.filter((candidate) => candidate.reason !== "sandbox-trap")
|
|
1749
|
+
.sort((left, right) => right.index - left.index),
|
|
1750
|
+
];
|
|
1751
|
+
for (const candidate of relevantSelectionOrder) {
|
|
874
1752
|
if (relevantRemaining <= 0) break;
|
|
875
|
-
|
|
1753
|
+
if (
|
|
1754
|
+
candidate.reason === "sandbox-trap" &&
|
|
1755
|
+
Buffer.byteLength(candidate.text, "utf8") > relevantRemaining
|
|
1756
|
+
) {
|
|
1757
|
+
requiredProfileOverflow = true;
|
|
1758
|
+
break;
|
|
1759
|
+
}
|
|
1760
|
+
const text = utf8Prefix(candidate.text, relevantRemaining);
|
|
876
1761
|
if (text) {
|
|
877
|
-
relevantSelected.
|
|
878
|
-
relevantRemaining -= text
|
|
1762
|
+
relevantSelected.push({ ...candidate, selectedText: text });
|
|
1763
|
+
relevantRemaining -= Buffer.byteLength(text, "utf8");
|
|
879
1764
|
}
|
|
880
1765
|
}
|
|
881
|
-
|
|
1766
|
+
relevantSelected.sort((left, right) => left.index - right.index);
|
|
1767
|
+
const pairedToolCallIds = new Set(
|
|
1768
|
+
relevantSelected
|
|
1769
|
+
.map((candidate) => candidate.toolCallId)
|
|
1770
|
+
.filter((id): id is string => Boolean(id)),
|
|
1771
|
+
);
|
|
1772
|
+
const resultReasons = new Set<EvidenceSelectionReason>([
|
|
1773
|
+
"same-tool",
|
|
1774
|
+
"delete-precheck",
|
|
1775
|
+
"git-push-context",
|
|
1776
|
+
"provider-branch-protection",
|
|
1777
|
+
]);
|
|
1778
|
+
const finalTools = tools.selected.filter((tool) => {
|
|
1779
|
+
if (!resultReasons.has(tool.reason)) return true;
|
|
1780
|
+
if (tool.toolCallId && pairedToolCallIds.has(tool.toolCallId)) return true;
|
|
1781
|
+
return tool.secondaryReasons.some((reason) => !resultReasons.has(reason));
|
|
1782
|
+
});
|
|
1783
|
+
const selected = [...users.selected, ...finalTools].sort(
|
|
1784
|
+
(left, right) => left.index - right.index,
|
|
1785
|
+
);
|
|
1786
|
+
const baseRendered = selected
|
|
1787
|
+
.map((item) => {
|
|
1788
|
+
const attributes = item.kind === "tool"
|
|
1789
|
+
? ` id="${escapeEvidenceMarkup(item.toolCallId ?? item.id)}" reason="${item.reason}"`
|
|
1790
|
+
: ` reason="${item.reason}"`;
|
|
1791
|
+
return `<${item.kind}${attributes}>\n${escapeEvidenceMarkup(item.text)}\n</${item.kind}>`;
|
|
1792
|
+
})
|
|
1793
|
+
.join("\n\n");
|
|
1794
|
+
const authorizationNotice =
|
|
1795
|
+
users.compactionState === "authorization-unavailable"
|
|
1796
|
+
? "[Original user authorization is unavailable after compaction. Agent-generated summaries are not authorization.]"
|
|
1797
|
+
: users.compactionState === "summary-present"
|
|
1798
|
+
? "[A compaction/branch summary exists but is not user authorization.]"
|
|
1799
|
+
: "";
|
|
1800
|
+
const rendered = [
|
|
1801
|
+
authorizationNotice,
|
|
1802
|
+
baseRendered,
|
|
1803
|
+
...relevantSelected.map((candidate) => candidate.selectedText),
|
|
1804
|
+
]
|
|
882
1805
|
.filter(Boolean)
|
|
883
1806
|
.join("\n\n");
|
|
884
1807
|
const relevantTruncated =
|
|
885
1808
|
relevantSelected.length < relevantCandidates.length ||
|
|
886
|
-
relevantSelected.reduce(
|
|
1809
|
+
relevantSelected.reduce(
|
|
1810
|
+
(total, value) => total + value.selectedText.length,
|
|
1811
|
+
0,
|
|
1812
|
+
) <
|
|
887
1813
|
relevantCandidates.reduce((total, value) => total + value.text.length, 0);
|
|
888
|
-
const truncated =
|
|
1814
|
+
const truncated =
|
|
1815
|
+
users.truncated ||
|
|
1816
|
+
tools.truncated ||
|
|
1817
|
+
finalTools.length < tools.selected.length ||
|
|
1818
|
+
relevantTruncated;
|
|
889
1819
|
const text = truncated
|
|
890
1820
|
? `[Some transcript evidence was omitted or truncated.]\n\n${rendered}`
|
|
891
1821
|
: rendered;
|
|
892
1822
|
return {
|
|
893
1823
|
text: text || "(no eligible transcript evidence)",
|
|
1824
|
+
surfaceProfile: profile,
|
|
1825
|
+
reviewerEvidence: {
|
|
1826
|
+
userMessages: users.selected.map((item) => ({
|
|
1827
|
+
id: item.id,
|
|
1828
|
+
reason: item.reason,
|
|
1829
|
+
secondaryReasons: [...item.secondaryReasons],
|
|
1830
|
+
content: item.text,
|
|
1831
|
+
})),
|
|
1832
|
+
toolCalls: finalTools.map((item) => ({
|
|
1833
|
+
id: item.id,
|
|
1834
|
+
reason: item.reason,
|
|
1835
|
+
secondaryReasons: [...item.secondaryReasons],
|
|
1836
|
+
...(item.toolCallId ? { toolCallId: item.toolCallId } : {}),
|
|
1837
|
+
content: item.text,
|
|
1838
|
+
})),
|
|
1839
|
+
relevantResults: relevantSelected.map((item) => ({
|
|
1840
|
+
id: item.id,
|
|
1841
|
+
reason: item.reason,
|
|
1842
|
+
secondaryReasons: [],
|
|
1843
|
+
...(item.toolCallId ? { toolCallId: item.toolCallId } : {}),
|
|
1844
|
+
content: item.selectedText === item.text
|
|
1845
|
+
? item.content
|
|
1846
|
+
: item.selectedText,
|
|
1847
|
+
})),
|
|
1848
|
+
},
|
|
1849
|
+
budgetRemovals: [],
|
|
894
1850
|
userCharacters: users.selected.reduce(
|
|
895
1851
|
(total, item) => total + item.text.length,
|
|
896
1852
|
0,
|
|
897
1853
|
),
|
|
898
|
-
toolCharacters:
|
|
1854
|
+
toolCharacters: finalTools.reduce(
|
|
899
1855
|
(total, item) => total + item.text.length,
|
|
900
1856
|
0,
|
|
901
1857
|
),
|
|
902
1858
|
relevantResultCharacters: relevantSelected.reduce(
|
|
903
|
-
(total, item) =>
|
|
1859
|
+
(total, item) =>
|
|
1860
|
+
total +
|
|
1861
|
+
(item.selectedText === item.text
|
|
1862
|
+
? item.content.length
|
|
1863
|
+
: item.selectedText.length),
|
|
904
1864
|
0,
|
|
905
1865
|
),
|
|
906
1866
|
truncated,
|
|
1867
|
+
selectedCandidates: [
|
|
1868
|
+
...users.selected.map((item) =>
|
|
1869
|
+
selectedMetadata(
|
|
1870
|
+
item,
|
|
1871
|
+
evidence.find((candidate) => candidate.id === item.id)!,
|
|
1872
|
+
profile,
|
|
1873
|
+
),
|
|
1874
|
+
),
|
|
1875
|
+
...finalTools.map((item) =>
|
|
1876
|
+
selectedMetadata(
|
|
1877
|
+
item,
|
|
1878
|
+
evidence.find((candidate) => candidate.id === item.id)!,
|
|
1879
|
+
profile,
|
|
1880
|
+
),
|
|
1881
|
+
),
|
|
1882
|
+
...relevantSelected.map((item) => ({
|
|
1883
|
+
id: item.id,
|
|
1884
|
+
entryIndex: item.index,
|
|
1885
|
+
kind: item.sensitivity === "boundary" ? "sandbox-trap" as const : "tool-result" as const,
|
|
1886
|
+
...(item.toolCallId ? { toolCallId: item.toolCallId } : {}),
|
|
1887
|
+
reason: item.reason,
|
|
1888
|
+
secondaryReasons: [],
|
|
1889
|
+
surfaceProfile: profile,
|
|
1890
|
+
originalCharacters: item.rawCharacters,
|
|
1891
|
+
selectedCharacters: item.selectedText.length,
|
|
1892
|
+
estimatedTokens: Buffer.byteLength(item.selectedText, "utf8"),
|
|
1893
|
+
truncated: item.selectedText.length < item.rawCharacters,
|
|
1894
|
+
sensitivity: item.sensitivity,
|
|
1895
|
+
})),
|
|
1896
|
+
].sort((left, right) => left.entryIndex - right.entryIndex),
|
|
1897
|
+
...(requestAwareTools.criticalEvidenceOverflow
|
|
1898
|
+
? { failureCode: "critical_evidence_overflow" as const }
|
|
1899
|
+
: requiredProfileOverflow
|
|
1900
|
+
? { failureCode: "required_profile_overflow" as const }
|
|
1901
|
+
: users.constraintOverflow
|
|
1902
|
+
? { failureCode: "required_user_constraint_overflow" as const }
|
|
1903
|
+
: {}),
|
|
1904
|
+
userAuthorizationCeiling: users.authorizationCeiling,
|
|
1905
|
+
userConstraint: users.constraint,
|
|
1906
|
+
compactionState: users.compactionState,
|
|
907
1907
|
};
|
|
908
1908
|
}
|