@erichll/pi-auto-review 0.3.5 → 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 +37 -0
- package/README.md +184 -179
- package/package.json +8 -8
- package/src/broker/broker.ts +5 -5
- package/src/config.json +2 -1
- package/src/index.ts +1221 -113
- package/src/policy.ts +1206 -82
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[],
|
|
@@ -298,15 +411,139 @@ export function deterministicHardDeny(
|
|
|
298
411
|
};
|
|
299
412
|
}
|
|
300
413
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
414
|
+
// Secret path text is only evidence when it occurs as the operand of an
|
|
415
|
+
// operation that reads a file. In particular, a slash-delimited `.env` in
|
|
416
|
+
// a URL or literal request payload is not itself a credential source.
|
|
417
|
+
const envFileName =
|
|
418
|
+
String.raw`\.env(?:\.(?!example\b|sample\b)[A-Za-z0-9_-]*)*`;
|
|
419
|
+
const envFile =
|
|
420
|
+
envFileName + String.raw`(?=[\s"'/@<>=|;&)\`]|$)`;
|
|
421
|
+
const envPath =
|
|
422
|
+
String.raw`(?:[^\s"'@<>=|;&()]+\/)*` + envFile;
|
|
423
|
+
const credentialDirectoryPath =
|
|
424
|
+
String.raw`(?:~\/|\$HOME\/|\$\{HOME\}\/|\/|\.\.?\/)?` +
|
|
425
|
+
String.raw`(?:[^\s"'@<>=|;&()\/]+\/)*` +
|
|
426
|
+
String.raw`(?:\.ssh\/(?:id_[A-Za-z0-9_-]+|authorized_keys)|\.aws\/credentials|\.kube\/config|\.docker\/config\.json|\.npmrc|\.netrc|\.pi\/agent\/auth\.json)`;
|
|
427
|
+
const credentialPath =
|
|
428
|
+
String.raw`(?:` +
|
|
429
|
+
credentialDirectoryPath +
|
|
430
|
+
String.raw`|` + envPath + String.raw`)`;
|
|
431
|
+
const directCredentialUpload = new RegExp(
|
|
432
|
+
String.raw`\b(?:curl|wget)\b[^;\n]*(?:` +
|
|
433
|
+
String.raw`(?:--data(?:-binary|-urlencode)?|-d|--form|-F)(?:=|\s+)[^;\n\s]*@\s*["']?` + credentialPath +
|
|
434
|
+
String.raw`|(?:--upload-file|--post-file)(?:=|\s+)["']?` + credentialPath +
|
|
435
|
+
String.raw`|-T(?:\s*|=)["']?` + credentialPath +
|
|
436
|
+
String.raw`)["']?`,
|
|
437
|
+
"i",
|
|
438
|
+
);
|
|
439
|
+
const redirectedCredentialUpload = new RegExp(
|
|
440
|
+
String.raw`\b(?:` +
|
|
441
|
+
String.raw`(?:curl|wget)\b[^;\n]*(?:--data(?:-binary|-raw|-urlencode)?|-d|--form|-F|--upload-file|-T|--post-file|--post-data)[^;\n]*` +
|
|
442
|
+
String.raw`|(?:nc|ncat|socat)\b[^;\n]*` +
|
|
443
|
+
String.raw`)<\s*["']?` +
|
|
444
|
+
credentialPath,
|
|
445
|
+
"i",
|
|
446
|
+
);
|
|
447
|
+
// The pipe matcher covers any content-emitting reader that names a secret
|
|
448
|
+
// file (a bare filename such as `head .env`, `dd if=.env`, or a path like
|
|
449
|
+
// ~/.aws/credentials) and feeds a network sink. The producer set is
|
|
450
|
+
// enumerated rather than derived from directCredentialUpload because a bare
|
|
451
|
+
// filename argument carries no path/@/redirect evidence; keeping the list
|
|
452
|
+
// broad is fail-closed and cheap, since a match also requires a secret-file
|
|
453
|
+
// token before the pipe and a network sink after it. This is best-effort
|
|
454
|
+
// hardening, not a completeness guarantee: readers not on the list still
|
|
455
|
+
// fall through to the contextual matchers below or the dynamic reviewer.
|
|
456
|
+
// The leading lookbehind keeps the producers from
|
|
457
|
+
// matching mid-word (star, code) so the terms stay real tool tokens.
|
|
458
|
+
const credentialPipe = new RegExp(
|
|
459
|
+
String.raw`(?<![A-Za-z0-9_])(?:cat|sed|awk|base64|openssl|head|tail|grep|dd|sort|cut|strings|rev|uniq|fold|od|xxd|hexdump|base32|uuencode|gzip|bzip2|xz|zstd|tar|zip)\b[^|;\n]*(?:\.ssh|\.aws|\.kube|\.docker|\.npmrc|\.netrc|` +
|
|
460
|
+
String.raw`(?:^|[\s"'=/@])` +
|
|
461
|
+
envFile +
|
|
462
|
+
String.raw`|auth\.json)[^|;\n]*\|[^;\n]*\b(?:curl|wget|nc|ncat|socat)\b`,
|
|
463
|
+
"i",
|
|
464
|
+
);
|
|
465
|
+
const credentialReader =
|
|
466
|
+
String.raw`(?<![A-Za-z0-9_])(?:cat|sed|awk|base64|openssl|head|tail|grep|dd|sort|cut|strings|rev|uniq|fold|od|xxd|hexdump|base32|uuencode|gzip|bzip2|xz|zstd|tar|zip)\b[^|;\n]*` +
|
|
467
|
+
credentialPath;
|
|
468
|
+
const credentialShellExpansion =
|
|
469
|
+
String.raw`(?:\$\([^)]*` +
|
|
470
|
+
credentialReader +
|
|
471
|
+
String.raw`[^)]*\)|<\([^)]*` +
|
|
472
|
+
credentialReader +
|
|
473
|
+
String.raw`[^)]*\)|\`[^\`\n]*` +
|
|
474
|
+
credentialReader +
|
|
475
|
+
String.raw`[^\`\n]*\`)`;
|
|
476
|
+
const credentialExpansionUpload = new RegExp(
|
|
477
|
+
String.raw`\b(?:curl|wget)\b[^;\n]*(?:--data(?:-binary|-raw|-urlencode)?|-d|--form|-F|--upload-file|-T|--post-file|--post-data)[^;\n]*` +
|
|
478
|
+
credentialShellExpansion,
|
|
479
|
+
"i",
|
|
480
|
+
);
|
|
481
|
+
const redirectedCredentialExpansion = new RegExp(
|
|
482
|
+
String.raw`\b(?:nc|ncat|socat)\b[^;\n]*(?:<<<|<)\s*` +
|
|
483
|
+
credentialShellExpansion,
|
|
484
|
+
"i",
|
|
485
|
+
);
|
|
486
|
+
const stagedCredentialVariableRead = new RegExp(
|
|
487
|
+
String.raw`\b([A-Za-z_][A-Za-z0-9_]*)\s*=\s*` +
|
|
488
|
+
credentialShellExpansion,
|
|
489
|
+
"gi",
|
|
490
|
+
);
|
|
491
|
+
let stagedCredentialVariableUpload = false;
|
|
492
|
+
for (const match of command.matchAll(stagedCredentialVariableRead)) {
|
|
493
|
+
const variable = match[1];
|
|
494
|
+
const suffix = command.slice((match.index ?? 0) + match[0].length);
|
|
495
|
+
const escapedVariable = variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
496
|
+
const variableUpload = new RegExp(
|
|
497
|
+
String.raw`(?:\b(?:curl|wget)\b[^;\n]*(?:--data(?:-binary|-raw|-urlencode)?|-d|--form|-F|--upload-file|-T|--post-file|--post-data)[^;\n]*|\b(?:nc|ncat|socat)\b[^;\n]*<<<\s*["']?)` +
|
|
498
|
+
String.raw`(?:\$\{` +
|
|
499
|
+
escapedVariable +
|
|
500
|
+
String.raw`\}|\$` +
|
|
501
|
+
escapedVariable +
|
|
502
|
+
String.raw`(?![A-Za-z0-9_]))`,
|
|
503
|
+
"i",
|
|
504
|
+
);
|
|
505
|
+
if (variableUpload.test(suffix)) {
|
|
506
|
+
stagedCredentialVariableUpload = true;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const credentialSubstitution = new RegExp(
|
|
511
|
+
String.raw`\b(?:nc|ncat|socat)\b[^;\n]*\$\([^)]*` +
|
|
512
|
+
credentialReader +
|
|
513
|
+
String.raw`[^)]*\)`,
|
|
514
|
+
"i",
|
|
515
|
+
);
|
|
516
|
+
const stagedCredentialRead = new RegExp(
|
|
517
|
+
credentialReader + String.raw`[^|;\n]*>\s*["']?([^\s"';&|]+)["']?`,
|
|
518
|
+
"gi",
|
|
519
|
+
);
|
|
520
|
+
let stagedCredentialUpload = false;
|
|
521
|
+
for (const match of command.matchAll(stagedCredentialRead)) {
|
|
522
|
+
const target = match[1];
|
|
523
|
+
const suffix = command.slice((match.index ?? 0) + match[0].length);
|
|
524
|
+
const escapedTarget = target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
525
|
+
const targetUpload = new RegExp(
|
|
526
|
+
String.raw`\b(?:curl|wget)\b[^;\n]*(?:` +
|
|
527
|
+
String.raw`(?:--data(?:-binary|-urlencode)?|-d|--form|-F)(?:=|\s+)[^;\n\s]*@\s*["']?` + escapedTarget +
|
|
528
|
+
String.raw`|(?:--upload-file|--post-file)(?:=|\s+)["']?` + escapedTarget +
|
|
529
|
+
String.raw`|-T(?:\s*|=)["']?` + escapedTarget +
|
|
530
|
+
String.raw`)(?=[\s"']|$)`,
|
|
531
|
+
"i",
|
|
532
|
+
);
|
|
533
|
+
if (targetUpload.test(suffix)) {
|
|
534
|
+
stagedCredentialUpload = true;
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
307
538
|
if (
|
|
308
|
-
|
|
309
|
-
|
|
539
|
+
directCredentialUpload.test(command) ||
|
|
540
|
+
redirectedCredentialUpload.test(command) ||
|
|
541
|
+
credentialPipe.test(command) ||
|
|
542
|
+
credentialExpansionUpload.test(command) ||
|
|
543
|
+
redirectedCredentialExpansion.test(command) ||
|
|
544
|
+
credentialSubstitution.test(command) ||
|
|
545
|
+
stagedCredentialVariableUpload ||
|
|
546
|
+
stagedCredentialUpload
|
|
310
547
|
) {
|
|
311
548
|
return {
|
|
312
549
|
rule: "credential-exfiltration",
|
|
@@ -368,7 +605,9 @@ function userText(message: Record<string, unknown>): string {
|
|
|
368
605
|
.trim();
|
|
369
606
|
}
|
|
370
607
|
|
|
371
|
-
function toolTexts(
|
|
608
|
+
function toolTexts(
|
|
609
|
+
message: Record<string, unknown>,
|
|
610
|
+
): Array<{ id?: string; name: string; text: string }> {
|
|
372
611
|
if (message.role !== "assistant" || !Array.isArray(message.content)) {
|
|
373
612
|
return [];
|
|
374
613
|
}
|
|
@@ -378,13 +617,23 @@ function toolTexts(message: Record<string, unknown>): string[] {
|
|
|
378
617
|
if (record.type !== "toolCall" || typeof record.name !== "string") {
|
|
379
618
|
return [];
|
|
380
619
|
}
|
|
381
|
-
|
|
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
|
+
}];
|
|
382
631
|
});
|
|
383
632
|
}
|
|
384
633
|
|
|
385
634
|
function extractEvidence(entries: readonly unknown[]): Evidence[] {
|
|
386
635
|
const evidence: Evidence[] = [];
|
|
387
|
-
entries.forEach((entry) => {
|
|
636
|
+
entries.forEach((entry, entryIndex) => {
|
|
388
637
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return;
|
|
389
638
|
const message = (entry as Record<string, unknown>).message;
|
|
390
639
|
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
@@ -394,13 +643,34 @@ function extractEvidence(entries: readonly unknown[]): Evidence[] {
|
|
|
394
643
|
const user = userText(record);
|
|
395
644
|
if (user) {
|
|
396
645
|
evidence.push({
|
|
646
|
+
id: `${stableEntryId(entry, entryIndex)}:user`,
|
|
397
647
|
index: evidence.length,
|
|
648
|
+
entryIndex,
|
|
398
649
|
kind: "user",
|
|
399
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,
|
|
400
656
|
});
|
|
401
657
|
}
|
|
402
658
|
for (const tool of toolTexts(record)) {
|
|
403
|
-
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
|
+
});
|
|
404
674
|
}
|
|
405
675
|
});
|
|
406
676
|
return evidence;
|
|
@@ -411,6 +681,8 @@ type ToolCallRecord = {
|
|
|
411
681
|
name: string;
|
|
412
682
|
arguments: unknown;
|
|
413
683
|
rendered: string;
|
|
684
|
+
entryIndex: number;
|
|
685
|
+
candidateId: string;
|
|
414
686
|
};
|
|
415
687
|
|
|
416
688
|
function messageRecord(entry: unknown): Record<string, unknown> | undefined {
|
|
@@ -420,6 +692,14 @@ function messageRecord(entry: unknown): Record<string, unknown> | undefined {
|
|
|
420
692
|
return message as Record<string, unknown>;
|
|
421
693
|
}
|
|
422
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
|
+
|
|
423
703
|
function redactSensitiveResult(value: string): string {
|
|
424
704
|
return value
|
|
425
705
|
.replace(
|
|
@@ -544,20 +824,6 @@ function relevanceReason(
|
|
|
544
824
|
| "provider-branch-protection"
|
|
545
825
|
| undefined {
|
|
546
826
|
if (request.toolCallId && call.id === request.toolCallId) return "same-tool";
|
|
547
|
-
const callText = call.rendered.toLowerCase();
|
|
548
|
-
const needles = [
|
|
549
|
-
request.path,
|
|
550
|
-
request.resolvedPath,
|
|
551
|
-
request.destination,
|
|
552
|
-
request.command,
|
|
553
|
-
]
|
|
554
|
-
.filter((value): value is string => Boolean(value && value.length >= 3))
|
|
555
|
-
.map((value) => value.toLowerCase());
|
|
556
|
-
const namesMatch =
|
|
557
|
-
Boolean(request.toolName) &&
|
|
558
|
-
(call.name === request.toolName ||
|
|
559
|
-
call.name.endsWith(`/${request.toolName}`) ||
|
|
560
|
-
request.toolName?.endsWith(`/${call.name}`));
|
|
561
827
|
const currentCommand = request.command || "";
|
|
562
828
|
const priorCommand = commandArgument(call);
|
|
563
829
|
const pushedBranch = explicitPushBranch(currentCommand);
|
|
@@ -566,13 +832,19 @@ function relevanceReason(
|
|
|
566
832
|
/\b(?:rm|rmdir|unlink|trash|delete)\b/i.test(currentCommand);
|
|
567
833
|
const readOnlyCheck =
|
|
568
834
|
/^\s*(?:stat|ls|find|test|readlink|realpath)\b/i.test(priorCommand);
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
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
|
+
);
|
|
572
844
|
if (
|
|
573
845
|
destructive &&
|
|
574
846
|
readOnlyCheck &&
|
|
575
|
-
targets.some((target) =>
|
|
847
|
+
targets.some((target) => priorWords.includes(target))
|
|
576
848
|
) {
|
|
577
849
|
return "delete-precheck";
|
|
578
850
|
}
|
|
@@ -591,21 +863,40 @@ function relevanceReason(
|
|
|
591
863
|
) {
|
|
592
864
|
return "provider-branch-protection";
|
|
593
865
|
}
|
|
594
|
-
if (
|
|
595
|
-
(namesMatch || needles.length > 0) &&
|
|
596
|
-
needles.some((value) => callText.includes(value))
|
|
597
|
-
) {
|
|
598
|
-
return "same-tool";
|
|
599
|
-
}
|
|
600
866
|
return undefined;
|
|
601
867
|
}
|
|
602
868
|
|
|
603
869
|
function relevantResultEvidence(
|
|
604
870
|
entries: readonly unknown[],
|
|
605
871
|
request: RelevantBoundaryRequest,
|
|
606
|
-
|
|
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
|
+
}> {
|
|
607
891
|
const calls = new Map<string, ToolCallRecord>();
|
|
608
|
-
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
|
+
}> = [];
|
|
609
900
|
entries.forEach((entry, index) => {
|
|
610
901
|
const message = messageRecord(entry);
|
|
611
902
|
if (!message) return;
|
|
@@ -626,6 +917,8 @@ function relevantResultEvidence(
|
|
|
626
917
|
name: record.name,
|
|
627
918
|
arguments: record.arguments,
|
|
628
919
|
rendered: `${record.name} ${boundedString(record.arguments ?? {})}`,
|
|
920
|
+
entryIndex: index,
|
|
921
|
+
candidateId: `tool-call:${id}`,
|
|
629
922
|
});
|
|
630
923
|
}
|
|
631
924
|
return;
|
|
@@ -640,58 +933,728 @@ function relevantResultEvidence(
|
|
|
640
933
|
if (!call) return;
|
|
641
934
|
const reason = relevanceReason(call, request);
|
|
642
935
|
const text = resultText(message);
|
|
643
|
-
|
|
936
|
+
const crossTurnAllowed = reason === "same-tool";
|
|
937
|
+
if (reason && text && (crossTurnAllowed || call.entryIndex >= currentTurnStart)) {
|
|
644
938
|
results.push({
|
|
645
939
|
index,
|
|
646
|
-
|
|
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>`,
|
|
647
945
|
});
|
|
648
946
|
}
|
|
649
947
|
});
|
|
650
948
|
return results;
|
|
651
949
|
}
|
|
652
950
|
|
|
653
|
-
function
|
|
951
|
+
function sandboxTrapSupplement(
|
|
654
952
|
request: RelevantBoundaryRequest,
|
|
655
953
|
): string | undefined {
|
|
656
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;
|
|
657
967
|
return `<sandbox-trap>
|
|
658
|
-
${
|
|
659
|
-
|
|
660
|
-
|
|
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,
|
|
661
1101
|
path: request.path,
|
|
662
1102
|
resolvedPath: request.resolvedPath,
|
|
663
1103
|
destination: request.destination,
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
+
};
|
|
667
1630
|
}
|
|
668
1631
|
|
|
669
1632
|
function selectEvidence(
|
|
670
1633
|
evidence: Evidence[],
|
|
671
1634
|
kind: Evidence["kind"],
|
|
672
|
-
|
|
1635
|
+
budgetTokens: number,
|
|
673
1636
|
): { selected: Evidence[]; truncated: boolean } {
|
|
674
1637
|
const candidates = evidence.filter((item) => item.kind === kind);
|
|
675
|
-
if (candidates.length === 0 ||
|
|
1638
|
+
if (candidates.length === 0 || budgetTokens <= 0) {
|
|
676
1639
|
return { selected: [], truncated: candidates.length > 0 };
|
|
677
1640
|
}
|
|
678
1641
|
|
|
679
1642
|
const selected = new Map<number, Evidence>();
|
|
680
|
-
let remaining =
|
|
1643
|
+
let remaining = budgetTokens;
|
|
681
1644
|
const add = (item: Evidence, limit = remaining): void => {
|
|
682
1645
|
if (selected.has(item.index) || remaining <= 0) return;
|
|
683
|
-
const text = item.text
|
|
1646
|
+
const text = utf8Prefix(item.text, Math.min(remaining, limit));
|
|
684
1647
|
if (!text) return;
|
|
685
1648
|
selected.set(item.index, { ...item, text });
|
|
686
|
-
remaining -= text
|
|
1649
|
+
remaining -= Buffer.byteLength(text, "utf8");
|
|
687
1650
|
};
|
|
688
1651
|
|
|
689
1652
|
// Keep the original user intent as an anchor, then fill from newest to oldest.
|
|
690
1653
|
if (kind === "user") {
|
|
691
1654
|
const firstBudget =
|
|
692
1655
|
candidates.length > 1
|
|
693
|
-
? Math.max(1, Math.floor(
|
|
694
|
-
:
|
|
1656
|
+
? Math.max(1, Math.floor(budgetTokens / 2))
|
|
1657
|
+
: budgetTokens;
|
|
695
1658
|
add(candidates[0], firstBudget);
|
|
696
1659
|
if (candidates.length > 1) add(candidates[candidates.length - 1]);
|
|
697
1660
|
}
|
|
@@ -717,68 +1680,229 @@ export function buildClassifierTranscript(
|
|
|
717
1680
|
request: RelevantBoundaryRequest = {},
|
|
718
1681
|
): TranscriptResult {
|
|
719
1682
|
const evidence = extractEvidence(entries);
|
|
720
|
-
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,
|
|
721
1691
|
evidence,
|
|
722
|
-
|
|
723
|
-
|
|
1692
|
+
request,
|
|
1693
|
+
profile,
|
|
1694
|
+
config.maxUserTranscriptTokens,
|
|
724
1695
|
);
|
|
725
|
-
const
|
|
1696
|
+
const requestAwareTools = selectRequestAwareTools(
|
|
1697
|
+
entries,
|
|
726
1698
|
evidence,
|
|
727
|
-
|
|
728
|
-
|
|
1699
|
+
request,
|
|
1700
|
+
relevantUnits,
|
|
1701
|
+
turnStart,
|
|
729
1702
|
);
|
|
730
|
-
const
|
|
731
|
-
|
|
1703
|
+
const tools = selectEvidence(
|
|
1704
|
+
requestAwareTools.selected,
|
|
1705
|
+
"tool",
|
|
1706
|
+
config.maxToolTranscriptTokens,
|
|
732
1707
|
);
|
|
733
|
-
const baseRendered = selected
|
|
734
|
-
.map(
|
|
735
|
-
(item) =>
|
|
736
|
-
`<${item.kind}>\n${escapeEvidenceMarkup(item.text)}\n</${item.kind}>`,
|
|
737
|
-
)
|
|
738
|
-
.join("\n\n");
|
|
739
1708
|
const relevantBudget =
|
|
740
|
-
|
|
1709
|
+
config.maxRelevantResultTokens ?? config.maxToolTranscriptTokens;
|
|
1710
|
+
const sandboxSupplement = sandboxTrapSupplement(request);
|
|
1711
|
+
const sandboxEvidence = sandboxTrapEvidence(request);
|
|
741
1712
|
const relevantCandidates = [
|
|
742
|
-
...(
|
|
743
|
-
? [{
|
|
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
|
+
}]
|
|
744
1724
|
: []),
|
|
745
|
-
...
|
|
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
|
+
})),
|
|
746
1739
|
];
|
|
747
1740
|
let relevantRemaining = relevantBudget;
|
|
748
|
-
|
|
749
|
-
|
|
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) {
|
|
750
1752
|
if (relevantRemaining <= 0) break;
|
|
751
|
-
|
|
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);
|
|
752
1761
|
if (text) {
|
|
753
|
-
relevantSelected.
|
|
754
|
-
relevantRemaining -= text
|
|
1762
|
+
relevantSelected.push({ ...candidate, selectedText: text });
|
|
1763
|
+
relevantRemaining -= Buffer.byteLength(text, "utf8");
|
|
755
1764
|
}
|
|
756
1765
|
}
|
|
757
|
-
|
|
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
|
+
]
|
|
758
1805
|
.filter(Boolean)
|
|
759
1806
|
.join("\n\n");
|
|
760
1807
|
const relevantTruncated =
|
|
761
1808
|
relevantSelected.length < relevantCandidates.length ||
|
|
762
|
-
relevantSelected.reduce(
|
|
1809
|
+
relevantSelected.reduce(
|
|
1810
|
+
(total, value) => total + value.selectedText.length,
|
|
1811
|
+
0,
|
|
1812
|
+
) <
|
|
763
1813
|
relevantCandidates.reduce((total, value) => total + value.text.length, 0);
|
|
764
|
-
const truncated =
|
|
1814
|
+
const truncated =
|
|
1815
|
+
users.truncated ||
|
|
1816
|
+
tools.truncated ||
|
|
1817
|
+
finalTools.length < tools.selected.length ||
|
|
1818
|
+
relevantTruncated;
|
|
765
1819
|
const text = truncated
|
|
766
1820
|
? `[Some transcript evidence was omitted or truncated.]\n\n${rendered}`
|
|
767
1821
|
: rendered;
|
|
768
1822
|
return {
|
|
769
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: [],
|
|
770
1850
|
userCharacters: users.selected.reduce(
|
|
771
1851
|
(total, item) => total + item.text.length,
|
|
772
1852
|
0,
|
|
773
1853
|
),
|
|
774
|
-
toolCharacters:
|
|
1854
|
+
toolCharacters: finalTools.reduce(
|
|
775
1855
|
(total, item) => total + item.text.length,
|
|
776
1856
|
0,
|
|
777
1857
|
),
|
|
778
1858
|
relevantResultCharacters: relevantSelected.reduce(
|
|
779
|
-
(total, item) =>
|
|
1859
|
+
(total, item) =>
|
|
1860
|
+
total +
|
|
1861
|
+
(item.selectedText === item.text
|
|
1862
|
+
? item.content.length
|
|
1863
|
+
: item.selectedText.length),
|
|
780
1864
|
0,
|
|
781
1865
|
),
|
|
782
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,
|
|
783
1907
|
};
|
|
784
1908
|
}
|