Skip to content

Commit 647fea8

Browse files
committed
fix: address Copilot review feedback
- writeEnvFile now quotes values containing whitespace/special chars and escapes embedded " and \. Fixes the round-trip corruption Copilot flagged: a pre-existing `FOO="a b"` would previously come back as `FOO=a b` after any unrelated config update because stripQuotes read but writeEnvFile didn't re-quote. - fetchSkills no longer attaches the admin token — /api/skills is unauthenticated and there's no reason to broaden the token's exposure surface (server logs, future proxies) until the endpoint actually guards. - POST /api/config wraps req.json() in try/catch and returns 400 for malformed JSON instead of leaking a 500 on a public endpoint. - Comment on the isSafeEnvValue guard now reflects the full denylist (not just newlines) so future readers don't narrow the check by mistake.
1 parent eff212d commit 647fea8

3 files changed

Lines changed: 26 additions & 7 deletions

File tree

agent-templates/next/app/(agent)/_components/settings-panel.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,9 +485,10 @@ export default function SettingsPanel({ config, onChange }: { config: AgentConfi
485485

486486
const fetchSkills = useCallback(async () => {
487487
try {
488-
// Skills endpoint isn't auth-guarded today, but sending the header
489-
// costs nothing and keeps us consistent if it ever becomes guarded.
490-
const res = await fetch("/api/skills", { headers: configHeaders(false) });
488+
// /api/skills is unauthenticated; keep the admin token out of its
489+
// request surface (server logs, future proxies) until the endpoint
490+
// actually requires it.
491+
const res = await fetch("/api/skills");
491492
if (res.ok) {
492493
const data: SkillInfo[] = await res.json();
493494
setSkills(data.filter((s) => s.category !== "Export"));

agent-templates/next/app/(agent)/_lib/config/keys.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,18 @@ function readEnvFile(): Record<string, string> {
164164
}
165165
}
166166

167+
// readEnvFile strips surrounding quotes, so we lose the knowledge that a
168+
// user's pre-existing value was originally quoted. Re-quote on write
169+
// whenever the value contains a character that dotenv would otherwise
170+
// mis-parse, escaping backslash and double-quote inside the quoted form.
171+
// Managed keys (filtered by isSafeEnvValue) never hit this branch; it
172+
// exists to preserve unrelated entries like FOO="a b" across round-trip.
167173
function writeEnvFile(vars: Record<string, string>) {
168-
const lines = Object.entries(vars).map(([k, v]) => `${k}=${v}`);
174+
const lines = Object.entries(vars).map(([k, v]) => {
175+
if (!/[\s"'#$\\]/.test(v)) return `${k}=${v}`;
176+
const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
177+
return `${k}="${escaped}"`;
178+
});
169179
fs.writeFileSync(ENV_FILE, lines.join("\n") + "\n", "utf-8");
170180
}
171181

agent-templates/next/app/(agent)/api/config/route.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ export async function POST(req: Request) {
2727
const denied = requireConfigWriteAuth(req);
2828
if (denied) return denied;
2929

30-
const { keys, values } = (await req.json()) as {
30+
let body: unknown;
31+
try {
32+
body = await req.json();
33+
} catch {
34+
return Response.json({ error: "Request body is not valid JSON" }, { status: 400 });
35+
}
36+
const { keys, values } = (body ?? {}) as {
3137
keys?: Record<string, string>;
3238
values?: Record<string, string>;
3339
};
@@ -36,8 +42,10 @@ export async function POST(req: Request) {
3642
return Response.json({ error: "Invalid request body" }, { status: 400 });
3743
}
3844

39-
// Reject payloads where any value would inject newlines into .env.local.
40-
// Empty string means "delete this key" and is legal.
45+
// Reject payloads whose values would break out of their .env.local line
46+
// or trip dotenv's quote/comment/variable-expansion parsing (see
47+
// isSafeEnvValue for the full denylist). Empty string means "delete
48+
// this key" and is legal.
4149
for (const value of Object.values(keys ?? {})) {
4250
if (value !== "" && !isSafeEnvValue(value)) {
4351
return Response.json({ error: "Invalid characters in key value" }, { status: 400 });

0 commit comments

Comments
 (0)