@mrclrchtr/supi-consulting 0.0.0-stage → 9.0.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/CLAUDE.md +23 -0
- package/CONTEXT.md +53 -0
- package/README.md +104 -2
- package/docs/adr/0001-use-an-isolated-antigravity-home.md +5 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +137 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +73 -0
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +42 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +342 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +363 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +17 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/context.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +313 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +120 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug.ts +14 -0
- package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +64 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +38 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +356 -0
- package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +44 -0
- package/node_modules/@mrclrchtr/supi-core/src/path.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/project.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +4 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +93 -0
- package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-capabilities.ts +242 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-capability-profiles.ts +240 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +71 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +47 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +105 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +454 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/node_modules/@mrclrchtr/supi-core/src/tui.ts +141 -0
- package/package.json +73 -4
- package/scripts/live-probe.ts +139 -0
- package/src/agents/antigravity/activity.ts +22 -0
- package/src/agents/antigravity/adapter.ts +196 -0
- package/src/agents/antigravity/availability.ts +231 -0
- package/src/agents/antigravity/isolated-home.ts +346 -0
- package/src/agents/antigravity/models.ts +15 -0
- package/src/agents/antigravity/process/environment.ts +33 -0
- package/src/agents/antigravity/process/event-values.ts +273 -0
- package/src/agents/antigravity/process/events.ts +366 -0
- package/src/agents/antigravity/process/hooks.ts +219 -0
- package/src/agents/antigravity/process/ndjson.ts +120 -0
- package/src/agents/antigravity/process/protocol.ts +69 -0
- package/src/agents/antigravity/process/runner.ts +147 -0
- package/src/agents/antigravity/process/subprocess.ts +278 -0
- package/src/agents/antigravity/process/usage.ts +50 -0
- package/src/agents/antigravity/structured-output.ts +58 -0
- package/src/agents/antigravity/types.ts +17 -0
- package/src/agents/errors.ts +27 -0
- package/src/agents/types.ts +43 -0
- package/src/catalogue.ts +17 -0
- package/src/config.ts +26 -0
- package/src/conversation/handles.ts +187 -0
- package/src/evidence-hash.ts +6 -0
- package/src/extension.ts +26 -0
- package/src/footer-constants.ts +5 -0
- package/src/footer.ts +25 -0
- package/src/runtime.ts +244 -0
- package/src/settings.ts +38 -0
- package/src/tool/consulting_run/evidence.ts +169 -0
- package/src/tool/consulting_run/execute.ts +201 -0
- package/src/tool/consulting_run/guidance.ts +3 -0
- package/src/tool/consulting_run/input.ts +117 -0
- package/src/tool/consulting_run/register.ts +36 -0
- package/src/tool/consulting_run/render.ts +261 -0
- package/src/tool/consulting_run/result.ts +185 -0
- package/src/tool/consulting_run/spec.ts +20 -0
- package/src/types.ts +104 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatAntigravityLoginCommand,
|
|
3
|
+
getIsolatedAntigravityPaths,
|
|
4
|
+
type IsolatedAntigravityPaths,
|
|
5
|
+
} from "./isolated-home.ts";
|
|
6
|
+
import { CURATED_MODELS, type CuratedModel, isCuratedModel } from "./models.ts";
|
|
7
|
+
import type { AntigravityProbeResult } from "./process/runner.ts";
|
|
8
|
+
import { AntigravityProcessError, runAntigravityProbe } from "./process/runner.ts";
|
|
9
|
+
|
|
10
|
+
/** Minimum supported Antigravity CLI version. */
|
|
11
|
+
export const MINIMUM_ANTIGRAVITY_VERSION = Object.freeze({ major: 1, minor: 1, patch: 24 });
|
|
12
|
+
const MAX_DISCOVERY_OUTPUT_BYTES = 128 * 1024;
|
|
13
|
+
|
|
14
|
+
/** Parsed Antigravity CLI version. */
|
|
15
|
+
export interface AntigravityVersion {
|
|
16
|
+
major: number;
|
|
17
|
+
minor: number;
|
|
18
|
+
patch: number;
|
|
19
|
+
prerelease?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A successful immutable availability snapshot. */
|
|
23
|
+
export interface AvailableAntigravity {
|
|
24
|
+
status: "available";
|
|
25
|
+
cliVersion: string;
|
|
26
|
+
catalogue: readonly CuratedModel[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A bounded reason why the tool cannot be registered. */
|
|
30
|
+
export interface UnavailableAntigravity {
|
|
31
|
+
status: "unavailable";
|
|
32
|
+
reason: "missing" | "old-version" | "authentication" | "discovery" | "no-curated-model";
|
|
33
|
+
warning: string;
|
|
34
|
+
cliVersion?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Result of one immutable Antigravity availability discovery. */
|
|
38
|
+
export type AntigravityAvailability = AvailableAntigravity | UnavailableAntigravity;
|
|
39
|
+
|
|
40
|
+
/** Parse the first semantic version in CLI output. */
|
|
41
|
+
export function parseAntigravityVersion(output: string): AntigravityVersion | undefined {
|
|
42
|
+
const match = output.match(/\b(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?\b/);
|
|
43
|
+
if (!match) return undefined;
|
|
44
|
+
return {
|
|
45
|
+
major: Number(match[1]),
|
|
46
|
+
minor: Number(match[2]),
|
|
47
|
+
patch: Number(match[3]),
|
|
48
|
+
...(match[4] ? { prerelease: match[4] } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Return whether a parsed version meets the supported minimum. */
|
|
53
|
+
export function isSupportedAntigravityVersion(version: AntigravityVersion): boolean {
|
|
54
|
+
const minimum = MINIMUM_ANTIGRAVITY_VERSION;
|
|
55
|
+
if (version.major !== minimum.major) return version.major > minimum.major;
|
|
56
|
+
if (version.minor !== minimum.minor) return version.minor > minimum.minor;
|
|
57
|
+
if (version.patch !== minimum.patch) return version.patch > minimum.patch;
|
|
58
|
+
return version.prerelease === undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Format a version without retaining the CLI's complete output. */
|
|
62
|
+
export function formatAntigravityVersion(version: AntigravityVersion): string {
|
|
63
|
+
return `${version.major}.${version.minor}.${version.patch}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Parse bounded tab-separated `agy models` output. */
|
|
67
|
+
export function parseAvailableModels(output: string): string[] {
|
|
68
|
+
if (Buffer.byteLength(output, "utf8") > MAX_DISCOVERY_OUTPUT_BYTES) {
|
|
69
|
+
throw new Error("Antigravity model output exceeded its limit.");
|
|
70
|
+
}
|
|
71
|
+
const found = new Set<string>();
|
|
72
|
+
for (const line of output.split(/\r?\n/)) {
|
|
73
|
+
for (const field of line.split("\t")) {
|
|
74
|
+
const model = field.trim();
|
|
75
|
+
if (isCuratedModel(model)) found.add(model);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return [...found];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Intersect discovered IDs with the immutable curated model order. */
|
|
82
|
+
export function intersectCuratedModels(discovered: Iterable<string>): readonly CuratedModel[] {
|
|
83
|
+
const available = new Set(discovered);
|
|
84
|
+
return Object.freeze(CURATED_MODELS.filter((model) => available.has(model)));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Discover the supported Antigravity models in the Isolated Antigravity Home. */
|
|
88
|
+
export async function discoverAntigravityAvailability(
|
|
89
|
+
options: {
|
|
90
|
+
paths?: IsolatedAntigravityPaths;
|
|
91
|
+
signal?: AbortSignal;
|
|
92
|
+
runProbe?: typeof runAntigravityProbe;
|
|
93
|
+
} = {},
|
|
94
|
+
): Promise<AntigravityAvailability> {
|
|
95
|
+
const paths = options.paths ?? getIsolatedAntigravityPaths();
|
|
96
|
+
const runProbe = options.runProbe ?? runAntigravityProbe;
|
|
97
|
+
let versionResult: AntigravityProbeResult;
|
|
98
|
+
try {
|
|
99
|
+
versionResult = await runProbe({
|
|
100
|
+
paths,
|
|
101
|
+
cwd: paths.consultationWorkspace,
|
|
102
|
+
args: ["--version"],
|
|
103
|
+
signal: options.signal,
|
|
104
|
+
timeoutMs: 15_000,
|
|
105
|
+
maxStdoutBytes: MAX_DISCOVERY_OUTPUT_BYTES,
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return unavailableFromError(error, paths);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const version = parseAntigravityVersion(`${versionResult.stdout}\n${versionResult.stderr}`);
|
|
112
|
+
if (!version || versionResult.exitCode !== 0 || versionResult.signal) {
|
|
113
|
+
return {
|
|
114
|
+
status: "unavailable",
|
|
115
|
+
reason: "discovery",
|
|
116
|
+
warning:
|
|
117
|
+
"Antigravity CLI version could not be read. Install or update Antigravity, then reload PI.",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const cliVersion = formatAntigravityVersion(version);
|
|
121
|
+
if (!isSupportedAntigravityVersion(version)) {
|
|
122
|
+
return {
|
|
123
|
+
status: "unavailable",
|
|
124
|
+
reason: "old-version",
|
|
125
|
+
cliVersion,
|
|
126
|
+
warning: `Antigravity CLI ${cliVersion} is too old; version ${formatAntigravityVersion(MINIMUM_ANTIGRAVITY_VERSION)} or newer is required.`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let modelsResult: AntigravityProbeResult;
|
|
131
|
+
try {
|
|
132
|
+
modelsResult = await runProbe({
|
|
133
|
+
paths,
|
|
134
|
+
cwd: paths.consultationWorkspace,
|
|
135
|
+
args: ["models"],
|
|
136
|
+
signal: options.signal,
|
|
137
|
+
timeoutMs: 30_000,
|
|
138
|
+
maxStdoutBytes: MAX_DISCOVERY_OUTPUT_BYTES,
|
|
139
|
+
});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return unavailableFromError(error, paths, cliVersion);
|
|
142
|
+
}
|
|
143
|
+
const modelOutput = `${modelsResult.stdout}\n${modelsResult.stderr}`;
|
|
144
|
+
if (looksUnauthenticated(modelOutput)) return authenticationUnavailable(paths, cliVersion);
|
|
145
|
+
if (modelsResult.exitCode !== 0 || modelsResult.signal) {
|
|
146
|
+
return {
|
|
147
|
+
status: "unavailable",
|
|
148
|
+
reason: "discovery",
|
|
149
|
+
cliVersion,
|
|
150
|
+
warning:
|
|
151
|
+
"Antigravity model discovery failed. Check the isolated Antigravity sign-in, then reload PI.",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let catalogue: readonly CuratedModel[];
|
|
156
|
+
try {
|
|
157
|
+
catalogue = intersectCuratedModels(parseAvailableModels(modelsResult.stdout));
|
|
158
|
+
} catch {
|
|
159
|
+
return {
|
|
160
|
+
status: "unavailable",
|
|
161
|
+
reason: "discovery",
|
|
162
|
+
cliVersion,
|
|
163
|
+
warning: "Antigravity model discovery output exceeded its limit.",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (catalogue.length === 0) {
|
|
167
|
+
return {
|
|
168
|
+
status: "unavailable",
|
|
169
|
+
reason: "no-curated-model",
|
|
170
|
+
cliVersion,
|
|
171
|
+
warning: "No supported curated Antigravity model is available to this account.",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return { status: "available", cliVersion, catalogue };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function unavailableFromError(
|
|
178
|
+
error: unknown,
|
|
179
|
+
paths: IsolatedAntigravityPaths,
|
|
180
|
+
cliVersion?: string,
|
|
181
|
+
): UnavailableAntigravity {
|
|
182
|
+
if (error instanceof AntigravityProcessError && error.kind === "missing") {
|
|
183
|
+
return {
|
|
184
|
+
status: "unavailable",
|
|
185
|
+
reason: "missing",
|
|
186
|
+
warning: "Antigravity CLI `agy` was not found on PATH. Install it, then reload PI.",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
looksUnauthenticated(
|
|
191
|
+
error instanceof AntigravityProcessError
|
|
192
|
+
? `${error.message}\n${error.stderr ?? ""}`
|
|
193
|
+
: error instanceof Error
|
|
194
|
+
? error.message
|
|
195
|
+
: "",
|
|
196
|
+
)
|
|
197
|
+
) {
|
|
198
|
+
return authenticationUnavailable(paths, cliVersion);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
status: "unavailable",
|
|
202
|
+
reason: "discovery",
|
|
203
|
+
...(cliVersion ? { cliVersion } : {}),
|
|
204
|
+
warning: "Antigravity availability discovery failed. Check the installation, then reload PI.",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function authenticationUnavailable(
|
|
209
|
+
paths: IsolatedAntigravityPaths,
|
|
210
|
+
cliVersion?: string,
|
|
211
|
+
): UnavailableAntigravity {
|
|
212
|
+
return {
|
|
213
|
+
status: "unavailable",
|
|
214
|
+
reason: "authentication",
|
|
215
|
+
...(cliVersion ? { cliVersion } : {}),
|
|
216
|
+
warning: `Antigravity needs sign-in in its Isolated Antigravity Home. Run:\n${formatAntigravityLoginCommand(paths)}\nThen exit Antigravity and reload PI.`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function looksUnauthenticated(value: string): boolean {
|
|
221
|
+
const normalized = value.toLowerCase();
|
|
222
|
+
return [
|
|
223
|
+
"please sign in",
|
|
224
|
+
"not authenticated",
|
|
225
|
+
"authentication required",
|
|
226
|
+
"unauthorized",
|
|
227
|
+
"sign-in required",
|
|
228
|
+
"login required",
|
|
229
|
+
"log in",
|
|
230
|
+
].some((marker) => normalized.includes(marker));
|
|
231
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
chmod,
|
|
5
|
+
lstat,
|
|
6
|
+
mkdir,
|
|
7
|
+
open,
|
|
8
|
+
readFile,
|
|
9
|
+
readlink,
|
|
10
|
+
rename,
|
|
11
|
+
rm,
|
|
12
|
+
stat,
|
|
13
|
+
symlink,
|
|
14
|
+
} from "node:fs/promises";
|
|
15
|
+
import { basename, dirname, join } from "node:path";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
|
|
19
|
+
const execFileAsync = promisify(execFile);
|
|
20
|
+
const ANTIGRAVITY_BASE_DIR = join("supi", "consulting", "antigravity");
|
|
21
|
+
const SETTINGS_RELATIVE_PATH = join(".gemini", "antigravity-cli", "settings.json");
|
|
22
|
+
// macOS uses this conventional filename as the default keychain for an alternate HOME.
|
|
23
|
+
const KEYCHAIN_RELATIVE_PATH = join("Library", "Keychains", "login.keychain-db");
|
|
24
|
+
// A keychain with the special login name gets a user-password-managed password. Use a
|
|
25
|
+
// package-owned keychain with an empty password, then expose it through the conventional name.
|
|
26
|
+
const PRIVATE_KEYCHAIN_FILE_NAME = "antigravity.keychain-db";
|
|
27
|
+
const EMPTY_TEMPLATE_NAME = "empty-git-template";
|
|
28
|
+
const SECURITY_COMMAND = "/usr/bin/security";
|
|
29
|
+
|
|
30
|
+
/** The package-owned paths used by Antigravity. */
|
|
31
|
+
export interface IsolatedAntigravityPaths {
|
|
32
|
+
agentDir: string;
|
|
33
|
+
baseDir: string;
|
|
34
|
+
homeDir: string;
|
|
35
|
+
consultationWorkspace: string;
|
|
36
|
+
settingsPath: string;
|
|
37
|
+
gitTemplateDir: string;
|
|
38
|
+
/** The conventional macOS keychain path backed by the private isolated keychain. */
|
|
39
|
+
keychainPath: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Return package-owned Antigravity paths for one Pi agent directory. */
|
|
43
|
+
export function getIsolatedAntigravityPaths(
|
|
44
|
+
agentDir = resolveAgentDirectory(),
|
|
45
|
+
): IsolatedAntigravityPaths {
|
|
46
|
+
const baseDir = join(agentDir, ANTIGRAVITY_BASE_DIR);
|
|
47
|
+
const homeDir = join(baseDir, "home");
|
|
48
|
+
const consultationWorkspace = join(baseDir, "consultation-workspace");
|
|
49
|
+
return {
|
|
50
|
+
agentDir,
|
|
51
|
+
baseDir,
|
|
52
|
+
homeDir,
|
|
53
|
+
consultationWorkspace,
|
|
54
|
+
settingsPath: join(homeDir, SETTINGS_RELATIVE_PATH),
|
|
55
|
+
gitTemplateDir: join(baseDir, EMPTY_TEMPLATE_NAME),
|
|
56
|
+
keychainPath: join(homeDir, KEYCHAIN_RELATIVE_PATH),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Return the agent state directory used by the current Pi installation. */
|
|
61
|
+
export function resolveAgentDirectory(): string {
|
|
62
|
+
return process.env.PI_CODING_AGENT_DIR || getAgentDir();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The exact Inspection Permission Set enforced in the Isolated Antigravity Home. */
|
|
66
|
+
export const INSPECTION_PERMISSION_SET = Object.freeze({
|
|
67
|
+
allowNonWorkspaceAccess: false,
|
|
68
|
+
permissions: Object.freeze({
|
|
69
|
+
allow: Object.freeze(["read_url(*)"]),
|
|
70
|
+
deny: Object.freeze([
|
|
71
|
+
"write_file(*)",
|
|
72
|
+
"command(*)",
|
|
73
|
+
"unsandboxed(*)",
|
|
74
|
+
"mcp(*)",
|
|
75
|
+
"execute_url(*)",
|
|
76
|
+
]),
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const consultationInitializations = new Map<string, Promise<void>>();
|
|
81
|
+
const keychainInitializations = new Map<string, Promise<void>>();
|
|
82
|
+
|
|
83
|
+
/** Options for preparing an isolated profile. */
|
|
84
|
+
export interface IsolatedAntigravityPreparationOptions {
|
|
85
|
+
/** Override the host platform for tests. */
|
|
86
|
+
platform?: NodeJS.Platform;
|
|
87
|
+
/** Run one macOS Security command. */
|
|
88
|
+
runSecurityCommand?: (args: readonly string[], homeDir: string) => Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Create the stable empty Consultation Workspace without user Git templates. */
|
|
92
|
+
export async function initializeConsultationWorkspace(
|
|
93
|
+
paths: IsolatedAntigravityPaths,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
const existing = consultationInitializations.get(paths.consultationWorkspace);
|
|
96
|
+
if (existing !== undefined) {
|
|
97
|
+
await existing;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const initialization = initializeConsultationWorkspaceOnce(paths).finally(() => {
|
|
101
|
+
consultationInitializations.delete(paths.consultationWorkspace);
|
|
102
|
+
});
|
|
103
|
+
consultationInitializations.set(paths.consultationWorkspace, initialization);
|
|
104
|
+
await initialization;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function initializeConsultationWorkspaceOnce(paths: IsolatedAntigravityPaths): Promise<void> {
|
|
108
|
+
await mkdir(paths.baseDir, { recursive: true, mode: 0o700 });
|
|
109
|
+
await mkdir(paths.gitTemplateDir, { recursive: true, mode: 0o700 });
|
|
110
|
+
await mkdir(paths.consultationWorkspace, { recursive: true, mode: 0o700 });
|
|
111
|
+
await chmod(paths.baseDir, 0o700);
|
|
112
|
+
await chmod(paths.gitTemplateDir, 0o700);
|
|
113
|
+
await chmod(paths.consultationWorkspace, 0o700);
|
|
114
|
+
|
|
115
|
+
if (await isGitRepository(paths.consultationWorkspace)) return;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await execFileAsync(
|
|
119
|
+
"git",
|
|
120
|
+
["init", "--quiet", `--template=${paths.gitTemplateDir}`, paths.consultationWorkspace],
|
|
121
|
+
{
|
|
122
|
+
env: {
|
|
123
|
+
...process.env,
|
|
124
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
125
|
+
},
|
|
126
|
+
maxBuffer: 64 * 1024,
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new Error("Could not initialize the Antigravity Consultation Workspace.", {
|
|
131
|
+
cause: error,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function isGitRepository(directory: string): Promise<boolean> {
|
|
137
|
+
try {
|
|
138
|
+
await stat(join(directory, ".git"));
|
|
139
|
+
return true;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Prepare both package-owned directories and atomically enforce permissions. */
|
|
146
|
+
export async function prepareIsolatedAntigravityHome(
|
|
147
|
+
paths = getIsolatedAntigravityPaths(),
|
|
148
|
+
options: IsolatedAntigravityPreparationOptions = {},
|
|
149
|
+
): Promise<IsolatedAntigravityPaths> {
|
|
150
|
+
const keychainDirectory = dirname(paths.keychainPath);
|
|
151
|
+
const libraryDirectory = dirname(keychainDirectory);
|
|
152
|
+
const isMacOS = (options.platform ?? process.platform) === "darwin";
|
|
153
|
+
await mkdir(paths.homeDir, { recursive: true, mode: 0o700 });
|
|
154
|
+
await mkdir(dirname(paths.settingsPath), { recursive: true, mode: 0o700 });
|
|
155
|
+
if (isMacOS) await mkdir(keychainDirectory, { recursive: true, mode: 0o700 });
|
|
156
|
+
await chmod(paths.homeDir, 0o700);
|
|
157
|
+
await chmod(dirname(paths.settingsPath), 0o700);
|
|
158
|
+
if (isMacOS) {
|
|
159
|
+
await chmod(libraryDirectory, 0o700);
|
|
160
|
+
await chmod(keychainDirectory, 0o700);
|
|
161
|
+
}
|
|
162
|
+
await Promise.all([
|
|
163
|
+
initializeConsultationWorkspace(paths),
|
|
164
|
+
mergeInspectionSettings(paths.settingsPath),
|
|
165
|
+
prepareIsolatedMacOSKeychain(paths, options),
|
|
166
|
+
]);
|
|
167
|
+
return paths;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function prepareIsolatedMacOSKeychain(
|
|
171
|
+
paths: IsolatedAntigravityPaths,
|
|
172
|
+
options: IsolatedAntigravityPreparationOptions,
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
if ((options.platform ?? process.platform) !== "darwin") return;
|
|
175
|
+
const existing = keychainInitializations.get(paths.keychainPath);
|
|
176
|
+
if (existing !== undefined) {
|
|
177
|
+
await existing;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const runSecurityCommand = options.runSecurityCommand ?? runMacOSSecurityCommand;
|
|
181
|
+
const initialization = prepareIsolatedMacOSKeychainOnce(paths, runSecurityCommand).finally(() => {
|
|
182
|
+
keychainInitializations.delete(paths.keychainPath);
|
|
183
|
+
});
|
|
184
|
+
keychainInitializations.set(paths.keychainPath, initialization);
|
|
185
|
+
await initialization;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function prepareIsolatedMacOSKeychainOnce(
|
|
189
|
+
paths: IsolatedAntigravityPaths,
|
|
190
|
+
runSecurityCommand: (args: readonly string[], homeDir: string) => Promise<void>,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
const keychainDirectory = dirname(paths.keychainPath);
|
|
193
|
+
const privateKeychainPath = join(keychainDirectory, PRIVATE_KEYCHAIN_FILE_NAME);
|
|
194
|
+
await replaceLegacyKeychainEntry(paths.keychainPath, privateKeychainPath);
|
|
195
|
+
if (!(await pathExists(privateKeychainPath))) {
|
|
196
|
+
await runSecurityCommand(["create-keychain", "-p", "", privateKeychainPath], paths.homeDir);
|
|
197
|
+
}
|
|
198
|
+
if (!(await isSymlinkTo(paths.keychainPath, PRIVATE_KEYCHAIN_FILE_NAME))) {
|
|
199
|
+
await rm(paths.keychainPath, { force: true });
|
|
200
|
+
await symlink(PRIVATE_KEYCHAIN_FILE_NAME, paths.keychainPath);
|
|
201
|
+
}
|
|
202
|
+
// The private keychain has an empty password. Unlocking it with that known password is
|
|
203
|
+
// non-interactive and prevents a locked keychain from starting SecurityAgent.
|
|
204
|
+
await runSecurityCommand(["unlock-keychain", "-p", "", privateKeychainPath], paths.homeDir);
|
|
205
|
+
await chmod(privateKeychainPath, 0o600);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function replaceLegacyKeychainEntry(
|
|
209
|
+
keychainPath: string,
|
|
210
|
+
privateKeychainPath: string,
|
|
211
|
+
): Promise<void> {
|
|
212
|
+
const entry = await readFileEntry(keychainPath);
|
|
213
|
+
if (!entry) return;
|
|
214
|
+
if (entry.isSymbolicLink()) {
|
|
215
|
+
if (await isSymlinkTo(keychainPath, PRIVATE_KEYCHAIN_FILE_NAME)) return;
|
|
216
|
+
await rm(keychainPath, { force: true });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (keychainPath === privateKeychainPath) return;
|
|
220
|
+
await rm(keychainPath, { force: true });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function isSymlinkTo(filePath: string, target: string): Promise<boolean> {
|
|
224
|
+
const entry = await readFileEntry(filePath);
|
|
225
|
+
if (!entry?.isSymbolicLink()) return false;
|
|
226
|
+
try {
|
|
227
|
+
return (await readlink(filePath)) === target;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (isMissingFile(error)) return false;
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function readFileEntry(
|
|
235
|
+
filePath: string,
|
|
236
|
+
): Promise<Awaited<ReturnType<typeof lstat>> | undefined> {
|
|
237
|
+
try {
|
|
238
|
+
return await lstat(filePath);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (isMissingFile(error)) return undefined;
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function runMacOSSecurityCommand(args: readonly string[], homeDir: string): Promise<void> {
|
|
246
|
+
try {
|
|
247
|
+
await execFileAsync(SECURITY_COMMAND, [...args], {
|
|
248
|
+
env: { HOME: homeDir },
|
|
249
|
+
maxBuffer: 64 * 1024,
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
throw new Error("Could not prepare the isolated Antigravity macOS keychain.", {
|
|
253
|
+
cause: error,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function pathExists(filePath: string): Promise<boolean> {
|
|
259
|
+
try {
|
|
260
|
+
await stat(filePath);
|
|
261
|
+
return true;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (isMissingFile(error)) return false;
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Merge and atomically write the Inspection Permission Set. */
|
|
269
|
+
export async function mergeInspectionSettings(settingsPath: string): Promise<void> {
|
|
270
|
+
const existing = await readSettingsObject(settingsPath);
|
|
271
|
+
const existingPermissions = isRecord(existing.permissions) ? existing.permissions : {};
|
|
272
|
+
const merged = {
|
|
273
|
+
...existing,
|
|
274
|
+
allowNonWorkspaceAccess: INSPECTION_PERMISSION_SET.allowNonWorkspaceAccess,
|
|
275
|
+
permissions: {
|
|
276
|
+
...existingPermissions,
|
|
277
|
+
allow: [...INSPECTION_PERMISSION_SET.permissions.allow],
|
|
278
|
+
deny: [...INSPECTION_PERMISSION_SET.permissions.deny],
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
await writeJsonAtomically(settingsPath, merged);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function readSettingsObject(settingsPath: string): Promise<Record<string, unknown>> {
|
|
285
|
+
let content: string;
|
|
286
|
+
try {
|
|
287
|
+
content = await readFile(settingsPath, "utf8");
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (isMissingFile(error)) return {};
|
|
290
|
+
throw new Error("Could not read the isolated Antigravity settings.", { cause: error });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const parsed: unknown = JSON.parse(content);
|
|
295
|
+
if (!isRecord(parsed)) throw new Error("settings root is not an object");
|
|
296
|
+
return parsed;
|
|
297
|
+
} catch (error) {
|
|
298
|
+
throw new Error("The isolated Antigravity settings file is not valid JSON.", { cause: error });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Write JSON by replacing a same-directory temporary file. */
|
|
303
|
+
export async function writeJsonAtomically(
|
|
304
|
+
filePath: string,
|
|
305
|
+
value: Record<string, unknown>,
|
|
306
|
+
): Promise<void> {
|
|
307
|
+
const directory = dirname(filePath);
|
|
308
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
309
|
+
const temporaryPath = join(directory, `.${basename(filePath)}.${randomUUID()}.tmp`);
|
|
310
|
+
let handle: Awaited<ReturnType<typeof open>> | undefined;
|
|
311
|
+
try {
|
|
312
|
+
handle = await open(temporaryPath, "wx", 0o600);
|
|
313
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
314
|
+
await handle.sync();
|
|
315
|
+
await handle.close();
|
|
316
|
+
handle = undefined;
|
|
317
|
+
await chmod(temporaryPath, 0o600);
|
|
318
|
+
await rename(temporaryPath, filePath);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (handle) await handle.close().catch(() => undefined);
|
|
321
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
322
|
+
throw new Error("Could not atomically write the isolated Antigravity settings.", {
|
|
323
|
+
cause: error,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Build the resolved command a user can run to authenticate Antigravity. */
|
|
329
|
+
export function formatAntigravityLoginCommand(paths: IsolatedAntigravityPaths): string {
|
|
330
|
+
return [
|
|
331
|
+
`cd ${shellQuote(paths.consultationWorkspace)} &&`,
|
|
332
|
+
`HOME=${shellQuote(paths.homeDir)} AGY_CLI_DISABLE_AUTO_UPDATE=true agy`,
|
|
333
|
+
].join("\n");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function shellQuote(value: string): string {
|
|
337
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
341
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isMissingFile(error: unknown): boolean {
|
|
345
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
346
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Model choices that Antigravity can expose to a new Consultation. */
|
|
2
|
+
export const CURATED_MODELS = Object.freeze([
|
|
3
|
+
"gemini-3.8-flash-low",
|
|
4
|
+
"gemini-3.8-flash-medium",
|
|
5
|
+
"gemini-3.8-flash-high",
|
|
6
|
+
"gemini-3.1-pro-high",
|
|
7
|
+
] as const);
|
|
8
|
+
|
|
9
|
+
/** A model in Antigravity's discovered catalogue. */
|
|
10
|
+
export type CuratedModel = (typeof CURATED_MODELS)[number];
|
|
11
|
+
|
|
12
|
+
/** Check whether an ID belongs to Antigravity's curated catalogue. */
|
|
13
|
+
export function isCuratedModel(value: unknown): value is CuratedModel {
|
|
14
|
+
return typeof value === "string" && CURATED_MODELS.includes(value as CuratedModel);
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Environment variables that are safe and useful for a non-interactive agy process. */
|
|
2
|
+
export const APPROVED_ENVIRONMENT_KEYS = Object.freeze([
|
|
3
|
+
"PATH",
|
|
4
|
+
"TMPDIR",
|
|
5
|
+
"TMP",
|
|
6
|
+
"TEMP",
|
|
7
|
+
"LANG",
|
|
8
|
+
"LC_ALL",
|
|
9
|
+
"LC_CTYPE",
|
|
10
|
+
"LC_COLLATE",
|
|
11
|
+
"TERM",
|
|
12
|
+
"TERM_PROGRAM",
|
|
13
|
+
"COLORTERM",
|
|
14
|
+
"USER",
|
|
15
|
+
"LOGNAME",
|
|
16
|
+
"SHELL",
|
|
17
|
+
"NO_COLOR",
|
|
18
|
+
] as const);
|
|
19
|
+
|
|
20
|
+
/** Build the restricted environment used by every Antigravity process. */
|
|
21
|
+
export function buildAntigravityEnvironment(
|
|
22
|
+
homeDir: string,
|
|
23
|
+
source: NodeJS.ProcessEnv = process.env,
|
|
24
|
+
): NodeJS.ProcessEnv {
|
|
25
|
+
const environment: NodeJS.ProcessEnv = {};
|
|
26
|
+
for (const key of APPROVED_ENVIRONMENT_KEYS) {
|
|
27
|
+
const value = source[key];
|
|
28
|
+
if (value !== undefined) environment[key] = value;
|
|
29
|
+
}
|
|
30
|
+
environment.HOME = homeDir;
|
|
31
|
+
environment.AGY_CLI_DISABLE_AUTO_UPDATE = "true";
|
|
32
|
+
return environment;
|
|
33
|
+
}
|