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