@mrclrchtr/supi-consulting 11.1.0 → 11.3.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 +9 -0
- package/CLAUDE.md +1 -1
- package/README.md +65 -11
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/child-web-tools.ts +110 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +11 -0
- package/package.json +3 -2
- package/scripts/live-probe.ts +83 -35
- package/src/agents/antigravity/adapter.ts +32 -7
- package/src/agents/antigravity/availability.ts +2 -2
- package/src/agents/antigravity/inspection-agent.ts +53 -0
- package/src/agents/antigravity/isolated-home.ts +1 -7
- package/src/agents/antigravity/process/event-values.ts +39 -13
- package/src/agents/antigravity/process/events.ts +156 -131
- package/src/agents/antigravity/process/hooks.ts +9 -1
- package/src/agents/antigravity/process/permission-diagnostics.ts +275 -0
- package/src/agents/antigravity/process/protocol.ts +57 -1
- package/src/agents/antigravity/process/runner.ts +71 -24
- package/src/agents/errors.ts +192 -11
- package/src/agents/types.ts +2 -0
- package/src/catalogue.ts +3 -3
- package/src/config.ts +60 -4
- package/src/runtime.ts +3 -1
- package/src/settings.ts +21 -2
- package/src/tool/consulting_run/execute.ts +26 -2
- package/src/tool/consulting_run/guidance.ts +1 -1
- package/src/tool/consulting_run/input.ts +2 -2
- package/src/tool/consulting_run/register.ts +3 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// The generated name is deliberately excluded: it prevents workspace-name collisions,
|
|
6
|
+
// but is not part of the saved conversation's behavior or compatibility contract.
|
|
7
|
+
const DEFINITION = `description: Bounded read-only consultations with observed evidence.
|
|
8
|
+
mainAgent: true
|
|
9
|
+
subagent: false
|
|
10
|
+
tools:
|
|
11
|
+
- search_web
|
|
12
|
+
- read_url_content
|
|
13
|
+
- view_file
|
|
14
|
+
- list_dir
|
|
15
|
+
- find_by_name
|
|
16
|
+
- grep_search
|
|
17
|
+
- finish
|
|
18
|
+
---
|
|
19
|
+
Provide bounded, read-only consultations using the available inspection tools.
|
|
20
|
+
Do not run commands, modify files, automate browsers, or delegate work.
|
|
21
|
+
For web research, use search_web to discover sources and read_url_content to verify relevant pages before citing them. Report limitations instead of claiming unperformed reads.
|
|
22
|
+
Return workspace evidence as workspace-relative paths; omit references outside the selected workspace.
|
|
23
|
+
Complete each request with finish using the current output schema.
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
/** Compatibility marker for the static profile, not an attestation from Antigravity. */
|
|
27
|
+
export const INSPECTION_AGENT_FINGERPRINT = createHash("sha256").update(DEFINITION).digest("hex");
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create an exclusively owned profile for a new conversation. Antigravity retains
|
|
31
|
+
* its definition in conversation state, so callers remove it after the process ends.
|
|
32
|
+
* Keep this out of general home preparation: discovery and follow-ups need no profile.
|
|
33
|
+
*/
|
|
34
|
+
export async function createInspectionAgent(
|
|
35
|
+
homeDir: string,
|
|
36
|
+
): Promise<{ name: string; dispose: () => Promise<void> }> {
|
|
37
|
+
const parent = join(homeDir, ".gemini", "config", "agents");
|
|
38
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
39
|
+
const name = `supi-consulting-${randomUUID()}`;
|
|
40
|
+
const directory = join(parent, name);
|
|
41
|
+
await mkdir(directory, { mode: 0o700 });
|
|
42
|
+
const dispose = () => rm(directory, { recursive: true, force: true });
|
|
43
|
+
try {
|
|
44
|
+
await writeFile(join(directory, "agent.md"), `---\nname: ${name}\n${DEFINITION}`, {
|
|
45
|
+
mode: 0o600,
|
|
46
|
+
flag: "wx",
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
await dispose();
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
return { name, dispose };
|
|
53
|
+
}
|
|
@@ -67,13 +67,7 @@ export const INSPECTION_PERMISSION_SET = Object.freeze({
|
|
|
67
67
|
allowNonWorkspaceAccess: false,
|
|
68
68
|
permissions: Object.freeze({
|
|
69
69
|
allow: Object.freeze(["read_url(*)"]),
|
|
70
|
-
deny: Object.freeze([
|
|
71
|
-
"write_file(*)",
|
|
72
|
-
"command(*)",
|
|
73
|
-
"unsandboxed(*)",
|
|
74
|
-
"mcp(*)",
|
|
75
|
-
"execute_url(*)",
|
|
76
|
-
]),
|
|
70
|
+
deny: Object.freeze(["write_file(*)", "command(*)", "mcp(*)", "execute_url(*)"]),
|
|
77
71
|
}),
|
|
78
72
|
});
|
|
79
73
|
|
|
@@ -171,24 +171,42 @@ export function extractUrl(value: unknown): string | undefined {
|
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
174
|
+
const WORKSPACE_PATH_FIELDS = [
|
|
175
|
+
"path",
|
|
176
|
+
"file",
|
|
177
|
+
"file_path",
|
|
178
|
+
"relative_path",
|
|
179
|
+
"directory_path",
|
|
180
|
+
"search_path",
|
|
181
|
+
"search_directory",
|
|
182
|
+
"absolute_path",
|
|
183
|
+
"directory",
|
|
184
|
+
] as const;
|
|
185
|
+
|
|
186
|
+
/** Distinguish an omitted path parameter from an explicitly supplied but invalid target. */
|
|
187
|
+
export function structuredWorkspacePath(value: unknown): { value: unknown } | undefined {
|
|
188
|
+
if (!isRecord(value)) return undefined;
|
|
189
|
+
for (const name of WORKSPACE_PATH_FIELDS) {
|
|
190
|
+
for (const [key, field] of Object.entries(value)) {
|
|
191
|
+
if (normalizeFieldName(key) === normalizeFieldName(name)) return { value: field };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Read a structured path; requested-target diagnostics preserve significant filename whitespace. */
|
|
198
|
+
export function extractWorkspacePath(
|
|
199
|
+
value: unknown,
|
|
200
|
+
options: { preserveWhitespace?: boolean } = {},
|
|
201
|
+
): string | undefined {
|
|
202
|
+
return readBoundedField(value, WORKSPACE_PATH_FIELDS, () => true, options.preserveWhitespace);
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
function readBoundedField(
|
|
189
206
|
value: unknown,
|
|
190
207
|
names: readonly string[],
|
|
191
208
|
predicate: (candidate: string) => boolean = () => true,
|
|
209
|
+
preserveWhitespace = false,
|
|
192
210
|
): string | undefined {
|
|
193
211
|
if (!isRecord(value)) return undefined;
|
|
194
212
|
const fields = Object.entries(value);
|
|
@@ -196,13 +214,20 @@ function readBoundedField(
|
|
|
196
214
|
const normalizedName = normalizeFieldName(name);
|
|
197
215
|
for (const [key, field] of fields) {
|
|
198
216
|
if (normalizeFieldName(key) !== normalizedName) continue;
|
|
199
|
-
const candidate =
|
|
217
|
+
const candidate = boundedFieldText(field, preserveWhitespace);
|
|
200
218
|
if (candidate && predicate(candidate)) return candidate;
|
|
201
219
|
}
|
|
202
220
|
}
|
|
203
221
|
return undefined;
|
|
204
222
|
}
|
|
205
223
|
|
|
224
|
+
function boundedFieldText(value: unknown, preserveWhitespace: boolean): string | undefined {
|
|
225
|
+
if (preserveWhitespace && typeof value === "string") {
|
|
226
|
+
return value.length > 0 && value.length <= 4_096 ? value : undefined;
|
|
227
|
+
}
|
|
228
|
+
return safeString(value, 4_096);
|
|
229
|
+
}
|
|
230
|
+
|
|
206
231
|
function normalizeFieldName(value: string): string {
|
|
207
232
|
return value.toLowerCase().replace(/[-_]/g, "");
|
|
208
233
|
}
|
|
@@ -246,6 +271,7 @@ function selectAnswerFields(value: Record<string, unknown>): Record<string, unkn
|
|
|
246
271
|
answer: value.answer,
|
|
247
272
|
sources: value.sources,
|
|
248
273
|
workspaceEvidence: value.workspaceEvidence,
|
|
274
|
+
...(Object.hasOwn(value, "responseToken") ? { responseToken: value.responseToken } : {}),
|
|
249
275
|
};
|
|
250
276
|
}
|
|
251
277
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAbsolute, normalize as normalizePath, relative } from "node:path";
|
|
2
2
|
import { hashEvidence } from "../../../evidence-hash.ts";
|
|
3
3
|
import type { ConsultationUsage } from "../../../types.ts";
|
|
4
|
+
import type { ConsultingDeniedOperation, ConsultingFailureDiagnostics } from "../../errors.ts";
|
|
4
5
|
import { isWebToolName, isWorkspaceToolName } from "../activity.ts";
|
|
5
6
|
import { validateAntigravityAnswer } from "../structured-output.ts";
|
|
6
7
|
import type { AntigravityExecutionFacts } from "../types.ts";
|
|
@@ -13,20 +14,34 @@ import {
|
|
|
13
14
|
extractUrl,
|
|
14
15
|
extractWorkspacePath,
|
|
15
16
|
isErrorStatus,
|
|
16
|
-
isRecord,
|
|
17
|
-
isSuccessStatus,
|
|
18
17
|
isTerminalEvent,
|
|
19
18
|
isToolResultEvent,
|
|
20
19
|
isToolStartEvent,
|
|
21
|
-
safeString,
|
|
22
20
|
toolInput,
|
|
23
21
|
toolName,
|
|
24
22
|
} from "./event-values.ts";
|
|
25
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
classifyDeniedOperation,
|
|
25
|
+
classifyToolResult,
|
|
26
|
+
type DiagnosticReadScope,
|
|
27
|
+
FileResources,
|
|
28
|
+
type FileTargetObservation,
|
|
29
|
+
fileTargetUpdate,
|
|
30
|
+
isAgentInternalFileActivity,
|
|
31
|
+
isPermissionDenial,
|
|
32
|
+
terminalDeniedOperations,
|
|
33
|
+
} from "./permission-diagnostics.ts";
|
|
34
|
+
import {
|
|
35
|
+
hasExplicitSuccess,
|
|
36
|
+
isNonToolStepUpdate,
|
|
37
|
+
isSuccessfulActivity,
|
|
38
|
+
nestedToolEvents,
|
|
39
|
+
normalizeProtocolEvent,
|
|
40
|
+
} from "./protocol.ts";
|
|
26
41
|
import { AntigravityResponseError } from "./response-error.ts";
|
|
27
42
|
import { mergeUsage, readUsage } from "./usage.ts";
|
|
28
43
|
|
|
29
|
-
interface PendingTool {
|
|
44
|
+
interface PendingTool extends FileTargetObservation {
|
|
30
45
|
name: string;
|
|
31
46
|
urlHash?: string;
|
|
32
47
|
pathHash?: string;
|
|
@@ -34,19 +49,16 @@ interface PendingTool {
|
|
|
34
49
|
denied?: boolean;
|
|
35
50
|
}
|
|
36
51
|
|
|
37
|
-
interface ToolResultClassification {
|
|
38
|
-
kind: "pending" | "failure" | "success";
|
|
39
|
-
denied: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
52
|
const MAX_COMPLETED_TOOL_IDS = 4_096;
|
|
43
53
|
|
|
44
54
|
/**
|
|
45
55
|
* Reduces parsed events to bounded execution facts. Raw events and tool payloads
|
|
46
|
-
* are not retained
|
|
56
|
+
* are not retained; only bounded structured requested file targets survive pending calls.
|
|
47
57
|
*/
|
|
48
58
|
export class AntigravityEventAccumulator {
|
|
49
59
|
readonly #workspaceDirectory: string | undefined;
|
|
60
|
+
readonly #readScope: DiagnosticReadScope;
|
|
61
|
+
readonly #expectedResponseToken: string | undefined;
|
|
50
62
|
#pendingTools = new Map<string, PendingTool>();
|
|
51
63
|
#completedToolIds = new Set<string>();
|
|
52
64
|
#conversationId: string | undefined;
|
|
@@ -58,12 +70,17 @@ export class AntigravityEventAccumulator {
|
|
|
58
70
|
#toolCounts = new Map<string, number>();
|
|
59
71
|
#successfulToolNames = new Set<string>();
|
|
60
72
|
#permissionDenials = 0;
|
|
73
|
+
#permissionDenialsByOperation: Partial<Record<ConsultingDeniedOperation, number>> = {};
|
|
74
|
+
#deniedFileAccessOutsideReadRoots = 0;
|
|
75
|
+
#deniedFileResources = new FileResources();
|
|
61
76
|
#unattributedPermissionDenialSeen = false;
|
|
62
77
|
#sourceHashes = new Set<string>();
|
|
63
78
|
#workspacePathHashes = new Set<string>();
|
|
64
79
|
|
|
65
|
-
constructor(options: {
|
|
80
|
+
constructor(options: DiagnosticReadScope & { expectedResponseToken?: string } = {}) {
|
|
66
81
|
this.#workspaceDirectory = options.workspaceDirectory;
|
|
82
|
+
this.#readScope = options;
|
|
83
|
+
this.#expectedResponseToken = options.expectedResponseToken;
|
|
67
84
|
}
|
|
68
85
|
|
|
69
86
|
/** Consume one already-parsed event. */
|
|
@@ -74,8 +91,9 @@ export class AntigravityEventAccumulator {
|
|
|
74
91
|
conversationId(event) ?? conversationId(normalizedEvent) ?? this.#conversationId;
|
|
75
92
|
this.#usage = mergeUsage(this.#usage, mergeUsage(readUsage(event), readUsage(normalizedEvent)));
|
|
76
93
|
const terminal = isTerminalEvent(normalizedEvent);
|
|
77
|
-
const nestedTools =
|
|
78
|
-
|
|
94
|
+
const nestedTools = nestedToolEvents(normalizedEvent);
|
|
95
|
+
for (const toolEvent of nestedTools) this.consume(toolEvent);
|
|
96
|
+
if (nestedTools.length && !terminal) return;
|
|
79
97
|
|
|
80
98
|
if (isToolStartEvent(normalizedEvent)) {
|
|
81
99
|
this.#rememberToolStart(normalizedEvent);
|
|
@@ -90,27 +108,28 @@ export class AntigravityEventAccumulator {
|
|
|
90
108
|
return;
|
|
91
109
|
}
|
|
92
110
|
|
|
93
|
-
|
|
94
|
-
if (directName && isSuccessfulActivity(normalizedEvent)) {
|
|
95
|
-
this.#recordSuccessfulTool(directName, normalizedEvent);
|
|
96
|
-
}
|
|
97
|
-
const denied = isPermissionDenial(normalizedEvent);
|
|
98
|
-
if (denied && !isNonToolStepUpdate(normalizedEvent)) {
|
|
99
|
-
const counted = directName
|
|
100
|
-
? this.#recordPermissionDenial(directName, normalizedEvent)
|
|
101
|
-
: this.#recordUnattributedPermissionDenial();
|
|
102
|
-
if (counted) this.#permissionDenials += 1;
|
|
103
|
-
}
|
|
111
|
+
this.#consumeActivity(normalizedEvent);
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
/** Return safe execution facts even when the response is incomplete. */
|
|
107
|
-
get diagnostics(): {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
get diagnostics(): ConsultingFailureDiagnostics {
|
|
116
|
+
const denied = this.#deniedFileResources.snapshot;
|
|
117
|
+
const unfinished = new FileResources();
|
|
118
|
+
for (const pending of this.#pendingTools.values()) {
|
|
119
|
+
if (!pending.denied)
|
|
120
|
+
unfinished.record(classifyDeniedOperation(pending.name), pending.requestedResource);
|
|
121
|
+
}
|
|
122
|
+
const pending = unfinished.snapshot;
|
|
112
123
|
return {
|
|
113
124
|
permissionDenials: this.#permissionDenials,
|
|
125
|
+
permissionDenialsByOperation: { ...this.#permissionDenialsByOperation },
|
|
126
|
+
deniedFileAccessOutsideReadRoots: this.#deniedFileAccessOutsideReadRoots,
|
|
127
|
+
deniedResources: denied.resources,
|
|
128
|
+
deniedResourcesOmitted: denied.omitted,
|
|
129
|
+
deniedResourcePathsUnavailable: denied.unavailable,
|
|
130
|
+
unfinishedResources: pending.resources,
|
|
131
|
+
unfinishedResourcesOmitted: pending.omitted,
|
|
132
|
+
unfinishedResourcePathsUnavailable: pending.unavailable,
|
|
114
133
|
terminalSeen: this.#terminalSeen,
|
|
115
134
|
terminalSucceeded: this.#terminalSucceeded,
|
|
116
135
|
};
|
|
@@ -122,8 +141,16 @@ export class AntigravityEventAccumulator {
|
|
|
122
141
|
if (!this.#terminalSucceeded) throw new AntigravityResponseError("terminal-failed");
|
|
123
142
|
if (!this.#candidate) throw new AntigravityResponseError("missing-answer");
|
|
124
143
|
if (!this.#conversationId) throw new AntigravityResponseError("missing-conversation");
|
|
144
|
+
const { responseToken, ...candidate } = this.#candidate;
|
|
145
|
+
const answer = validateAntigravityAnswer(candidate);
|
|
146
|
+
if (
|
|
147
|
+
this.#expectedResponseToken !== undefined &&
|
|
148
|
+
responseToken !== this.#expectedResponseToken
|
|
149
|
+
) {
|
|
150
|
+
throw new AntigravityResponseError("unbound-answer");
|
|
151
|
+
}
|
|
125
152
|
return {
|
|
126
|
-
answer
|
|
153
|
+
answer,
|
|
127
154
|
conversationId: this.#conversationId,
|
|
128
155
|
...(this.#usage ? { usage: this.#usage } : {}),
|
|
129
156
|
observedToolNames: [...this.#toolNames],
|
|
@@ -135,6 +162,44 @@ export class AntigravityEventAccumulator {
|
|
|
135
162
|
};
|
|
136
163
|
}
|
|
137
164
|
|
|
165
|
+
#consumeActivity(event: Record<string, unknown>): void {
|
|
166
|
+
if (isNonToolStepUpdate(event)) return;
|
|
167
|
+
const id = eventId(event);
|
|
168
|
+
if (id && this.#completedToolIds.has(id)) return;
|
|
169
|
+
let pending = this.#pendingTools.get(id ?? "");
|
|
170
|
+
const name = toolName(event) ?? pending?.name;
|
|
171
|
+
if (name && eventType(event) === "step_update") {
|
|
172
|
+
pending = this.#recordPendingToolResult(id, name, event, pending);
|
|
173
|
+
}
|
|
174
|
+
if (isPermissionDenial(event)) {
|
|
175
|
+
this.#observeDeniedActivity(name, event, pending);
|
|
176
|
+
} else if (name) {
|
|
177
|
+
this.#observeExplicitOutcome(name, event, pending);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#observeDeniedActivity(
|
|
182
|
+
name: string | undefined,
|
|
183
|
+
event: Record<string, unknown>,
|
|
184
|
+
pending?: PendingTool,
|
|
185
|
+
): void {
|
|
186
|
+
const counted = name
|
|
187
|
+
? this.#recordPermissionDenial(name, event, pending)
|
|
188
|
+
: this.#recordUnattributedPermissionDenial();
|
|
189
|
+
if (counted) this.#countPermissionDenial(name, event, pending);
|
|
190
|
+
if (isErrorStatus(eventStatus(event))) this.#completeToolResult(eventId(event));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#observeExplicitOutcome(
|
|
194
|
+
name: string,
|
|
195
|
+
event: Record<string, unknown>,
|
|
196
|
+
pending?: PendingTool,
|
|
197
|
+
): void {
|
|
198
|
+
const successful = isSuccessfulActivity(event);
|
|
199
|
+
if (successful) this.#recordSuccessfulTool(name, event, pending);
|
|
200
|
+
if (successful || isErrorStatus(eventStatus(event))) this.#completeToolResult(eventId(event));
|
|
201
|
+
}
|
|
202
|
+
|
|
138
203
|
#rememberToolStart(event: Record<string, unknown>): void {
|
|
139
204
|
const name = toolName(event);
|
|
140
205
|
if (!name) return;
|
|
@@ -152,14 +217,15 @@ export class AntigravityEventAccumulator {
|
|
|
152
217
|
observed: true,
|
|
153
218
|
...(urlHash ? { urlHash } : {}),
|
|
154
219
|
...(pathHash ? { pathHash } : {}),
|
|
220
|
+
...fileTargetUpdate(name, event, this.#readScope, previous),
|
|
155
221
|
...(denied ? { denied: true } : {}),
|
|
156
222
|
},
|
|
157
223
|
previous,
|
|
158
224
|
);
|
|
159
225
|
if (!previous) this.#recordObservedTool(name);
|
|
160
226
|
if (id) this.#pendingTools.set(id, pending);
|
|
161
|
-
if (
|
|
162
|
-
if (
|
|
227
|
+
if (denied && !previous?.denied) this.#countPermissionDenial(name, event, pending);
|
|
228
|
+
if (!denied) this.#observeExplicitOutcome(name, event, pending);
|
|
163
229
|
}
|
|
164
230
|
|
|
165
231
|
#consumeToolResult(event: Record<string, unknown>): void {
|
|
@@ -171,12 +237,14 @@ export class AntigravityEventAccumulator {
|
|
|
171
237
|
if (!name) return;
|
|
172
238
|
const classification = classifyToolResult(event);
|
|
173
239
|
if (classification.kind === "pending") {
|
|
174
|
-
this.#recordPendingToolResult(id, name, pending);
|
|
240
|
+
this.#recordPendingToolResult(id, name, event, pending);
|
|
175
241
|
return;
|
|
176
242
|
}
|
|
177
243
|
if (id) this.#completeToolResult(id);
|
|
178
244
|
if (classification.kind === "failure") {
|
|
179
|
-
|
|
245
|
+
if (classification.denied && !pending?.denied)
|
|
246
|
+
this.#countPermissionDenial(name, event, pending);
|
|
247
|
+
if (!pending?.observed) this.#recordObservedTool(name);
|
|
180
248
|
return;
|
|
181
249
|
}
|
|
182
250
|
this.#recordSuccessfulTool(name, event, pending);
|
|
@@ -185,20 +253,41 @@ export class AntigravityEventAccumulator {
|
|
|
185
253
|
#recordPendingToolResult(
|
|
186
254
|
id: string | undefined,
|
|
187
255
|
name: string,
|
|
256
|
+
event: Record<string, unknown>,
|
|
188
257
|
pending: PendingTool | undefined,
|
|
189
|
-
):
|
|
258
|
+
): PendingTool {
|
|
190
259
|
if (!pending?.observed) this.#recordObservedTool(name);
|
|
191
|
-
|
|
260
|
+
const updated = {
|
|
261
|
+
...pending,
|
|
262
|
+
name,
|
|
263
|
+
observed: true,
|
|
264
|
+
...fileTargetUpdate(name, event, this.#readScope, pending),
|
|
265
|
+
};
|
|
266
|
+
if (id) this.#pendingTools.set(id, updated);
|
|
267
|
+
return updated;
|
|
192
268
|
}
|
|
193
269
|
|
|
194
|
-
#completeToolResult(id: string): void {
|
|
270
|
+
#completeToolResult(id: string | undefined): void {
|
|
271
|
+
if (!id) return;
|
|
195
272
|
this.#pendingTools.delete(id);
|
|
196
273
|
this.#rememberCompletedToolId(id);
|
|
197
274
|
}
|
|
198
275
|
|
|
199
|
-
#
|
|
200
|
-
|
|
201
|
-
|
|
276
|
+
#countPermissionDenial(
|
|
277
|
+
name: string | undefined,
|
|
278
|
+
event: Record<string, unknown>,
|
|
279
|
+
pending?: PendingTool,
|
|
280
|
+
): void {
|
|
281
|
+
this.#permissionDenials += 1;
|
|
282
|
+
const operation = classifyDeniedOperation(name);
|
|
283
|
+
this.#permissionDenialsByOperation[operation] =
|
|
284
|
+
(this.#permissionDenialsByOperation[operation] ?? 0) + 1;
|
|
285
|
+
const started = pending ?? this.#pendingTools.get(eventId(event) ?? "");
|
|
286
|
+
const target = fileTargetUpdate(name, event, this.#readScope, started);
|
|
287
|
+
this.#deniedFileResources.record(operation, target.requestedResource);
|
|
288
|
+
if (target.outsideReadRoots) {
|
|
289
|
+
this.#deniedFileAccessOutsideReadRoots += 1;
|
|
290
|
+
}
|
|
202
291
|
}
|
|
203
292
|
|
|
204
293
|
#rememberCompletedToolId(id: string): void {
|
|
@@ -219,10 +308,14 @@ export class AntigravityEventAccumulator {
|
|
|
219
308
|
return true;
|
|
220
309
|
}
|
|
221
310
|
|
|
222
|
-
#recordPermissionDenial(
|
|
311
|
+
#recordPermissionDenial(
|
|
312
|
+
name: string,
|
|
313
|
+
event: Record<string, unknown>,
|
|
314
|
+
observation?: PendingTool,
|
|
315
|
+
): boolean {
|
|
223
316
|
const id = eventId(event);
|
|
224
317
|
if (id && this.#completedToolIds.has(id)) return false;
|
|
225
|
-
const pending = id ? this.#pendingTools.get(id) : undefined;
|
|
318
|
+
const pending = observation ?? (id ? this.#pendingTools.get(id) : undefined);
|
|
226
319
|
if (pending?.denied) return false;
|
|
227
320
|
if (!pending?.observed) this.#recordObservedTool(name);
|
|
228
321
|
if (id) {
|
|
@@ -234,6 +327,7 @@ export class AntigravityEventAccumulator {
|
|
|
234
327
|
|
|
235
328
|
#recordSuccessfulTool(name: string, event: Record<string, unknown>, pending?: PendingTool): void {
|
|
236
329
|
if (!pending?.observed) this.#recordObservedTool(name);
|
|
330
|
+
if (isAgentInternalFileActivity(name, event, this.#readScope, pending)) return;
|
|
237
331
|
this.#successfulToolNames.add(name);
|
|
238
332
|
const input = toolInput(event) ?? event;
|
|
239
333
|
const sourceHash = pending?.urlHash ?? hashValue(extractUrl(input));
|
|
@@ -261,8 +355,27 @@ export class AntigravityEventAccumulator {
|
|
|
261
355
|
return normalizeWorkspacePath(normalized);
|
|
262
356
|
}
|
|
263
357
|
|
|
358
|
+
/** Terminal summaries prove an operation was denied, but supply no call IDs or targets. */
|
|
359
|
+
#consumeTerminalDenials(event: Record<string, unknown>): void {
|
|
360
|
+
const operations = terminalDeniedOperations(event);
|
|
361
|
+
// The generic notice may describe the same denial as the terminal summary.
|
|
362
|
+
// Keep a conservative lower bound without attributing it to a particular call.
|
|
363
|
+
if (operations.size && this.#unattributedPermissionDenialSeen) {
|
|
364
|
+
this.#permissionDenials -= 1;
|
|
365
|
+
const other = (this.#permissionDenialsByOperation.other ?? 1) - 1;
|
|
366
|
+
if (other) this.#permissionDenialsByOperation.other = other;
|
|
367
|
+
else delete this.#permissionDenialsByOperation.other;
|
|
368
|
+
}
|
|
369
|
+
for (const [operation, name] of operations) {
|
|
370
|
+
if (!this.#permissionDenialsByOperation[operation]) {
|
|
371
|
+
this.#countPermissionDenial(name, {});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
264
376
|
#consumeTerminal(event: Record<string, unknown>): void {
|
|
265
377
|
this.#terminalSeen = true;
|
|
378
|
+
this.#consumeTerminalDenials(event);
|
|
266
379
|
const status = eventStatus(event);
|
|
267
380
|
if (isErrorStatus(status) || !hasExplicitSuccess(event, status)) {
|
|
268
381
|
throw new AntigravityResponseError("terminal-failed");
|
|
@@ -273,94 +386,6 @@ export class AntigravityEventAccumulator {
|
|
|
273
386
|
}
|
|
274
387
|
}
|
|
275
388
|
|
|
276
|
-
function consumeNestedToolBlocks(
|
|
277
|
-
accumulator: AntigravityEventAccumulator,
|
|
278
|
-
event: Record<string, unknown>,
|
|
279
|
-
): boolean {
|
|
280
|
-
const message = isRecord(event.message) ? event.message : undefined;
|
|
281
|
-
const content = message?.content;
|
|
282
|
-
if (!Array.isArray(content)) return false;
|
|
283
|
-
let consumed = false;
|
|
284
|
-
for (const block of content) {
|
|
285
|
-
if (!isRecord(block)) continue;
|
|
286
|
-
const normalizedBlock = normalizeProtocolEvent(block);
|
|
287
|
-
if (!isToolActivityBlock(normalizedBlock)) continue;
|
|
288
|
-
accumulator.consume(normalizedBlock);
|
|
289
|
-
consumed = true;
|
|
290
|
-
}
|
|
291
|
-
return consumed;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function isToolActivityBlock(event: Record<string, unknown>): boolean {
|
|
295
|
-
if (isToolStartEvent(event) || isToolResultEvent(event)) return true;
|
|
296
|
-
return eventType(event) === "step_update" && !isNonToolStepUpdate(event);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function hasExplicitSuccess(event: Record<string, unknown>, status: string | undefined): boolean {
|
|
300
|
-
return status !== undefined
|
|
301
|
-
? isSuccessStatus(status)
|
|
302
|
-
: event.is_error === false || event.isError === false || event.success === true;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function isSuccessfulActivity(event: Record<string, unknown>): boolean {
|
|
306
|
-
const status = eventStatus(event);
|
|
307
|
-
return (
|
|
308
|
-
!isErrorStatus(status) &&
|
|
309
|
-
isSuccessStatus(status) &&
|
|
310
|
-
(status !== undefined || event.success === true || event.ok === true)
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function isNonToolStepUpdate(event: Record<string, unknown>): boolean {
|
|
315
|
-
const type = eventType(event);
|
|
316
|
-
if (!["assistant", "step_update"].includes(type)) return false;
|
|
317
|
-
const declaredType = safeString(event.step_type ?? event.stepType, 40)?.toLowerCase();
|
|
318
|
-
if (declaredType) return declaredType !== "tool";
|
|
319
|
-
return toolName(event) === undefined && !isRecord(event.tool_info) && !isRecord(event.toolInfo);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function classifyToolResult(event: Record<string, unknown>): ToolResultClassification {
|
|
323
|
-
const status = eventStatus(event);
|
|
324
|
-
const denied = isPermissionDenial(event);
|
|
325
|
-
if (denied || isErrorStatus(status)) return { kind: "failure", denied };
|
|
326
|
-
return { kind: isSuccessStatus(status) ? "success" : "pending", denied };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function isPermissionDenial(event: Record<string, unknown>): boolean {
|
|
330
|
-
const status = eventStatus(event);
|
|
331
|
-
if (["denied", "permission_denied", "forbidden", "blocked"].includes(status ?? "")) return true;
|
|
332
|
-
const type = eventType(event);
|
|
333
|
-
if (type.includes("permission") || type.includes("denied") || type.includes("blocked"))
|
|
334
|
-
return true;
|
|
335
|
-
const hasExplicitFailure =
|
|
336
|
-
event.is_error === true ||
|
|
337
|
-
event.isError === true ||
|
|
338
|
-
isErrorStatus(status) ||
|
|
339
|
-
event.error !== undefined ||
|
|
340
|
-
event.reason !== undefined;
|
|
341
|
-
if (!hasExplicitFailure) return false;
|
|
342
|
-
const message = [event.error, event.reason, event.message, event.content]
|
|
343
|
-
.map((value) => safeDenialText(value))
|
|
344
|
-
.join(" ")
|
|
345
|
-
.toLowerCase();
|
|
346
|
-
return message.includes("permission") || message.includes("denied");
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function safeDenialText(value: unknown, depth = 0): string {
|
|
350
|
-
if (depth > 3) return "";
|
|
351
|
-
if (typeof value === "string") return value.slice(0, 4_000);
|
|
352
|
-
if (Array.isArray(value)) {
|
|
353
|
-
return value
|
|
354
|
-
.slice(0, 20)
|
|
355
|
-
.map((item) => safeDenialText(item, depth + 1))
|
|
356
|
-
.join(" ");
|
|
357
|
-
}
|
|
358
|
-
if (!isRecord(value)) return "";
|
|
359
|
-
return ["text", "message", "content", "error", "reason"]
|
|
360
|
-
.map((key) => safeDenialText(value[key], depth + 1))
|
|
361
|
-
.join(" ");
|
|
362
|
-
}
|
|
363
|
-
|
|
364
389
|
function mergePendingTool(current: PendingTool, previous: PendingTool | undefined): PendingTool {
|
|
365
390
|
if (previous?.urlHash && !current.urlHash) current.urlHash = previous.urlHash;
|
|
366
391
|
if (previous?.pathHash && !current.pathHash) current.pathHash = previous.pathHash;
|
|
@@ -14,6 +14,8 @@ export interface ProjectHookProbeResult {
|
|
|
14
14
|
export async function probeProjectHooks(options: {
|
|
15
15
|
paths: IsolatedAntigravityPaths;
|
|
16
16
|
cwd: string;
|
|
17
|
+
/** Match the paid process's inspection roots when checking project hook state. */
|
|
18
|
+
additionalReadOnlyDirectories?: readonly string[];
|
|
17
19
|
signal?: AbortSignal;
|
|
18
20
|
onProcessStart?: () => void;
|
|
19
21
|
}): Promise<ProjectHookProbeResult> {
|
|
@@ -21,7 +23,13 @@ export async function probeProjectHooks(options: {
|
|
|
21
23
|
const result = await runAntigravityProbe({
|
|
22
24
|
paths: options.paths,
|
|
23
25
|
cwd: options.cwd,
|
|
24
|
-
args: [
|
|
26
|
+
args: [
|
|
27
|
+
...PROJECT_HOOK_PROBE_ARGUMENTS,
|
|
28
|
+
...(options.additionalReadOnlyDirectories ?? []).flatMap((directory) => [
|
|
29
|
+
"--add-dir",
|
|
30
|
+
directory,
|
|
31
|
+
]),
|
|
32
|
+
],
|
|
25
33
|
signal: options.signal,
|
|
26
34
|
timeoutMs: 15_000,
|
|
27
35
|
onProcessStart: options.onProcessStart,
|