mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
Q2: one-time notice naming the Codex provider and account for outside reviews
Outside reviews are on by default and use whatever Codex login exists (#965). The first automatic Codex review on a machine now prints one non-blocking notice: the provider (config.toml model_provider, else openai), where the credential comes from (CODEX_API_KEY, OPENAI_API_KEY, the ChatGPT login or the key saved by codex login; never the value), that reviews stay on, and how to turn them off (gstack-config set codex_reviews disabled). A marker in the gstack state root keeps it to once per machine. Tests that run the generated command now use private state roots. Also shortens the no-sandbox provenance line.
This commit is contained in:
1 parent
e2f9e144c0
commit
605f49d3d5
6 files changed
+87
-2
No files matched your search
@@ -7,6 +7,7 @@
|
||||
# _gstack_codex_select_model — resolve + print the runtime model (#2914)
|
||||
# _gstack_codex_sandbox_mode — read-only, or full access only for GSTACK_CODEX_NO_SANDBOX=1
|
||||
# _gstack_codex_sandbox_preflight — free `codex sandbox` check before paid calls (Linux)
|
||||
# _gstack_codex_first_use_notice — once per machine: provider, account, how to turn off
|
||||
# _gstack_codex_model_probe — round-trip probe of the selected model (#2477)
|
||||
# _gstack_codex_version_check — warn on known-bad Codex CLI versions
|
||||
# _gstack_codex_timeout_wrapper — gtimeout -> timeout -> bash-native watchdog
|
||||
@@ -126,6 +127,30 @@ _gstack_codex_sandbox_preflight() {
|
||||
return 3
|
||||
}
|
||||
|
||||
# --- First-use notice (#965) ------------------------------------------------
|
||||
|
||||
_gstack_codex_first_use_notice() {
|
||||
# Outside reviews are on by default and use whatever Codex login exists, so
|
||||
# the first one on a machine says where the code goes, with which account,
|
||||
# and how to turn reviews off. Shown once per state root; never blocks and
|
||||
# never prints a credential, only where it comes from.
|
||||
local _root _marker _codex_home="${CODEX_HOME:-$HOME/.codex}" _account _provider
|
||||
_root="$(. "${BASH_SOURCE[0]%/*}/gstack-state-root.sh" 2>/dev/null && gstack_state_root)" || return 0
|
||||
[ -n "$_root" ] || return 0
|
||||
_marker="$_root/.codex-review-notice-shown"
|
||||
[ -e "$_marker" ] && return 0
|
||||
if [ -n "$(printf '%s' "${CODEX_API_KEY:-}" | tr -d '[:space:]')" ]; then _account="the API key in CODEX_API_KEY"
|
||||
elif [ -n "$(printf '%s' "${OPENAI_API_KEY:-}" | tr -d '[:space:]')" ]; then _account="the API key in OPENAI_API_KEY"
|
||||
elif grep -q '"auth_mode"[[:space:]]*:[[:space:]]*"chatgpt"' "$_codex_home/auth.json" 2>/dev/null; then _account="the ChatGPT account Codex is logged in with (see: codex login status)"
|
||||
elif [ -f "$_codex_home/auth.json" ]; then _account="the API key saved by codex login in $_codex_home/auth.json"
|
||||
else _account="the credentials your Codex config.toml provider names"
|
||||
fi
|
||||
_provider=$(sed -n 's/^[[:space:]]*model_provider[[:space:]]*=[[:space:]]*"\([A-Za-z0-9_.-]*\)".*/\1/p' "$_codex_home/config.toml" 2>/dev/null | head -1)
|
||||
echo "NOTICE: gstack outside reviews send the review prompt and code to Codex (provider: ${_provider:-openai}) using $_account. Reviews stay on; this notice shows once per machine. To turn them off: gstack-config set codex_reviews disabled" >&2
|
||||
mkdir -p "$_root" 2>/dev/null && : > "$_marker" 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- Model round-trip probe (#2477) ------------------------------------------
|
||||
|
||||
_gstack_codex_model_probe() {
|
||||
|
||||
@@ -189,3 +189,20 @@ never an outage.
|
||||
```bash
|
||||
gstack-config set codex_reviews enabled
|
||||
```
|
||||
|
||||
<a id="codex-review-notice"></a>
|
||||
### `NOTICE: gstack outside reviews send the review prompt and code to Codex (...) using ...`
|
||||
|
||||
**Meaning.** Outside reviews are on by default. The first one on a machine
|
||||
says which provider receives your prompt and code and which login or key pays
|
||||
for it. It shows once (gstack records `.codex-review-notice-shown` in its state
|
||||
directory) and never blocks the review.
|
||||
|
||||
**Fix.** Nothing, if that is what you want. To stop sending code to Codex:
|
||||
|
||||
```bash
|
||||
gstack-config set codex_reviews disabled
|
||||
```
|
||||
|
||||
**Expected result.** Later reviews print `Codex review skipped (codex_reviews disabled)`
|
||||
or `CODEX_MODE: disabled`, and /ship and /review show the outside review as not run.
|
||||
@@ -133,6 +133,7 @@ export function outsideVoiceCommand(ctx: TemplateContext, opts: OutsideCommandOp
|
||||
const invocation = v.id === 'codex'
|
||||
? `source "${bin}/gstack-codex-probe" && _gstack_codex_select_model ${opts.structuredBase ? 'review' : 'exec'} || exit 1
|
||||
_gstack_codex_sandbox_preflight >/dev/null || exit 1
|
||||
_gstack_codex_first_use_notice
|
||||
_OUTSIDE_EXIT=0
|
||||
_gstack_codex_timeout_wrapper ${Math.ceil(opts.timeoutMs / 1000)} ${codex} >"$_OUTSIDE_TMP/${opts.structuredBase ? 'text' : 'events'}" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$?
|
||||
${opts.structuredBase ? 'cat "$_OUTSIDE_TMP/text"' : 'cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events"'}`
|
||||
@@ -242,7 +243,7 @@ export function outsideVoiceFailurePolicy(ctx: TemplateContext, opts: OutsideVoi
|
||||
|
||||
export function outsideVoiceProvenance(ctx: TemplateContext, phase: string): string {
|
||||
const v = outsideVoiceFor(ctx);
|
||||
return `Retain the historical review-log skill ID; add \`"host":"${ctx.host}","outside_provider":"${v.id}","outside_status":"completed|unavailable|disabled|skipped","phase":"${phase}"\`. Record differing attempt outcomes separately. \`source:"${v.id}"\` requires completed CLI output; native uses \`source:"in-host"\` (historical \`source:"claude"\`: native Claude). Availability/native fallback is not outside completion. Preserve all reported modelUsage; unknown model identity stays unknown.${v.id === 'codex' ? ' If `WARNING: GSTACK_CODEX_NO_SANDBOX=1` printed, add `"codex_sandbox":"danger-full-access"`.' : ''}`;
|
||||
return `Retain the historical review-log skill ID; add \`"host":"${ctx.host}","outside_provider":"${v.id}","outside_status":"completed|unavailable|disabled|skipped","phase":"${phase}"\`. Record differing attempt outcomes separately. \`source:"${v.id}"\` requires completed CLI output; native uses \`source:"in-host"\` (historical \`source:"claude"\`: native Claude). Availability/native fallback is not outside completion. Preserve all reported modelUsage; unknown model identity stays unknown.${v.id === 'codex' ? ' Under `GSTACK_CODEX_NO_SANDBOX=1` add `"codex_sandbox":"danger-full-access"`.' : ''}`;
|
||||
}
|
||||
|
||||
export function generateOutsideVoiceRouting(ctx: TemplateContext): string {
|
||||
|
||||
@@ -473,3 +473,29 @@ describe('B2: a 404 model rejection is unusable, never ready (#2843)', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Q2: one-time outside-review notice (#965)', () => {
|
||||
test('names the account source per auth mode, never the credential, and shows once per state root', () => {
|
||||
for (const [env, files, account] of [
|
||||
[{ CODEX_API_KEY: 'sk-codex-secret' }, {}, 'the API key in CODEX_API_KEY'],
|
||||
[{ OPENAI_API_KEY: 'sk-openai-secret' }, {}, 'the API key in OPENAI_API_KEY'],
|
||||
[{}, { 'auth.json': '{"auth_mode":"chatgpt","tokens":{"id_token":"secret-token"}}' }, 'the ChatGPT account Codex is logged in with'],
|
||||
[{}, { 'auth.json': '{"auth_mode":"apikey","OPENAI_API_KEY":"sk-file-secret"}' }, 'the API key saved by codex login'],
|
||||
[{}, { 'config.toml': 'model_provider = "mimo"\n[model_providers.mimo]\nenv_key = "MIMO_API_KEY"\n' }, 'Codex (provider: mimo) using the credentials your Codex config.toml provider names'],
|
||||
] as const) {
|
||||
const f = makeFixture();
|
||||
try {
|
||||
fs.rmSync(path.join(f.codexHome, 'auth.json'));
|
||||
fs.rmSync(path.join(f.codexHome, 'config.toml'));
|
||||
for (const [name, body] of Object.entries(files)) fs.writeFileSync(path.join(f.codexHome, name), body);
|
||||
const first = runProbe(f, 'ok', { ...env }, '_gstack_codex_first_use_notice; echo "rc=$?"');
|
||||
expect(first.stdout).toBe('rc=0\n');
|
||||
expect(first.stderr).toContain(account);
|
||||
expect(first.stderr).toContain('To turn them off: gstack-config set codex_reviews disabled');
|
||||
expect(first.stderr).not.toMatch(/secret/);
|
||||
expect(fs.existsSync(path.join(f.gstackHome, '.codex-review-notice-shown'))).toBe(true);
|
||||
expect(runProbe(f, 'ok', { ...env }, '_gstack_codex_first_use_notice').stderr).toBe('');
|
||||
} finally { fs.rmSync(f.home, { recursive: true, force: true }); }
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2198,7 +2198,8 @@ describe('DESIGN_OUTSIDE_VOICES resolver', () => {
|
||||
const prepare = (file: string) => command.replace("'<prepared-prompt-file>'", quote(file))
|
||||
.replaceAll('$HOME/.claude/skills/gstack', ROOT);
|
||||
const env = { ...process.env, PATH: dir + path.delimiter + process.env.PATH, CAPTURE: capture,
|
||||
CODEX_THREAD_ID: '', CODEX_SANDBOX: '', CLAUDECODE: '1', GSTACK_ACTIVE_HOST: 'claude' };
|
||||
CODEX_THREAD_ID: '', CODEX_SANDBOX: '', CLAUDECODE: '1', GSTACK_ACTIVE_HOST: 'claude',
|
||||
GSTACK_HOME: path.join(dir, 'state'), GSTACK_STATE_ROOT: '' };
|
||||
const result = spawnSync('bash', ['-c', prepare(brief)], { cwd: ROOT, env, encoding: 'utf8', timeout: 5_000 });
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
const { args, stdin } = JSON.parse(fs.readFileSync(capture, 'utf8'));
|
||||
|
||||
@@ -56,6 +56,7 @@ function environment(host: 'codex' | 'claude'): NodeJS.ProcessEnv {
|
||||
CAPTURE, FAKE_PROVIDER:host === 'codex' ? 'claude-code' : 'codex',
|
||||
CODEX_THREAD_ID:host === 'codex' ? 'codex-fixture' : '', CODEX_SANDBOX:'',
|
||||
CLAUDECODE:host === 'claude' ? '1' : '', GSTACK_ACTIVE_HOST:host, CODEX_HOME:TMP, GSTACK_CODEX_MODEL:'',
|
||||
GSTACK_HOME:path.join(TMP,'state'), GSTACK_STATE_ROOT:'', CODEX_API_KEY:'', OPENAI_API_KEY:'',
|
||||
PATH:`${BIN}${path.delimiter}${process.env.PATH}`,
|
||||
GIT_AUTHOR_NAME:'Test',GIT_AUTHOR_EMAIL:'test@example.invalid',GIT_COMMITTER_NAME:'Test',GIT_COMMITTER_EMAIL:'test@example.invalid'};
|
||||
}
|
||||
@@ -278,6 +279,19 @@ describe('generated outside-review dispatch', () => {
|
||||
expect(fs.existsSync(path.join(DIR, 'NEVER'))).toBe(false);
|
||||
});
|
||||
|
||||
test('Q2: the first automatic Codex review on a machine names the provider and account, once, without blocking', () => {
|
||||
const state = path.join(TMP, 'notice-state');
|
||||
const first = invoke('claude', {}, { GSTACK_HOME: state, OPENAI_API_KEY: 'sk-test-secret-value' });
|
||||
expect(first.status).toBe(0);
|
||||
expect(first.stderr).toContain('NOTICE: gstack outside reviews send the review prompt and code to Codex (provider: openai) using the API key in OPENAI_API_KEY.');
|
||||
expect(first.stderr).toContain('To turn them off: gstack-config set codex_reviews disabled');
|
||||
expect(first.stderr).not.toContain('sk-test-secret-value');
|
||||
expect(first.stdout).toContain('OUTSIDE_STATUS: completed');
|
||||
const second = invoke('claude', {}, { GSTACK_HOME: state, OPENAI_API_KEY: 'sk-test-secret-value' });
|
||||
expect(second.status).toBe(0);
|
||||
expect(second.stderr).not.toContain('NOTICE:');
|
||||
});
|
||||
|
||||
test('malformed Claude JSON cannot reach completion evaluation', () => {
|
||||
const result = invoke('codex',{}, {FAKE_MODE:'malformed'});
|
||||
expect(result.status).toBe(1);
|
||||
@@ -320,6 +334,7 @@ describe('generated outside-review dispatch', () => {
|
||||
const probe = path.join(BIN, 'gstack-codex-probe');
|
||||
fs.writeFileSync(probe, `_gstack_codex_select_model() { _GSTACK_CODEX_SEL=gpt-6-astra; _GSTACK_CODEX_SANDBOX=read-only; }
|
||||
_gstack_codex_sandbox_preflight() { return 0; }
|
||||
_gstack_codex_first_use_notice() { :; }
|
||||
_gstack_codex_timeout_wrapper() { echo 'Partial finding'; return 124; }
|
||||
_gstack_codex_log_event() { printf '%s %s\\n' "$1" "$2" >> "$FAKE_EVENTS"; }
|
||||
_gstack_codex_log_hang() { printf '%s %s\\n' "$1" "$2" >> "$FAKE_EVENTS"; }
|
||||
|
||||
Reference in new issue
Block a user