@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,275 @@
|
|
|
1
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type ConsultingDeniedOperation,
|
|
5
|
+
type ConsultingFileResource,
|
|
6
|
+
MAX_FAILURE_RESOURCE_PATH_CHARS,
|
|
7
|
+
MAX_FAILURE_RESOURCES,
|
|
8
|
+
} from "../../errors.ts";
|
|
9
|
+
import {
|
|
10
|
+
eventStatus,
|
|
11
|
+
eventType,
|
|
12
|
+
extractWorkspacePath,
|
|
13
|
+
isErrorStatus,
|
|
14
|
+
isRecord,
|
|
15
|
+
isSuccessStatus,
|
|
16
|
+
normalizeToolName,
|
|
17
|
+
safeString,
|
|
18
|
+
structuredWorkspacePath,
|
|
19
|
+
toolInput,
|
|
20
|
+
} from "./event-values.ts";
|
|
21
|
+
|
|
22
|
+
/** Reduce known tool names to fixed diagnostic categories, without retaining arbitrary names. */
|
|
23
|
+
export function classifyDeniedOperation(name: string | undefined): ConsultingDeniedOperation {
|
|
24
|
+
switch (name) {
|
|
25
|
+
case "command":
|
|
26
|
+
case "run_command":
|
|
27
|
+
case "run_terminal_command":
|
|
28
|
+
return "command";
|
|
29
|
+
case "read_file":
|
|
30
|
+
case "view_file":
|
|
31
|
+
case "view_file_outline":
|
|
32
|
+
case "view_file_item":
|
|
33
|
+
case "list_dir":
|
|
34
|
+
case "list_files":
|
|
35
|
+
case "find_by_name":
|
|
36
|
+
case "grep_search":
|
|
37
|
+
case "code_search":
|
|
38
|
+
return "file-read";
|
|
39
|
+
case "write_file":
|
|
40
|
+
case "write_to_file":
|
|
41
|
+
case "replace_file_content":
|
|
42
|
+
case "multi_replace_file_content":
|
|
43
|
+
case "apply_patch":
|
|
44
|
+
return "file-write";
|
|
45
|
+
case "search_web":
|
|
46
|
+
case "read_url_content":
|
|
47
|
+
case "read_url":
|
|
48
|
+
case "web_search":
|
|
49
|
+
return "web-read";
|
|
50
|
+
case "execute_url":
|
|
51
|
+
case "browser_subagent":
|
|
52
|
+
return "browser";
|
|
53
|
+
default:
|
|
54
|
+
return name === "mcp" || name?.startsWith("mcp_") ? "mcp" : "other";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Classify result events, preserving their legacy implicit-success default when status is omitted. */
|
|
59
|
+
export function classifyToolResult(event: Record<string, unknown>): {
|
|
60
|
+
kind: "pending" | "failure" | "success";
|
|
61
|
+
denied: boolean;
|
|
62
|
+
} {
|
|
63
|
+
const status = eventStatus(event);
|
|
64
|
+
const denied = isPermissionDenial(event);
|
|
65
|
+
if (denied || isErrorStatus(status)) return { kind: "failure", denied };
|
|
66
|
+
return { kind: isSuccessStatus(status) ? "success" : "pending", denied };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Fixed operation minima from terminal summaries, with no inferred call IDs or file targets. */
|
|
70
|
+
export function terminalDeniedOperations(
|
|
71
|
+
event: Record<string, unknown>,
|
|
72
|
+
): Map<ConsultingDeniedOperation, string> {
|
|
73
|
+
const operations = new Map<ConsultingDeniedOperation, string>();
|
|
74
|
+
if (!Array.isArray(event.denied_actions)) return operations;
|
|
75
|
+
for (const action of event.denied_actions) {
|
|
76
|
+
const name = isRecord(action) ? safeString(action.action, 80) : undefined;
|
|
77
|
+
if (!name) continue;
|
|
78
|
+
const normalizedName = normalizeToolName(name);
|
|
79
|
+
operations.set(classifyDeniedOperation(normalizedName), normalizedName);
|
|
80
|
+
}
|
|
81
|
+
return operations;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Extract a file target: undefined means omitted; null means explicitly supplied but unavailable. */
|
|
85
|
+
function requestedFileResource(
|
|
86
|
+
name: string | undefined,
|
|
87
|
+
event: Record<string, unknown>,
|
|
88
|
+
): ConsultingFileResource | null | undefined {
|
|
89
|
+
const operation = classifyDeniedOperation(name);
|
|
90
|
+
if (operation !== "file-read" && operation !== "file-write") return undefined;
|
|
91
|
+
const field = structuredWorkspacePath(toolInput(event) ?? event);
|
|
92
|
+
if (!field) return undefined;
|
|
93
|
+
const path = field.value;
|
|
94
|
+
return typeof path === "string" &&
|
|
95
|
+
path.length > 0 &&
|
|
96
|
+
path.length <= MAX_FAILURE_RESOURCE_PATH_CHARS
|
|
97
|
+
? { operation, requestedPath: path }
|
|
98
|
+
: null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Bounded, deduplicated requested targets for one category of file observations. */
|
|
102
|
+
export class FileResources {
|
|
103
|
+
#resources: ConsultingFileResource[] = [];
|
|
104
|
+
#omitted = 0;
|
|
105
|
+
#unavailable = 0;
|
|
106
|
+
|
|
107
|
+
/** Record an observation, counting absent targets and capacity omissions without guessing. */
|
|
108
|
+
record(operation: ConsultingDeniedOperation, resource?: ConsultingFileResource | null): void {
|
|
109
|
+
if (operation !== "file-read" && operation !== "file-write") return;
|
|
110
|
+
if (!resource || resource.operation !== operation) {
|
|
111
|
+
this.#unavailable += 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (
|
|
115
|
+
this.#resources.some(
|
|
116
|
+
(item) => item.operation === operation && item.requestedPath === resource.requestedPath,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
return;
|
|
120
|
+
if (this.#resources.length === MAX_FAILURE_RESOURCES) {
|
|
121
|
+
this.#omitted += 1;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.#resources.push({ ...resource });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Copy metadata only; neither raw payloads nor inferred outcomes are exposed. */
|
|
128
|
+
get snapshot() {
|
|
129
|
+
return {
|
|
130
|
+
resources: this.#resources.map((resource) => ({ ...resource })),
|
|
131
|
+
omitted: this.#omitted,
|
|
132
|
+
unavailable: this.#unavailable,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Recognize denial observations without retaining the raw error or tool output. */
|
|
138
|
+
export function isPermissionDenial(event: Record<string, unknown>): boolean {
|
|
139
|
+
const status = eventStatus(event);
|
|
140
|
+
if (["denied", "permission_denied", "forbidden", "blocked"].includes(status ?? "")) return true;
|
|
141
|
+
const type = eventType(event);
|
|
142
|
+
if (type.includes("permission") || type.includes("denied") || type.includes("blocked"))
|
|
143
|
+
return true;
|
|
144
|
+
const hasExplicitFailure =
|
|
145
|
+
event.is_error === true ||
|
|
146
|
+
event.isError === true ||
|
|
147
|
+
isErrorStatus(status) ||
|
|
148
|
+
event.error !== undefined ||
|
|
149
|
+
event.reason !== undefined;
|
|
150
|
+
if (!hasExplicitFailure) return false;
|
|
151
|
+
const message = [event.error, event.reason, event.message, event.content]
|
|
152
|
+
.map((value) => safeDenialText(value))
|
|
153
|
+
.join(" ")
|
|
154
|
+
.toLowerCase();
|
|
155
|
+
return message.includes("permission") || message.includes("denied");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function safeDenialText(value: unknown, depth = 0): string {
|
|
159
|
+
if (depth > 3) return "";
|
|
160
|
+
if (typeof value === "string") return value.slice(0, 4_000);
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
return value
|
|
163
|
+
.slice(0, 20)
|
|
164
|
+
.map((item) => safeDenialText(item, depth + 1))
|
|
165
|
+
.join(" ");
|
|
166
|
+
}
|
|
167
|
+
if (!isRecord(value)) return "";
|
|
168
|
+
return ["text", "message", "content", "error", "reason"]
|
|
169
|
+
.map((key) => safeDenialText(value[key], depth + 1))
|
|
170
|
+
.join(" ");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Approved filesystem roots for diagnostic location classification, not permission enforcement. */
|
|
174
|
+
export interface DiagnosticReadScope {
|
|
175
|
+
workspaceDirectory?: string;
|
|
176
|
+
additionalReadOnlyDirectories?: readonly string[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Correlated target facts; null invalidates an earlier resource instead of reviving a stale path. */
|
|
180
|
+
export interface FileTargetObservation {
|
|
181
|
+
requestedResource?: ConsultingFileResource | null;
|
|
182
|
+
outsideReadRoots?: boolean;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Preserve earlier facts only when a path is omitted; explicit replacements supersede both facts. */
|
|
186
|
+
export function fileTargetUpdate(
|
|
187
|
+
name: string | undefined,
|
|
188
|
+
event: Record<string, unknown>,
|
|
189
|
+
scope: DiagnosticReadScope,
|
|
190
|
+
previous?: FileTargetObservation,
|
|
191
|
+
): FileTargetObservation {
|
|
192
|
+
const operation = classifyDeniedOperation(name);
|
|
193
|
+
if (operation !== "file-read" && operation !== "file-write") return {};
|
|
194
|
+
const resource = requestedFileResource(name, event);
|
|
195
|
+
return resource === undefined
|
|
196
|
+
? {
|
|
197
|
+
requestedResource: previous?.requestedResource,
|
|
198
|
+
outsideReadRoots: previous?.outsideReadRoots,
|
|
199
|
+
}
|
|
200
|
+
: {
|
|
201
|
+
requestedResource: resource,
|
|
202
|
+
outsideReadRoots: fileAccessOutsideReadRoots(name, event, scope),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Antigravity caches fetched pages in its own conversation directory and reads them
|
|
208
|
+
* back with file tools. A successful file call proven outside the approved roots is
|
|
209
|
+
* agent-internal, not workspace use. Unknown or missing paths stay counted.
|
|
210
|
+
*/
|
|
211
|
+
export function isAgentInternalFileActivity(
|
|
212
|
+
name: string,
|
|
213
|
+
event: Record<string, unknown>,
|
|
214
|
+
scope: DiagnosticReadScope,
|
|
215
|
+
pending?: FileTargetObservation,
|
|
216
|
+
): boolean {
|
|
217
|
+
const target = pending?.requestedResource ? pending : fileTargetUpdate(name, event, scope);
|
|
218
|
+
return target.outsideReadRoots === true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Classify the requested file location independently of the denial cause. Resolve
|
|
223
|
+
* aliases (including missing targets' existing parents); inaccessible paths and
|
|
224
|
+
* parent traversal stay unknown rather than relying on lexical symlink semantics.
|
|
225
|
+
*/
|
|
226
|
+
export function fileAccessOutsideReadRoots(
|
|
227
|
+
name: string | undefined,
|
|
228
|
+
event: Record<string, unknown>,
|
|
229
|
+
scope: DiagnosticReadScope,
|
|
230
|
+
): boolean | undefined {
|
|
231
|
+
const operation = classifyDeniedOperation(name);
|
|
232
|
+
if (operation !== "file-read" && operation !== "file-write") return undefined;
|
|
233
|
+
const path = extractWorkspacePath(toolInput(event) ?? event, { preserveWhitespace: true });
|
|
234
|
+
if (!path || !scope.workspaceDirectory) return undefined;
|
|
235
|
+
// Resolving '..' lexically can erase a symlink before its destination is inspected.
|
|
236
|
+
if (path.split(/[\\/]/).includes("..")) return undefined;
|
|
237
|
+
const target = canonicalDiagnosticPath(resolve(scope.workspaceDirectory, path));
|
|
238
|
+
const roots = [scope.workspaceDirectory, ...(scope.additionalReadOnlyDirectories ?? [])].map(
|
|
239
|
+
canonicalDiagnosticPath,
|
|
240
|
+
);
|
|
241
|
+
if (!target || roots.some((root) => root === undefined)) return undefined;
|
|
242
|
+
return !roots.some((root) => {
|
|
243
|
+
if (!root) return false;
|
|
244
|
+
const distance = relative(root, target);
|
|
245
|
+
return (
|
|
246
|
+
distance === "" || (!isAbsolute(distance) && distance !== ".." && !distance.startsWith("../"))
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function canonicalDiagnosticPath(path: string): string | undefined {
|
|
252
|
+
let candidate = path;
|
|
253
|
+
const suffix: string[] = [];
|
|
254
|
+
while (true) {
|
|
255
|
+
try {
|
|
256
|
+
return resolve(realpathSync(candidate), ...suffix.reverse());
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (!canResolveParent(candidate, error)) return undefined;
|
|
259
|
+
const parent = dirname(candidate);
|
|
260
|
+
if (parent === candidate) return undefined;
|
|
261
|
+
suffix.push(basename(candidate));
|
|
262
|
+
candidate = parent;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function canResolveParent(path: string, error: unknown): boolean {
|
|
268
|
+
if (!isRecord(error) || !["ENOENT", "ENOTDIR"].includes(String(error.code))) return false;
|
|
269
|
+
try {
|
|
270
|
+
// A dangling symlink is not a lexical child of its containing directory.
|
|
271
|
+
return !lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink();
|
|
272
|
+
} catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -1,4 +1,60 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ERROR_STATES,
|
|
3
|
+
eventStatus,
|
|
4
|
+
eventType,
|
|
5
|
+
isErrorStatus,
|
|
6
|
+
isRecord,
|
|
7
|
+
isSuccessStatus,
|
|
8
|
+
isToolResultEvent,
|
|
9
|
+
isToolStartEvent,
|
|
10
|
+
SUCCESS_STATES,
|
|
11
|
+
safeString,
|
|
12
|
+
toolName,
|
|
13
|
+
} from "./event-values.ts";
|
|
14
|
+
|
|
15
|
+
/** Terminal results need explicit success; the absence of an error is not enough. */
|
|
16
|
+
export function hasExplicitSuccess(
|
|
17
|
+
event: Record<string, unknown>,
|
|
18
|
+
status: string | undefined,
|
|
19
|
+
): boolean {
|
|
20
|
+
return status !== undefined
|
|
21
|
+
? isSuccessStatus(status)
|
|
22
|
+
: event.is_error === false || event.isError === false || event.success === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Only explicit successful activity can establish observed evidence or complete a pending call. */
|
|
26
|
+
export function isSuccessfulActivity(event: Record<string, unknown>): boolean {
|
|
27
|
+
const status = eventStatus(event);
|
|
28
|
+
return (
|
|
29
|
+
!isErrorStatus(status) &&
|
|
30
|
+
isSuccessStatus(status) &&
|
|
31
|
+
(status !== undefined || event.success === true || event.ok === true)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Exclude planning/assistant steps even when their index matches a pending tool call. */
|
|
36
|
+
export function isNonToolStepUpdate(event: Record<string, unknown>): boolean {
|
|
37
|
+
const type = eventType(event);
|
|
38
|
+
if (!["assistant", "step_update"].includes(type)) return false;
|
|
39
|
+
const declaredType = safeString(event.step_type ?? event.stepType, 40)?.toLowerCase();
|
|
40
|
+
if (declaredType) return declaredType !== "tool";
|
|
41
|
+
return toolName(event) === undefined && !isRecord(event.tool_info) && !isRecord(event.toolInfo);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Extract normalized tool activity from assistant content without treating answer text as evidence. */
|
|
45
|
+
export function nestedToolEvents(event: Record<string, unknown>): Record<string, unknown>[] {
|
|
46
|
+
const content = isRecord(event.message) ? event.message.content : undefined;
|
|
47
|
+
if (!Array.isArray(content)) return [];
|
|
48
|
+
return content.filter(isRecord).map(normalizeProtocolEvent).filter(isToolActivityBlock);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isToolActivityBlock(event: Record<string, unknown>): boolean {
|
|
52
|
+
return (
|
|
53
|
+
isToolStartEvent(event) ||
|
|
54
|
+
isToolResultEvent(event) ||
|
|
55
|
+
(eventType(event) === "step_update" && !isNonToolStepUpdate(event))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
2
58
|
|
|
3
59
|
/** Adapt an agy event envelope to the small event vocabulary used by the reducer. */
|
|
4
60
|
export function normalizeProtocolEvent(event: Record<string, unknown>): Record<string, unknown> {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
5
|
+
import { createInspectionAgent } from "../inspection-agent.ts";
|
|
4
6
|
import { type IsolatedAntigravityPaths, prepareIsolatedAntigravityHome } from "../isolated-home.ts";
|
|
5
7
|
import type { AntigravityExecutionFacts, AntigravityProgressCallback } from "../types.ts";
|
|
6
|
-
import { eventType, toolName } from "./event-values.ts";
|
|
8
|
+
import { eventType, isRecord, toolName } from "./event-values.ts";
|
|
7
9
|
import { AntigravityEventAccumulator } from "./events.ts";
|
|
8
10
|
import { MAX_STREAM_EVENTS, parseNdjsonLine, StreamLimitError } from "./ndjson.ts";
|
|
9
11
|
import { AntigravityResponseError } from "./response-error.ts";
|
|
@@ -16,28 +18,43 @@ const DEFAULT_PROBE_TIMEOUT_MS = 15 * 1_000;
|
|
|
16
18
|
export type { AntigravityProbeResult } from "./subprocess.ts";
|
|
17
19
|
export { AntigravityProcessError } from "./subprocess.ts";
|
|
18
20
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Register extra roots per process, never in the shared profile. The enforced
|
|
23
|
+
* write_file(*) and command(*) denials keep these additional roots inspection-only.
|
|
24
|
+
*/
|
|
25
|
+
export function buildAntigravityRunArguments(options: {
|
|
26
|
+
schemaPath: string;
|
|
27
|
+
model: string;
|
|
28
|
+
/** Only new conversations select a profile; resumes retain their saved definition. */
|
|
29
|
+
agentName?: string;
|
|
30
|
+
conversationId?: string;
|
|
31
|
+
workspaceDirectory?: string;
|
|
32
|
+
additionalReadOnlyDirectories?: readonly string[];
|
|
33
|
+
}): string[] {
|
|
26
34
|
return [
|
|
27
35
|
"--input-format",
|
|
28
36
|
"stream-json",
|
|
29
37
|
"--output-format",
|
|
30
38
|
"stream-json",
|
|
31
39
|
"--json-schema",
|
|
32
|
-
schemaPath,
|
|
40
|
+
options.schemaPath,
|
|
33
41
|
"--model",
|
|
34
|
-
model,
|
|
42
|
+
options.model,
|
|
35
43
|
"--print-timeout",
|
|
36
44
|
"5m",
|
|
37
45
|
"--sandbox",
|
|
38
|
-
...
|
|
46
|
+
...[
|
|
47
|
+
...new Set([
|
|
48
|
+
...(options.workspaceDirectory ? [options.workspaceDirectory] : []),
|
|
49
|
+
...(options.additionalReadOnlyDirectories ?? []),
|
|
50
|
+
]),
|
|
51
|
+
].flatMap((directory) => ["--add-dir", directory]),
|
|
39
52
|
"--disable-slash-commands",
|
|
40
|
-
...(conversationId
|
|
53
|
+
...(options.conversationId
|
|
54
|
+
? ["--conversation", options.conversationId]
|
|
55
|
+
: options.agentName
|
|
56
|
+
? ["--agent", options.agentName]
|
|
57
|
+
: []),
|
|
41
58
|
];
|
|
42
59
|
}
|
|
43
60
|
|
|
@@ -60,6 +77,8 @@ export async function runAntigravityConversation(options: {
|
|
|
60
77
|
conversationId?: string;
|
|
61
78
|
/** Add the selected project directory to agy's sandbox workspace. */
|
|
62
79
|
workspaceDirectory?: string;
|
|
80
|
+
/** Canonical inspection roots; fixed profile rules still deny writes and commands. */
|
|
81
|
+
additionalReadOnlyDirectories?: readonly string[];
|
|
63
82
|
schema: Record<string, unknown>;
|
|
64
83
|
signal?: AbortSignal;
|
|
65
84
|
timeoutMs?: number;
|
|
@@ -69,20 +88,45 @@ export async function runAntigravityConversation(options: {
|
|
|
69
88
|
onProcessStart?: () => void;
|
|
70
89
|
}): Promise<AntigravityExecutionFacts> {
|
|
71
90
|
await prepareIsolatedAntigravityHome(options.paths);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
);
|
|
75
|
-
const
|
|
76
|
-
|
|
91
|
+
let schemaDirectory: string | undefined;
|
|
92
|
+
let inspectionAgent: Awaited<ReturnType<typeof createInspectionAgent>> | undefined;
|
|
93
|
+
const responseToken = randomUUID();
|
|
94
|
+
const accumulator = new AntigravityEventAccumulator({
|
|
95
|
+
expectedResponseToken: responseToken,
|
|
96
|
+
workspaceDirectory: options.cwd,
|
|
97
|
+
additionalReadOnlyDirectories: options.additionalReadOnlyDirectories,
|
|
98
|
+
});
|
|
77
99
|
let eventCount = 0;
|
|
78
100
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
options.
|
|
84
|
-
options.workspaceDirectory,
|
|
101
|
+
if (!options.conversationId) {
|
|
102
|
+
inspectionAgent = await createInspectionAgent(options.paths.homeDir);
|
|
103
|
+
}
|
|
104
|
+
schemaDirectory = await mkdtemp(
|
|
105
|
+
join(options.schemaDirectoryParent ?? tmpdir(), "supi-consulting-schema-"),
|
|
85
106
|
);
|
|
107
|
+
const schemaPath = join(schemaDirectory, "answer.json");
|
|
108
|
+
// A resumed CLI can replay its previous structured output. Bind and locally
|
|
109
|
+
// validate this invocation rather than trusting terminal success or schema shape.
|
|
110
|
+
const schema = {
|
|
111
|
+
...options.schema,
|
|
112
|
+
properties: {
|
|
113
|
+
...(isRecord(options.schema.properties) ? options.schema.properties : {}),
|
|
114
|
+
responseToken: { type: "string", enum: [responseToken] },
|
|
115
|
+
},
|
|
116
|
+
required: [
|
|
117
|
+
...(Array.isArray(options.schema.required) ? options.schema.required : []),
|
|
118
|
+
"responseToken",
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
await writeFile(schemaPath, `${JSON.stringify(schema)}\n`, { mode: 0o600 });
|
|
122
|
+
const args = buildAntigravityRunArguments({
|
|
123
|
+
schemaPath,
|
|
124
|
+
model: options.model,
|
|
125
|
+
agentName: inspectionAgent?.name,
|
|
126
|
+
conversationId: options.conversationId,
|
|
127
|
+
workspaceDirectory: options.workspaceDirectory,
|
|
128
|
+
additionalReadOnlyDirectories: options.additionalReadOnlyDirectories,
|
|
129
|
+
});
|
|
86
130
|
await runBoundedChildProcess({
|
|
87
131
|
args,
|
|
88
132
|
cwd: options.cwd,
|
|
@@ -118,7 +162,10 @@ export async function runAntigravityConversation(options: {
|
|
|
118
162
|
},
|
|
119
163
|
);
|
|
120
164
|
} finally {
|
|
121
|
-
await
|
|
165
|
+
await Promise.all([
|
|
166
|
+
inspectionAgent?.dispose(),
|
|
167
|
+
schemaDirectory ? rm(schemaDirectory, { recursive: true, force: true }) : undefined,
|
|
168
|
+
]);
|
|
122
169
|
}
|
|
123
170
|
}
|
|
124
171
|
|