From db149a616234b5cb230f89ae24ab7eb14df84009 Mon Sep 17 00:00:00 2001 From: Sina Date: Fri, 10 Jul 2026 11:21:19 -0700 Subject: [PATCH] fix(gstack-config): accept @local endpoint ids for brain_trust_policy endpoint-hash returns "local" for stdio/PGLite, but validators only allowed hex suffixes, so setup-gbrain could not persist trust policy. Co-authored-by: Cursor --- bin/gstack-config | 25 ++++++++++++++----------- browse/test/gstack-config.test.ts | 13 +++++++++++++ test/user-slug-fallback.test.ts | 14 ++++++++++---- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/bin/gstack-config b/bin/gstack-config index d9834c447..15c7300c5 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -134,12 +134,12 @@ lookup_default() { redact_repo_visibility) echo "" ;; # empty → fall through to gh/glab detection redact_prepush_hook) echo "false" ;; # Brain-aware planning (v1.48 / T5+T10+T16). Defaults documented inline: - # brain_trust_policy@ — unset on fresh install; setup-gbrain + # brain_trust_policy@ — unset on fresh install; setup-gbrain # writes 'personal' for local engines, # asks the user for remote-ambiguous. # salience_allowlist — empty falls through to # SALIENCE_DEFAULT_ALLOWLIST (D9). - # user_slug_at_ — empty triggers resolve-user-slug + # user_slug_at_ — empty triggers resolve-user-slug # fallback chain (D4 A3) on first call. brain_trust_policy*) echo "unset" ;; salience_allowlist) echo "" ;; @@ -260,14 +260,16 @@ resolve_user_slug() { case "${1:-}" in get) KEY="${2:?Usage: gstack-config get }" - # Validate key (alphanumeric + underscore + optional @ suffix for - # endpoint-namespaced keys introduced by the brain-aware planning layer) - if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+(@[a-f0-9]+)?$'; then - echo "Error: key must contain only alphanumeric characters, underscores, and an optional @ suffix" >&2 + # Validate key (alphanumeric + underscore + optional @ suffix for + # endpoint-namespaced keys introduced by the brain-aware planning layer). + # Endpoint ids are sha8/sha16 hex for remote MCP URLs, or the literal + # "local" for stdio/PGLite engines (see endpoint_hash). + if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+(@[a-zA-Z0-9]+)?$'; then + echo "Error: key must contain only alphanumeric characters, underscores, and an optional @ suffix" >&2 exit 1 fi - # Use literal match for keys containing @ (sha hashes), regex otherwise - VALUE=$(grep -F "${KEY}:" "$CONFIG_FILE" 2>/dev/null | grep -E "^${KEY%@*}(@[a-f0-9]+)?:" | grep -F "${KEY}:" | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true) + # Use literal match for keys containing @ (endpoint ids), regex otherwise + VALUE=$(grep -F "${KEY}:" "$CONFIG_FILE" 2>/dev/null | grep -E "^${KEY%@*}(@[a-zA-Z0-9]+)?:" | grep -F "${KEY}:" | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true) if [ -z "$VALUE" ]; then VALUE=$(lookup_default "$KEY") fi @@ -276,9 +278,10 @@ case "${1:-}" in set) KEY="${2:?Usage: gstack-config set }" VALUE="${3:?Usage: gstack-config set }" - # Validate key (alphanumeric + underscore + optional @ suffix) - if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+(@[a-f0-9]+)?$'; then - echo "Error: key must contain only alphanumeric characters, underscores, and an optional @ suffix" >&2 + # Validate key (alphanumeric + underscore + optional @ suffix). + # Accepts hex hashes and the literal "local" from endpoint_hash. + if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+(@[a-zA-Z0-9]+)?$'; then + echo "Error: key must contain only alphanumeric characters, underscores, and an optional @ suffix" >&2 exit 1 fi # Validate brain_trust_policy value domain (D4 / D11) diff --git a/browse/test/gstack-config.test.ts b/browse/test/gstack-config.test.ts index e342a50b7..51adbe813 100644 --- a/browse/test/gstack-config.test.ts +++ b/browse/test/gstack-config.test.ts @@ -108,6 +108,13 @@ describe('gstack-config', () => { expect(existsSync(join(nestedDir, 'config.yaml'))).toBe(true); }); + test('brain trust policy accepts local endpoint suffix', () => { + const { exitCode, stderr } = run(['set', 'brain_trust_policy@local', 'personal']); + expect(exitCode).toBe(0); + expect(stderr).toBe(''); + expect(run(['get', 'brain_trust_policy@local']).stdout).toBe('personal'); + }); + // ─── list ───────────────────────────────────────────────── test('list shows all keys', () => { writeFileSync(join(stateDir, 'config.yaml'), 'auto_upgrade: true\nupdate_check: false\n'); @@ -139,6 +146,12 @@ describe('gstack-config', () => { expect(stderr).toContain('alphanumeric'); }); + test('set rejects endpoint suffix with punctuation', () => { + const { exitCode, stderr } = run(['set', 'brain_trust_policy@local-dev', 'personal']); + expect(exitCode).toBe(1); + expect(stderr).toContain('endpoint-id'); + }); + test('set preserves value with sed special chars', () => { run(['set', 'test_special', 'a/b&c\\d']); const { stdout } = run(['get', 'test_special']); diff --git a/test/user-slug-fallback.test.ts b/test/user-slug-fallback.test.ts index 1d8c3f925..0c05f560e 100644 --- a/test/user-slug-fallback.test.ts +++ b/test/user-slug-fallback.test.ts @@ -8,7 +8,7 @@ * 3. sha8($(git config user.email)) * 4. anonymous- * - * Result is persisted under user_slug_at_ for stability. + * Result is persisted under user_slug_at_ for stability. * Test isolation via GSTACK_HOME and HOME env overrides. * * Gate-tier, free, ~50ms. @@ -87,12 +87,12 @@ describe('resolve-user-slug fallback chain', () => { expect(slug).toMatch(/^(email-|anonymous-)[a-f0-9]+$|^[a-zA-Z0-9-]+$/); }); - test('persists resolution to user_slug_at_ on first call', () => { + test('persists resolution to user_slug_at_ on first call', () => { runConfig(['resolve-user-slug'], { GSTACK_HOME: TMP_HOME, USER: 'persisttest' }); const configFile = join(TMP_HOME, 'config.yaml'); expect(existsSync(configFile)).toBe(true); const content = readFileSync(configFile, 'utf-8'); - expect(content).toMatch(/^user_slug_at_[a-f0-9]+:\s+persisttest/m); + expect(content).toMatch(/^user_slug_at_(local|[a-f0-9]{8}|[a-f0-9]{16}):\s+persisttest/m); }); test('subsequent calls return same slug (stable across sessions)', () => { @@ -104,7 +104,7 @@ describe('resolve-user-slug fallback chain', () => { }); }); -describe('brain_trust_policy@ namespace', () => { +describe('brain_trust_policy@ namespace', () => { test('default value is "unset"', () => { const result = runConfig(['get', 'brain_trust_policy@deadbeef'], { GSTACK_HOME: TMP_HOME }); expect(result.status).toBe(0); @@ -158,4 +158,10 @@ describe('key validation', () => { const result = runConfig(['get', 'brain_trust_policy@abc123ff'], { GSTACK_HOME: TMP_HOME }); expect(result.status).toBe(0); }); + + test('accepts @local suffix on key', () => { + const result = runConfig(['get', 'brain_trust_policy@local'], { GSTACK_HOME: TMP_HOME }); + expect(result.status).toBe(0); + expect(result.stdout).toBe('unset'); + }); });