nyk commited on
Commit
d842037
·
unverified ·
1 Parent(s): cec279d

fix(sync): support commented/trailing-comma OpenClaw config (#235)

Browse files
src/app/api/gateway-config/route.ts CHANGED
@@ -4,6 +4,7 @@ import { logAuditEvent } from '@/lib/db'
4
  import { config } from '@/lib/config'
5
  import { validateBody, gatewayConfigUpdateSchema } from '@/lib/validation'
6
  import { mutationLimiter } from '@/lib/rate-limit'
 
7
 
8
  function getConfigPath(): string | null {
9
  return config.openclawConfigPath || null
@@ -24,7 +25,7 @@ export async function GET(request: NextRequest) {
24
  try {
25
  const { readFile } = require('fs/promises')
26
  const raw = await readFile(configPath, 'utf-8')
27
- const parsed = JSON.parse(raw)
28
 
29
  // Redact sensitive fields for display
30
  const redacted = redactSensitive(JSON.parse(JSON.stringify(parsed)))
@@ -76,7 +77,7 @@ export async function PUT(request: NextRequest) {
76
  try {
77
  const { readFile, writeFile } = require('fs/promises')
78
  const raw = await readFile(configPath, 'utf-8')
79
- const parsed = JSON.parse(raw)
80
 
81
  // Apply updates via dot-notation
82
  const appliedKeys: string[] = []
 
4
  import { config } from '@/lib/config'
5
  import { validateBody, gatewayConfigUpdateSchema } from '@/lib/validation'
6
  import { mutationLimiter } from '@/lib/rate-limit'
7
+ import { parseJsonRelaxed } from '@/lib/json-relaxed'
8
 
9
  function getConfigPath(): string | null {
10
  return config.openclawConfigPath || null
 
25
  try {
26
  const { readFile } = require('fs/promises')
27
  const raw = await readFile(configPath, 'utf-8')
28
+ const parsed = parseJsonRelaxed<any>(raw)
29
 
30
  // Redact sensitive fields for display
31
  const redacted = redactSensitive(JSON.parse(JSON.stringify(parsed)))
 
77
  try {
78
  const { readFile, writeFile } = require('fs/promises')
79
  const raw = await readFile(configPath, 'utf-8')
80
+ const parsed = parseJsonRelaxed<any>(raw)
81
 
82
  // Apply updates via dot-notation
83
  const appliedKeys: string[] = []
src/lib/__tests__/json-relaxed.test.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it } from 'vitest'
2
+ import { parseJsonRelaxed } from '@/lib/json-relaxed'
3
+
4
+ describe('parseJsonRelaxed', () => {
5
+ it('parses strict JSON unchanged', () => {
6
+ const parsed = parseJsonRelaxed<{ a: number; b: string }>('{"a":1,"b":"ok"}')
7
+ expect(parsed).toEqual({ a: 1, b: 'ok' })
8
+ })
9
+
10
+ it('parses JSON with line comments and trailing commas', () => {
11
+ const raw = `{
12
+ // top-level comment
13
+ "agents": {
14
+ "list": [
15
+ { "id": "a", "name": "A", },
16
+ ],
17
+ },
18
+ }`
19
+
20
+ const parsed = parseJsonRelaxed<any>(raw)
21
+ expect(parsed.agents.list[0].id).toBe('a')
22
+ expect(parsed.agents.list[0].name).toBe('A')
23
+ })
24
+
25
+ it('parses JSON with block comments', () => {
26
+ const raw = `{
27
+ /* comment */
28
+ "gateway": { "port": 18789 }
29
+ }`
30
+
31
+ const parsed = parseJsonRelaxed<any>(raw)
32
+ expect(parsed.gateway.port).toBe(18789)
33
+ })
34
+
35
+ it('does not strip URL fragments inside strings', () => {
36
+ const raw = `{
37
+ "url": "https://example.com/a//b",
38
+ "ok": true,
39
+ }`
40
+
41
+ const parsed = parseJsonRelaxed<any>(raw)
42
+ expect(parsed.url).toBe('https://example.com/a//b')
43
+ expect(parsed.ok).toBe(true)
44
+ })
45
+
46
+ it('throws on invalid JSON after normalization', () => {
47
+ expect(() => parseJsonRelaxed<any>('{ broken: true }')).toThrow()
48
+ })
49
+ })
src/lib/agent-sync.ts CHANGED
@@ -12,6 +12,7 @@ import { join, isAbsolute, resolve } from 'path'
12
  import { existsSync, readFileSync } from 'fs'
13
  import { resolveWithin } from './paths'
14
  import { logger } from './logger'
 
15
 
16
  interface OpenClawAgent {
17
  id: string
@@ -184,7 +185,7 @@ async function readOpenClawAgents(): Promise<OpenClawAgent[]> {
184
 
185
  const { readFile } = require('fs/promises')
186
  const raw = await readFile(configPath, 'utf-8')
187
- const parsed = JSON.parse(raw)
188
  return parsed?.agents?.list || []
189
  }
190
 
@@ -345,7 +346,7 @@ export async function writeAgentToConfig(agentConfig: any): Promise<void> {
345
 
346
  const { readFile, writeFile } = require('fs/promises')
347
  const raw = await readFile(configPath, 'utf-8')
348
- const parsed = JSON.parse(raw)
349
 
350
  if (!parsed.agents) parsed.agents = {}
351
  if (!parsed.agents.list) parsed.agents.list = []
 
12
  import { existsSync, readFileSync } from 'fs'
13
  import { resolveWithin } from './paths'
14
  import { logger } from './logger'
15
+ import { parseJsonRelaxed } from './json-relaxed'
16
 
17
  interface OpenClawAgent {
18
  id: string
 
185
 
186
  const { readFile } = require('fs/promises')
187
  const raw = await readFile(configPath, 'utf-8')
188
+ const parsed = parseJsonRelaxed<any>(raw)
189
  return parsed?.agents?.list || []
190
  }
191
 
 
346
 
347
  const { readFile, writeFile } = require('fs/promises')
348
  const raw = await readFile(configPath, 'utf-8')
349
+ const parsed = parseJsonRelaxed<any>(raw)
350
 
351
  if (!parsed.agents) parsed.agents = {}
352
  if (!parsed.agents.list) parsed.agents.list = []
src/lib/json-relaxed.ts ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Parse JSON with tolerant fallback for JSONC-style inputs.
3
+ * Supports comments and trailing commas, then validates with JSON.parse.
4
+ */
5
+ export function parseJsonRelaxed<T>(raw: string): T {
6
+ try {
7
+ return JSON.parse(raw) as T
8
+ } catch {
9
+ const stripped = stripJsonComments(raw)
10
+ const normalized = removeTrailingCommas(stripped)
11
+ return JSON.parse(normalized) as T
12
+ }
13
+ }
14
+
15
+ function stripJsonComments(input: string): string {
16
+ let output = ''
17
+ let inString = false
18
+ let stringDelimiter = '"'
19
+ let inLineComment = false
20
+ let inBlockComment = false
21
+
22
+ for (let i = 0; i < input.length; i++) {
23
+ const current = input[i]
24
+ const next = i + 1 < input.length ? input[i + 1] : ''
25
+ const prev = i > 0 ? input[i - 1] : ''
26
+
27
+ if (inLineComment) {
28
+ if (current === '\n') {
29
+ inLineComment = false
30
+ output += current
31
+ }
32
+ continue
33
+ }
34
+
35
+ if (inBlockComment) {
36
+ if (current === '*' && next === '/') {
37
+ inBlockComment = false
38
+ i += 1
39
+ }
40
+ continue
41
+ }
42
+
43
+ if (inString) {
44
+ output += current
45
+ if (current === stringDelimiter && prev !== '\\') {
46
+ inString = false
47
+ }
48
+ continue
49
+ }
50
+
51
+ if ((current === '"' || current === "'") && prev !== '\\') {
52
+ inString = true
53
+ stringDelimiter = current
54
+ output += current
55
+ continue
56
+ }
57
+
58
+ if (current === '/' && next === '/') {
59
+ inLineComment = true
60
+ i += 1
61
+ continue
62
+ }
63
+
64
+ if (current === '/' && next === '*') {
65
+ inBlockComment = true
66
+ i += 1
67
+ continue
68
+ }
69
+
70
+ output += current
71
+ }
72
+
73
+ return output
74
+ }
75
+
76
+ function removeTrailingCommas(input: string): string {
77
+ let output = ''
78
+ let inString = false
79
+ let stringDelimiter = '"'
80
+
81
+ for (let i = 0; i < input.length; i++) {
82
+ const current = input[i]
83
+ const prev = i > 0 ? input[i - 1] : ''
84
+
85
+ if (inString) {
86
+ output += current
87
+ if (current === stringDelimiter && prev !== '\\') {
88
+ inString = false
89
+ }
90
+ continue
91
+ }
92
+
93
+ if ((current === '"' || current === "'") && prev !== '\\') {
94
+ inString = true
95
+ stringDelimiter = current
96
+ output += current
97
+ continue
98
+ }
99
+
100
+ if (current === ',') {
101
+ let j = i + 1
102
+ while (j < input.length && /\s/.test(input[j])) j += 1
103
+ if (j < input.length && (input[j] === '}' || input[j] === ']')) {
104
+ continue
105
+ }
106
+ }
107
+
108
+ output += current
109
+ }
110
+
111
+ return output
112
+ }