mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-18 13:37:23 +02:00
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 <cursoragent@cursor.com>
This commit is contained in:
+14
-11
@@ -134,12 +134,12 @@ lookup_default() {
|
|||||||
redact_repo_visibility) echo "" ;; # empty → fall through to gh/glab detection
|
redact_repo_visibility) echo "" ;; # empty → fall through to gh/glab detection
|
||||||
redact_prepush_hook) echo "false" ;;
|
redact_prepush_hook) echo "false" ;;
|
||||||
# Brain-aware planning (v1.48 / T5+T10+T16). Defaults documented inline:
|
# Brain-aware planning (v1.48 / T5+T10+T16). Defaults documented inline:
|
||||||
# brain_trust_policy@<hash> — unset on fresh install; setup-gbrain
|
# brain_trust_policy@<endpoint-id> — unset on fresh install; setup-gbrain
|
||||||
# writes 'personal' for local engines,
|
# writes 'personal' for local engines,
|
||||||
# asks the user for remote-ambiguous.
|
# asks the user for remote-ambiguous.
|
||||||
# salience_allowlist — empty falls through to
|
# salience_allowlist — empty falls through to
|
||||||
# SALIENCE_DEFAULT_ALLOWLIST (D9).
|
# SALIENCE_DEFAULT_ALLOWLIST (D9).
|
||||||
# user_slug_at_<hash> — empty triggers resolve-user-slug
|
# user_slug_at_<endpoint-id> — empty triggers resolve-user-slug
|
||||||
# fallback chain (D4 A3) on first call.
|
# fallback chain (D4 A3) on first call.
|
||||||
brain_trust_policy*) echo "unset" ;;
|
brain_trust_policy*) echo "unset" ;;
|
||||||
salience_allowlist) echo "" ;;
|
salience_allowlist) echo "" ;;
|
||||||
@@ -260,14 +260,16 @@ resolve_user_slug() {
|
|||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
get)
|
get)
|
||||||
KEY="${2:?Usage: gstack-config get <key>}"
|
KEY="${2:?Usage: gstack-config get <key>}"
|
||||||
# Validate key (alphanumeric + underscore + optional @<hash> suffix for
|
# Validate key (alphanumeric + underscore + optional @<endpoint-id> suffix for
|
||||||
# endpoint-namespaced keys introduced by the brain-aware planning layer)
|
# endpoint-namespaced keys introduced by the brain-aware planning layer).
|
||||||
if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+(@[a-f0-9]+)?$'; then
|
# Endpoint ids are sha8/sha16 hex for remote MCP URLs, or the literal
|
||||||
echo "Error: key must contain only alphanumeric characters, underscores, and an optional @<hex-hash> suffix" >&2
|
# "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 @<endpoint-id> suffix" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# Use literal match for keys containing @ (sha hashes), regex otherwise
|
# Use literal match for keys containing @ (endpoint ids), 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)
|
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
|
if [ -z "$VALUE" ]; then
|
||||||
VALUE=$(lookup_default "$KEY")
|
VALUE=$(lookup_default "$KEY")
|
||||||
fi
|
fi
|
||||||
@@ -276,9 +278,10 @@ case "${1:-}" in
|
|||||||
set)
|
set)
|
||||||
KEY="${2:?Usage: gstack-config set <key> <value>}"
|
KEY="${2:?Usage: gstack-config set <key> <value>}"
|
||||||
VALUE="${3:?Usage: gstack-config set <key> <value>}"
|
VALUE="${3:?Usage: gstack-config set <key> <value>}"
|
||||||
# Validate key (alphanumeric + underscore + optional @<hash> suffix)
|
# Validate key (alphanumeric + underscore + optional @<endpoint-id> suffix).
|
||||||
if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+(@[a-f0-9]+)?$'; then
|
# Accepts hex hashes and the literal "local" from endpoint_hash.
|
||||||
echo "Error: key must contain only alphanumeric characters, underscores, and an optional @<hex-hash> suffix" >&2
|
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 @<endpoint-id> suffix" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# Validate brain_trust_policy value domain (D4 / D11)
|
# Validate brain_trust_policy value domain (D4 / D11)
|
||||||
|
|||||||
@@ -108,6 +108,13 @@ describe('gstack-config', () => {
|
|||||||
expect(existsSync(join(nestedDir, 'config.yaml'))).toBe(true);
|
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 ─────────────────────────────────────────────────
|
// ─── list ─────────────────────────────────────────────────
|
||||||
test('list shows all keys', () => {
|
test('list shows all keys', () => {
|
||||||
writeFileSync(join(stateDir, 'config.yaml'), 'auto_upgrade: true\nupdate_check: false\n');
|
writeFileSync(join(stateDir, 'config.yaml'), 'auto_upgrade: true\nupdate_check: false\n');
|
||||||
@@ -139,6 +146,12 @@ describe('gstack-config', () => {
|
|||||||
expect(stderr).toContain('alphanumeric');
|
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', () => {
|
test('set preserves value with sed special chars', () => {
|
||||||
run(['set', 'test_special', 'a/b&c\\d']);
|
run(['set', 'test_special', 'a/b&c\\d']);
|
||||||
const { stdout } = run(['get', 'test_special']);
|
const { stdout } = run(['get', 'test_special']);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* 3. sha8($(git config user.email))
|
* 3. sha8($(git config user.email))
|
||||||
* 4. anonymous-<sha8(hostname)>
|
* 4. anonymous-<sha8(hostname)>
|
||||||
*
|
*
|
||||||
* Result is persisted under user_slug_at_<endpoint-hash> for stability.
|
* Result is persisted under user_slug_at_<endpoint-id> for stability.
|
||||||
* Test isolation via GSTACK_HOME and HOME env overrides.
|
* Test isolation via GSTACK_HOME and HOME env overrides.
|
||||||
*
|
*
|
||||||
* Gate-tier, free, ~50ms.
|
* 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-]+$/);
|
expect(slug).toMatch(/^(email-|anonymous-)[a-f0-9]+$|^[a-zA-Z0-9-]+$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('persists resolution to user_slug_at_<hash> on first call', () => {
|
test('persists resolution to user_slug_at_<endpoint-id> on first call', () => {
|
||||||
runConfig(['resolve-user-slug'], { GSTACK_HOME: TMP_HOME, USER: 'persisttest' });
|
runConfig(['resolve-user-slug'], { GSTACK_HOME: TMP_HOME, USER: 'persisttest' });
|
||||||
const configFile = join(TMP_HOME, 'config.yaml');
|
const configFile = join(TMP_HOME, 'config.yaml');
|
||||||
expect(existsSync(configFile)).toBe(true);
|
expect(existsSync(configFile)).toBe(true);
|
||||||
const content = readFileSync(configFile, 'utf-8');
|
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)', () => {
|
test('subsequent calls return same slug (stable across sessions)', () => {
|
||||||
@@ -104,7 +104,7 @@ describe('resolve-user-slug fallback chain', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('brain_trust_policy@<hash> namespace', () => {
|
describe('brain_trust_policy@<endpoint-id> namespace', () => {
|
||||||
test('default value is "unset"', () => {
|
test('default value is "unset"', () => {
|
||||||
const result = runConfig(['get', 'brain_trust_policy@deadbeef'], { GSTACK_HOME: TMP_HOME });
|
const result = runConfig(['get', 'brain_trust_policy@deadbeef'], { GSTACK_HOME: TMP_HOME });
|
||||||
expect(result.status).toBe(0);
|
expect(result.status).toBe(0);
|
||||||
@@ -158,4 +158,10 @@ describe('key validation', () => {
|
|||||||
const result = runConfig(['get', 'brain_trust_policy@abc123ff'], { GSTACK_HOME: TMP_HOME });
|
const result = runConfig(['get', 'brain_trust_policy@abc123ff'], { GSTACK_HOME: TMP_HOME });
|
||||||
expect(result.status).toBe(0);
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user