Files
gstack/scripts/preflight-agent-sdk.ts
T
Garry Tan 33cb4715ef v1.39.2.0 feat: GSTACK_* env-shim for Conductor + gbrain/gstack setup docs (#1534)
* feat: GSTACK_* env-key shim for Conductor workspaces

New lib/conductor-env-shim.ts promotes GSTACK_ANTHROPIC_API_KEY and
GSTACK_OPENAI_API_KEY to canonical names when canonical is empty. Wired
into the four TS entry points that hit paid APIs or gbrain embeddings:
gstack-gbrain-sync.ts, gstack-model-benchmark, preflight-agent-sdk.ts,
test/helpers/e2e-helpers.ts. Side-effect-only import, 15 lines total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: gbrain+gstack setup, Conductor env mapping (v1.39.2.0)

USING_GBRAIN_WITH_GSTACK.md: new "What you get after setup" section,
Path 4 (remote MCP / split-engine), /sync-gbrain workflow stages +
watermark mechanics, "Conductor + GSTACK_* env vars" section, env vars
table extended, two troubleshooting entries (silent embedding failure
and FILE_TOO_LARGE watermark block).

CONTRIBUTING.md "Conductor workspaces": new paragraph on the GSTACK_*
prefix pattern and the four entry points importing the shim.

VERSION 1.39.1.0 → 1.39.2.0 and CHANGELOG entry covering the shim +
docs (full release-summary format with before/after table).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: unit coverage for conductor-env-shim

Refactor lib/conductor-env-shim.ts to export promoteConductorEnv()
so unit tests can manipulate env and call it directly (a bare side-
effect IIFE on import isn't reachable from bun:test once cached).
The on-import IIFE still runs — existing four-entry-point imports
keep working unchanged.

test/conductor-env-shim.test.ts covers all three branches:
GSTACK_FOO present + FOO empty → promotion; FOO already set →
no-overwrite; nothing in env → no-op.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: Conductor strips canonical API keys (not just "doesn't inherit")

The prior docs framed the GSTACK_* prefix as collision-avoidance:
"Conductor exposes API keys under a GSTACK_ prefix so it never
collides with whatever the host system has set." That understates
the mechanism — Conductor actively strips ANTHROPIC_API_KEY and
OPENAI_API_KEY from every workspace's process env, so setting them
in ~/.zshrc or .env doesn't help. The fix path is to set the
GSTACK_-prefixed forms in Conductor's workspace env config; Conductor
passes those through untouched.

Three docs updated to reflect the strip, not the polite framing:
USING_GBRAIN_WITH_GSTACK.md (Conductor section), CONTRIBUTING.md
(Conductor workspaces paragraph), CHANGELOG.md (release summary).

README.md gains a "Running gstack in Conductor?" callout in the
GBrain section pointing at the canonical doc's anchor, plus a fourth
path entry (remote gbrain MCP / split-engine) that was already
documented in USING_GBRAIN but missing from the README summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:32:33 -07:00

129 lines
4.5 KiB
TypeScript

/**
* Preflight for the overlay efficacy harness.
*
* Confirms, before any paid eval runs:
* 1. `@anthropic-ai/claude-agent-sdk` loads and `query()` is the expected shape.
* 2. `claude-opus-4-7` is a live API model ID (not a Claude Code alias).
* 3. The SDK event stream contains the types we assume (system init, assistant,
* result) with the fields we destructure.
* 4. `scripts/resolvers/model-overlay.ts` resolves `{{INHERIT:claude}}` against
* `opus-4-7.md` with no unresolved inheritance directives.
* 5. A local `claude` binary exists at `which claude` so binary pinning is possible.
*
* Run: bun run scripts/preflight-agent-sdk.ts
*
* Exit 0 on success. Exit non-zero with a clear message on any failure. No
* side effects beyond stdout and a ~15 token API call.
*/
import '../lib/conductor-env-shim';
import { query, type SDKMessage } from '@anthropic-ai/claude-agent-sdk';
import { readOverlay } from './resolvers/model-overlay';
import { resolveClaudeBinary } from '../browse/src/claude-bin';
async function main() {
const failures: string[] = [];
const pass = (msg: string) => console.log(` ok ${msg}`);
const fail = (msg: string) => {
console.log(` FAIL ${msg}`);
failures.push(msg);
};
// 1. Overlay resolver
console.log('1. Overlay resolver');
const resolved = readOverlay('opus-4-7');
if (!resolved) {
fail("readOverlay('opus-4-7') returned empty");
} else {
pass(`resolved overlay length: ${resolved.length} chars`);
if (resolved.includes('{{INHERIT:')) {
fail('resolved overlay still contains {{INHERIT:...}} directive');
} else {
pass('no unresolved INHERIT directives');
}
}
// 2. Local claude binary exists
console.log('\n2. Binary pinning');
let claudePath: string | null = resolveClaudeBinary();
if (claudePath) {
pass(`local claude binary: ${claudePath}`);
} else {
fail('`Bun.which("claude")` failed — cannot pin binary (set GSTACK_CLAUDE_BIN to override)');
}
// 3. SDK query end-to-end
console.log('\n3. SDK query end-to-end');
if (!process.env.ANTHROPIC_API_KEY) {
console.log(' skip ANTHROPIC_API_KEY not set — cannot test live query');
} else {
try {
const events: SDKMessage[] = [];
const q = query({
prompt: 'say pong',
options: {
model: 'claude-opus-4-7',
systemPrompt: '',
tools: [],
permissionMode: 'bypassPermissions',
allowDangerouslySkipPermissions: true,
settingSources: [],
maxTurns: 1,
pathToClaudeCodeExecutable: claudePath ?? undefined,
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
},
});
for await (const ev of q) events.push(ev);
pass(`received ${events.length} events`);
const init = events.find(
(e) => e.type === 'system' && (e as { subtype?: string }).subtype === 'init',
) as { claude_code_version?: string; model?: string } | undefined;
if (!init) {
fail('no system/init event received');
} else {
pass(`system init: claude_code_version=${init.claude_code_version}, model=${init.model}`);
}
const assistantEvents = events.filter((e) => e.type === 'assistant');
if (assistantEvents.length === 0) {
fail('no assistant events received — model ID may be rejected');
} else {
pass(`received ${assistantEvents.length} assistant event(s)`);
const first = assistantEvents[0] as { message?: { content?: unknown[] } };
const content = first.message?.content;
if (!Array.isArray(content)) {
fail('first assistant event has no content[] array');
} else {
pass(`first assistant content[] has ${content.length} block(s)`);
}
}
const result = events.find((e) => e.type === 'result') as
| { subtype?: string; total_cost_usd?: number; num_turns?: number }
| undefined;
if (!result) {
fail('no result event received');
} else {
pass(
`result: subtype=${result.subtype}, cost=$${result.total_cost_usd?.toFixed(4)}, turns=${result.num_turns}`,
);
}
} catch (err) {
fail(`SDK query threw: ${err instanceof Error ? err.message : String(err)}`);
}
}
console.log();
if (failures.length > 0) {
console.log(`PREFLIGHT FAILED: ${failures.length} check(s) failed`);
process.exit(1);
}
console.log('PREFLIGHT OK');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});