mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-20 03:42:24 +02:00
fix(config): make gstack-config key validation locale-independent
POSIX bracket ranges like a-z follow the active collation order; under GNU grep with tr_TR.UTF-8 the range excludes the ASCII letter i, so every key containing i (skill_prefix, explain_level, ...) was rejected as invalid. Pin both get/set validators to LC_ALL=C, with a source-level tripwire test since macOS BSD grep doesn't reproduce the bug. Closes #2494. Contributed by @Math1987 (PR #2506). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a2ae26d44c
commit
da6fd8bf25
+2
-2
@@ -264,7 +264,7 @@ case "${1:-}" in
|
|||||||
# endpoint-namespaced keys introduced by the brain-aware planning layer).
|
# endpoint-namespaced keys introduced by the brain-aware planning layer).
|
||||||
# Endpoint ids are sha8/sha16 hex for remote MCP URLs, or the literal
|
# Endpoint ids are sha8/sha16 hex for remote MCP URLs, or the literal
|
||||||
# "local" for stdio/PGLite engines (see endpoint_hash).
|
# "local" for stdio/PGLite engines (see endpoint_hash).
|
||||||
if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+(@[a-zA-Z0-9]+)?$'; then
|
if ! printf '%s' "$KEY" | LC_ALL=C 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
|
echo "Error: key must contain only alphanumeric characters, underscores, and an optional @<endpoint-id> suffix" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -280,7 +280,7 @@ case "${1:-}" in
|
|||||||
VALUE="${3:?Usage: gstack-config set <key> <value>}"
|
VALUE="${3:?Usage: gstack-config set <key> <value>}"
|
||||||
# Validate key (alphanumeric + underscore + optional @<endpoint-id> suffix).
|
# Validate key (alphanumeric + underscore + optional @<endpoint-id> suffix).
|
||||||
# Accepts hex hashes and the literal "local" from endpoint_hash.
|
# 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
|
if ! printf '%s' "$KEY" | LC_ALL=C 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
|
echo "Error: key must contain only alphanumeric characters, underscores, and an optional @<endpoint-id> suffix" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Locale-independent key validation tests for bin/gstack-config.
|
||||||
|
*
|
||||||
|
* POSIX bracket ranges such as a-z follow the active collation order. Under
|
||||||
|
* GNU grep with tr_TR.UTF-8, that excludes the ASCII letter i and silently
|
||||||
|
* breaks most stored preferences. macOS BSD grep does not reproduce the bug,
|
||||||
|
* so the source-level tripwire pins the C-locale boundary on every platform.
|
||||||
|
*/
|
||||||
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import { spawnSync } from "child_process";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as os from "os";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
const ROOT = path.resolve(import.meta.dir, "..");
|
||||||
|
const CONFIG = path.join(ROOT, "bin", "gstack-config");
|
||||||
|
|
||||||
|
let stateRoot: string;
|
||||||
|
|
||||||
|
function run(args: string[]) {
|
||||||
|
const result = spawnSync(CONFIG, args, {
|
||||||
|
encoding: "utf8",
|
||||||
|
env: { ...process.env, GSTACK_STATE_ROOT: stateRoot },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: result.status ?? -1,
|
||||||
|
stdout: result.stdout ?? "",
|
||||||
|
stderr: result.stderr ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
stateRoot = fs.mkdtempSync(path.join(os.tmpdir(), "gstack-config-locale-"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(stateRoot, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("gstack-config key validation is locale-independent", () => {
|
||||||
|
test("both get and set validate ASCII ranges under the C locale", () => {
|
||||||
|
const source = fs.readFileSync(CONFIG, "utf8");
|
||||||
|
const guardedValidators = source.match(
|
||||||
|
/LC_ALL=C grep -qE '\^\[a-zA-Z0-9_\]\+\(@\[a-zA-Z0-9\]\+\)\?\$'/g,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(guardedValidators).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("round-trips existing keys that contain i", () => {
|
||||||
|
expect(run(["set", "skill_prefix", "true"]).status).toBe(0);
|
||||||
|
|
||||||
|
const get = run(["get", "skill_prefix"]);
|
||||||
|
expect(get.status).toBe(0);
|
||||||
|
expect(get.stdout).toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts an existing endpoint-scoped ASCII key", () => {
|
||||||
|
expect(run(["set", "brain_trust_policy@local", "personal"]).status).toBe(0);
|
||||||
|
|
||||||
|
const get = run(["get", "brain_trust_policy@local"]);
|
||||||
|
expect(get.status).toBe(0);
|
||||||
|
expect(get.stdout).toBe("personal");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("continues to reject non-ASCII keys", () => {
|
||||||
|
const set = run(["set", "skïll_prefix", "true"]);
|
||||||
|
expect(set.status).toBe(1);
|
||||||
|
expect(set.stderr).toContain("key must contain only alphanumeric characters");
|
||||||
|
|
||||||
|
const get = run(["get", "skïll_prefix"]);
|
||||||
|
expect(get.status).toBe(1);
|
||||||
|
expect(get.stderr).toContain("key must contain only alphanumeric characters");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user