This commit is contained in:
Jonas B. 2026-07-10 10:45:05 -07:00 committed by GitHub
commit 785cbda0bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 31 additions and 12 deletions

View File

@ -260,14 +260,14 @@ resolve_user_slug() {
case "${1:-}" in
get)
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)
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 @<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
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 +276,9 @@ case "${1:-}" in
set)
KEY="${2:?Usage: gstack-config set <key> <value>}"
VALUE="${3:?Usage: gstack-config set <key> <value>}"
# Validate key (alphanumeric + underscore + optional @<hash> 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 @<hex-hash> suffix" >&2
# Validate key (alphanumeric + underscore + optional @<endpoint-id> suffix)
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
fi
# Validate brain_trust_policy value domain (D4 / D11)

View File

@ -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']);

View File

@ -8,7 +8,7 @@
* 3. sha8($(git config user.email))
* 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.
*
* 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_<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' });
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@<hash> namespace', () => {
describe('brain_trust_policy@<endpoint-id> 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');
});
});