clawcontainer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CONTRIBUTING.md +76 -0
  2. package/DOCS.md +370 -0
  3. package/LICENSE +21 -0
  4. package/README.md +147 -0
  5. package/black_logo.png +0 -0
  6. package/dist/assets/abap-DLDM7-KI.js +1 -0
  7. package/dist/assets/apex-DNDY2TF8.js +1 -0
  8. package/dist/assets/azcli-Y6nb8tq_.js +1 -0
  9. package/dist/assets/bat-BwHxbl9M.js +1 -0
  10. package/dist/assets/bicep-CFznDFnq.js +2 -0
  11. package/dist/assets/cameligo-Bf6VGUru.js +1 -0
  12. package/dist/assets/clojure-Dnu-v4kV.js +1 -0
  13. package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  14. package/dist/assets/coffee-Bd8akH9Z.js +1 -0
  15. package/dist/assets/cpp-BbWJElDN.js +1 -0
  16. package/dist/assets/csharp-Co3qMtFm.js +1 -0
  17. package/dist/assets/csp-D-4FJmMZ.js +1 -0
  18. package/dist/assets/css-DdJfP1eB.js +3 -0
  19. package/dist/assets/css.worker-GxEd3MMM.js +93 -0
  20. package/dist/assets/cssMode-DM_ONlf-.js +1 -0
  21. package/dist/assets/cypher-cTPe9QuQ.js +1 -0
  22. package/dist/assets/dart-BOtBlQCF.js +1 -0
  23. package/dist/assets/dockerfile-BG73LgW2.js +1 -0
  24. package/dist/assets/ecl-BEgZUVRK.js +1 -0
  25. package/dist/assets/elixir-BkW5O-1t.js +1 -0
  26. package/dist/assets/flow9-BeJ5waoc.js +1 -0
  27. package/dist/assets/freemarker2-VbwzOQPq.js +3 -0
  28. package/dist/assets/fsharp-PahG7c26.js +1 -0
  29. package/dist/assets/go-acbASCJo.js +1 -0
  30. package/dist/assets/graphql-BxJiqAUM.js +1 -0
  31. package/dist/assets/handlebars-DLvQ802u.js +1 -0
  32. package/dist/assets/hcl-DtV1sZF8.js +1 -0
  33. package/dist/assets/html-DuEPBzmS.js +1 -0
  34. package/dist/assets/html.worker-lU17Tx2m.js +470 -0
  35. package/dist/assets/htmlMode-BfeYTJaB.js +1 -0
  36. package/dist/assets/index-BnBKg8GZ.js +1291 -0
  37. package/dist/assets/index-Dq3FlPWe.css +32 -0
  38. package/dist/assets/ini-Kd9XrMLS.js +1 -0
  39. package/dist/assets/java-CXBNlu9o.js +1 -0
  40. package/dist/assets/javascript-DQO1Leza.js +1 -0
  41. package/dist/assets/json.worker-CUJs-dtA.js +58 -0
  42. package/dist/assets/jsonMode--qsURhHr.js +7 -0
  43. package/dist/assets/julia-cl7-CwDS.js +1 -0
  44. package/dist/assets/kotlin-s7OhZKlX.js +1 -0
  45. package/dist/assets/less-9HpZscsL.js +2 -0
  46. package/dist/assets/lexon-OrD6JF1K.js +1 -0
  47. package/dist/assets/liquid-PL6MZtM8.js +1 -0
  48. package/dist/assets/lspLanguageFeatures-Cy5rDFeq.js +4 -0
  49. package/dist/assets/lua-Cyyb5UIc.js +1 -0
  50. package/dist/assets/m3-B8OfTtLu.js +1 -0
  51. package/dist/assets/markdown-BFxVWTOG.js +1 -0
  52. package/dist/assets/mdx-Cb3Jy14X.js +1 -0
  53. package/dist/assets/mips-CiqrrVzr.js +1 -0
  54. package/dist/assets/msdax-DmeGPVcC.js +1 -0
  55. package/dist/assets/mysql-C_tMU-Nz.js +1 -0
  56. package/dist/assets/objective-c-BDtDVThU.js +1 -0
  57. package/dist/assets/pascal-vHIfCaH5.js +1 -0
  58. package/dist/assets/pascaligo-DtZ0uQbO.js +1 -0
  59. package/dist/assets/perl-Ub6l9XKa.js +1 -0
  60. package/dist/assets/pgsql-BlNEE0v7.js +1 -0
  61. package/dist/assets/php-BBUBE1dy.js +1 -0
  62. package/dist/assets/pla-DSh2-awV.js +1 -0
  63. package/dist/assets/postiats-CocnycG-.js +1 -0
  64. package/dist/assets/powerquery-tScXyioY.js +1 -0
  65. package/dist/assets/powershell-COWaemsV.js +1 -0
  66. package/dist/assets/protobuf-Brw8urJB.js +2 -0
  67. package/dist/assets/pug-8SOpv6rk.js +1 -0
  68. package/dist/assets/python-Usm4OUwq.js +1 -0
  69. package/dist/assets/qsharp-Bw9ernYp.js +1 -0
  70. package/dist/assets/r-j7ic8hl3.js +1 -0
  71. package/dist/assets/razor-BIOole7a.js +1 -0
  72. package/dist/assets/redis-Bu5POkcn.js +1 -0
  73. package/dist/assets/redshift-Bs9aos_-.js +1 -0
  74. package/dist/assets/restructuredtext-CqXO7rUv.js +1 -0
  75. package/dist/assets/ruby-zBfavPgS.js +1 -0
  76. package/dist/assets/rust-BzKRNQWT.js +1 -0
  77. package/dist/assets/sb-BBc9UKZt.js +1 -0
  78. package/dist/assets/scala-D9hQfWCl.js +1 -0
  79. package/dist/assets/scheme-BPhDTwHR.js +1 -0
  80. package/dist/assets/scss-CBJaRo0y.js +3 -0
  81. package/dist/assets/shell-DiJ1NA_G.js +1 -0
  82. package/dist/assets/solidity-Db0IVjzk.js +1 -0
  83. package/dist/assets/sophia-CnS9iZB_.js +1 -0
  84. package/dist/assets/sparql-CJmd_6j2.js +1 -0
  85. package/dist/assets/sql-ClhHkBeG.js +1 -0
  86. package/dist/assets/st-CHwy0fLd.js +1 -0
  87. package/dist/assets/swift-Bqt4WxQ4.js +3 -0
  88. package/dist/assets/systemverilog-Bs9z6M-B.js +1 -0
  89. package/dist/assets/tcl-Dm6ycUr_.js +1 -0
  90. package/dist/assets/ts.worker-Dy9lDQQT.js +67731 -0
  91. package/dist/assets/tsMode-CDjF3DWK.js +11 -0
  92. package/dist/assets/twig-Csy3S7wG.js +1 -0
  93. package/dist/assets/typescript-CJR4sLnG.js +1 -0
  94. package/dist/assets/typespec-Btyra-wh.js +1 -0
  95. package/dist/assets/vb-Db0cS2oM.js +1 -0
  96. package/dist/assets/wgsl-DumH7NcR.js +298 -0
  97. package/dist/assets/xml-CJZS3uh7.js +1 -0
  98. package/dist/assets/yaml-DB88cW5z.js +1 -0
  99. package/dist/audit.d.ts +48 -0
  100. package/dist/container.d.ts +100 -0
  101. package/dist/event-emitter.d.ts +7 -0
  102. package/dist/favicon.png +0 -0
  103. package/dist/git-service.d.ts +31 -0
  104. package/dist/index.html +188 -0
  105. package/dist/logo-sm.png +0 -0
  106. package/dist/logo.png +0 -0
  107. package/dist/main.d.ts +1 -0
  108. package/dist/monaco-editor.d.ts +11 -0
  109. package/dist/monacoeditorwork/css.worker.bundle.js +54264 -0
  110. package/dist/monacoeditorwork/editor.worker.bundle.js +14317 -0
  111. package/dist/monacoeditorwork/html.worker.bundle.js +30449 -0
  112. package/dist/monacoeditorwork/json.worker.bundle.js +22085 -0
  113. package/dist/monacoeditorwork/ts.worker.bundle.js +225552 -0
  114. package/dist/net-intercept.d.ts +2 -0
  115. package/dist/network-hook.d.ts +1 -0
  116. package/dist/plugin.d.ts +20 -0
  117. package/dist/policy.d.ts +58 -0
  118. package/dist/sdk.d.ts +61 -0
  119. package/dist/tab-manager.d.ts +11 -0
  120. package/dist/templates.d.ts +46 -0
  121. package/dist/terminal.d.ts +19 -0
  122. package/dist/types.d.ts +109 -0
  123. package/dist/ui.d.ts +81 -0
  124. package/dist/workspace.d.ts +16 -0
  125. package/index.html +159 -0
  126. package/logo.png +0 -0
  127. package/package.json +31 -0
  128. package/public/favicon.png +0 -0
  129. package/public/logo-sm.png +0 -0
  130. package/public/logo.png +0 -0
  131. package/src/audit.ts +196 -0
  132. package/src/container.ts +723 -0
  133. package/src/event-emitter.ts +28 -0
  134. package/src/git-service.ts +202 -0
  135. package/src/main.ts +9 -0
  136. package/src/monaco-editor.ts +111 -0
  137. package/src/net-intercept.ts +74 -0
  138. package/src/network-hook.ts +248 -0
  139. package/src/plugin.ts +63 -0
  140. package/src/policy.ts +403 -0
  141. package/src/sdk.ts +355 -0
  142. package/src/style.css +432 -0
  143. package/src/tab-manager.ts +30 -0
  144. package/src/templates.ts +271 -0
  145. package/src/terminal.ts +78 -0
  146. package/src/types.ts +113 -0
  147. package/src/ui.ts +1266 -0
  148. package/src/workspace.ts +107 -0
  149. package/tsconfig.json +20 -0
  150. package/vite.config.ts +52 -0
package/src/plugin.ts ADDED
@@ -0,0 +1,63 @@
1
+ // ─── Plugin Manager ─────────────────────────────────────────────────────────
2
+
3
+ import type { ClawContainerPlugin, ClawContainerSDK, TabDefinition } from './types.js';
4
+
5
+ export class PluginManager {
6
+ private plugins: ClawContainerPlugin[] = [];
7
+
8
+ /** Register a plugin, storing it for lifecycle dispatch. */
9
+ register(plugin: ClawContainerPlugin): void {
10
+ this.plugins.push(plugin);
11
+ }
12
+
13
+ /** Dispatch onInit to all registered plugins in order. */
14
+ dispatchInit(cc: ClawContainerSDK): void {
15
+ for (const p of this.plugins) p.onInit?.(cc);
16
+ }
17
+
18
+ /** Dispatch onReady to all registered plugins in order. */
19
+ dispatchReady(cc: ClawContainerSDK): void {
20
+ for (const p of this.plugins) p.onReady?.(cc);
21
+ }
22
+
23
+ /** Dispatch onDestroy to all registered plugins in order. */
24
+ dispatchDestroy(cc: ClawContainerSDK): void {
25
+ for (const p of this.plugins) p.onDestroy?.(cc);
26
+ }
27
+
28
+ /** Collect merged extra npm dependencies from all plugins. */
29
+ get mergedServices(): Record<string, string> {
30
+ const merged: Record<string, string> = {};
31
+ for (const p of this.plugins) {
32
+ if (p.services) Object.assign(merged, p.services);
33
+ }
34
+ return merged;
35
+ }
36
+
37
+ /** Collect merged extra workspace files from all plugins. */
38
+ get mergedWorkspace(): Record<string, string> {
39
+ const merged: Record<string, string> = {};
40
+ for (const p of this.plugins) {
41
+ if (p.workspace) Object.assign(merged, p.workspace);
42
+ }
43
+ return merged;
44
+ }
45
+
46
+ /** Collect merged extra env vars from all plugins. */
47
+ get mergedEnv(): Record<string, string> {
48
+ const merged: Record<string, string> = {};
49
+ for (const p of this.plugins) {
50
+ if (p.env) Object.assign(merged, p.env);
51
+ }
52
+ return merged;
53
+ }
54
+
55
+ /** Collect all tab definitions from plugins. */
56
+ get mergedTabs(): TabDefinition[] {
57
+ const tabs: TabDefinition[] = [];
58
+ for (const p of this.plugins) {
59
+ if (p.tabs) tabs.push(...p.tabs);
60
+ }
61
+ return tabs;
62
+ }
63
+ }
package/src/policy.ts ADDED
@@ -0,0 +1,403 @@
1
+ // ─── Generic WebContainer Policy / Guardrail Engine ─────────────────────────
2
+
3
+ export type PolicyAction =
4
+ | 'file.read'
5
+ | 'file.write'
6
+ | 'process.spawn'
7
+ | 'server.bind'
8
+ | 'env.configure'
9
+ | 'tool.use'
10
+ | 'git.clone'
11
+ | 'git.push';
12
+
13
+ export interface FileRule {
14
+ pattern: string;
15
+ allow: boolean;
16
+ }
17
+
18
+ export interface ProcessRule {
19
+ pattern: string;
20
+ allow: boolean;
21
+ }
22
+
23
+ export interface PortRule {
24
+ port: number | '*';
25
+ allow: boolean;
26
+ }
27
+
28
+ export interface ToolRule {
29
+ name: string;
30
+ allow: boolean;
31
+ }
32
+
33
+ export interface RuntimeLimits {
34
+ maxFileSize: number;
35
+ maxProcesses: number;
36
+ maxTurns: number;
37
+ timeoutSec: number;
38
+ }
39
+
40
+ export interface Policy {
41
+ version: '1';
42
+ mode: 'allow-all' | 'deny-all';
43
+ files: { read: FileRule[]; write: FileRule[] };
44
+ processes: ProcessRule[];
45
+ ports: PortRule[];
46
+ tools: ToolRule[];
47
+ limits: RuntimeLimits;
48
+ }
49
+
50
+ export interface CheckResult {
51
+ allowed: boolean;
52
+ rule?: string;
53
+ action: PolicyAction;
54
+ subject: string;
55
+ }
56
+
57
+ export class PolicyDeniedError extends Error {
58
+ action: PolicyAction;
59
+ subject: string;
60
+ rule: string;
61
+
62
+ constructor(action: PolicyAction, subject: string, rule: string) {
63
+ super(`Policy denied: ${action} on "${subject}" (rule: ${rule})`);
64
+ this.name = 'PolicyDeniedError';
65
+ this.action = action;
66
+ this.subject = subject;
67
+ this.rule = rule;
68
+ }
69
+ }
70
+
71
+ // ─── PolicyEngine ────────────────────────────────────────────────────────────
72
+
73
+ export class PolicyEngine {
74
+ private policy: Policy;
75
+
76
+ constructor(policy?: Policy) {
77
+ this.policy = policy ?? PolicyEngine.defaultPolicy();
78
+ }
79
+
80
+ loadPolicy(policy: Policy): void {
81
+ this.policy = policy;
82
+ }
83
+
84
+ getPolicy(): Policy {
85
+ return this.policy;
86
+ }
87
+
88
+ check(
89
+ action: PolicyAction,
90
+ subject: string,
91
+ meta?: Record<string, unknown>,
92
+ ): CheckResult {
93
+ const p = this.policy;
94
+
95
+ // File rules
96
+ if (action === 'file.read') {
97
+ for (const rule of p.files.read) {
98
+ if (globMatch(rule.pattern, subject)) {
99
+ return { allowed: rule.allow, rule: `file.read: ${rule.pattern}`, action, subject };
100
+ }
101
+ }
102
+ return { allowed: p.mode === 'allow-all', action, subject };
103
+ }
104
+
105
+ if (action === 'file.write') {
106
+ // Check size limit first
107
+ if (meta?.size != null && typeof meta.size === 'number') {
108
+ if (meta.size > p.limits.maxFileSize) {
109
+ return {
110
+ allowed: false,
111
+ rule: `limits.maxFileSize: ${p.limits.maxFileSize}`,
112
+ action,
113
+ subject,
114
+ };
115
+ }
116
+ }
117
+ for (const rule of p.files.write) {
118
+ if (globMatch(rule.pattern, subject)) {
119
+ return { allowed: rule.allow, rule: `file.write: ${rule.pattern}`, action, subject };
120
+ }
121
+ }
122
+ return { allowed: p.mode === 'allow-all', action, subject };
123
+ }
124
+
125
+ // Process rules
126
+ if (action === 'process.spawn') {
127
+ // Check process count limit
128
+ if (meta?.activeProcesses != null && typeof meta.activeProcesses === 'number') {
129
+ if (meta.activeProcesses >= p.limits.maxProcesses) {
130
+ return {
131
+ allowed: false,
132
+ rule: `limits.maxProcesses: ${p.limits.maxProcesses}`,
133
+ action,
134
+ subject,
135
+ };
136
+ }
137
+ }
138
+ for (const rule of p.processes) {
139
+ if (globMatch(rule.pattern, subject)) {
140
+ return { allowed: rule.allow, rule: `process: ${rule.pattern}`, action, subject };
141
+ }
142
+ }
143
+ return { allowed: p.mode === 'allow-all', action, subject };
144
+ }
145
+
146
+ // Port rules
147
+ if (action === 'server.bind') {
148
+ const port = Number(subject);
149
+ for (const rule of p.ports) {
150
+ if (rule.port === '*' || rule.port === port) {
151
+ return { allowed: rule.allow, rule: `port: ${rule.port}`, action, subject };
152
+ }
153
+ }
154
+ return { allowed: p.mode === 'allow-all', action, subject };
155
+ }
156
+
157
+ // Tool rules
158
+ if (action === 'tool.use') {
159
+ for (const rule of p.tools) {
160
+ if (rule.name === subject || rule.name === '*') {
161
+ return { allowed: rule.allow, rule: `tool: ${rule.name}`, action, subject };
162
+ }
163
+ }
164
+ return { allowed: p.mode === 'allow-all', action, subject };
165
+ }
166
+
167
+ // env.configure — no specific rules, just mode
168
+ return { allowed: p.mode === 'allow-all', action, subject };
169
+ }
170
+
171
+ enforce(
172
+ action: PolicyAction,
173
+ subject: string,
174
+ meta?: Record<string, unknown>,
175
+ ): CheckResult {
176
+ const result = this.check(action, subject, meta);
177
+ if (!result.allowed) {
178
+ throw new PolicyDeniedError(action, subject, result.rule ?? this.policy.mode);
179
+ }
180
+ return result;
181
+ }
182
+
183
+ // ─── YAML serialization (hand-rolled, no deps) ──────────────────────────
184
+
185
+ toYaml(): string {
186
+ const p = this.policy;
187
+ const lines: string[] = [];
188
+ lines.push(`version: "1"`);
189
+ lines.push(`mode: ${p.mode}`);
190
+
191
+ lines.push(`files:`);
192
+ lines.push(` read:`);
193
+ for (const r of p.files.read) {
194
+ lines.push(` - pattern: "${r.pattern}"`);
195
+ lines.push(` allow: ${r.allow}`);
196
+ }
197
+ lines.push(` write:`);
198
+ for (const r of p.files.write) {
199
+ lines.push(` - pattern: "${r.pattern}"`);
200
+ lines.push(` allow: ${r.allow}`);
201
+ }
202
+
203
+ lines.push(`processes:`);
204
+ for (const r of p.processes) {
205
+ lines.push(` - pattern: "${r.pattern}"`);
206
+ lines.push(` allow: ${r.allow}`);
207
+ }
208
+
209
+ lines.push(`ports:`);
210
+ for (const r of p.ports) {
211
+ lines.push(` - port: ${r.port === '*' ? '"*"' : r.port}`);
212
+ lines.push(` allow: ${r.allow}`);
213
+ }
214
+
215
+ lines.push(`tools:`);
216
+ for (const r of p.tools) {
217
+ lines.push(` - name: "${r.name}"`);
218
+ lines.push(` allow: ${r.allow}`);
219
+ }
220
+
221
+ lines.push(`limits:`);
222
+ lines.push(` maxFileSize: ${p.limits.maxFileSize}`);
223
+ lines.push(` maxProcesses: ${p.limits.maxProcesses}`);
224
+ lines.push(` maxTurns: ${p.limits.maxTurns}`);
225
+ lines.push(` timeoutSec: ${p.limits.timeoutSec}`);
226
+
227
+ return lines.join('\n') + '\n';
228
+ }
229
+
230
+ static fromYaml(yaml: string): Policy {
231
+ const p = PolicyEngine.defaultPolicy();
232
+
233
+ const get = (key: string): string | undefined => {
234
+ const m = yaml.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
235
+ return m?.[1]?.trim().replace(/^["']|["']$/g, '');
236
+ };
237
+
238
+ const ver = get('version');
239
+ if (ver && ver !== '1') throw new Error(`Unsupported policy version: ${ver}`);
240
+
241
+ const mode = get('mode');
242
+ if (mode === 'allow-all' || mode === 'deny-all') p.mode = mode;
243
+
244
+ // Parse limits
245
+ const maxFileSize = get('maxFileSize');
246
+ if (maxFileSize) p.limits.maxFileSize = Number(maxFileSize);
247
+ const maxProcesses = get('maxProcesses');
248
+ if (maxProcesses) p.limits.maxProcesses = Number(maxProcesses);
249
+ const maxTurns = get('maxTurns');
250
+ if (maxTurns) p.limits.maxTurns = Number(maxTurns);
251
+ const timeoutSec = get('timeoutSec');
252
+ if (timeoutSec) p.limits.timeoutSec = Number(timeoutSec);
253
+
254
+ // Parse list sections
255
+ p.files.read = parseFileRules(yaml, 'read');
256
+ p.files.write = parseFileRules(yaml, 'write');
257
+ p.processes = parsePatternRules(yaml, 'processes');
258
+ p.ports = parsePortRules(yaml);
259
+ p.tools = parseToolRules(yaml);
260
+
261
+ return p;
262
+ }
263
+
264
+ static defaultPolicy(): Policy {
265
+ return {
266
+ version: '1',
267
+ mode: 'allow-all',
268
+ files: { read: [], write: [] },
269
+ processes: [],
270
+ ports: [],
271
+ tools: [],
272
+ limits: {
273
+ maxFileSize: 10_485_760,
274
+ maxProcesses: 10,
275
+ maxTurns: 50,
276
+ timeoutSec: 120,
277
+ },
278
+ };
279
+ }
280
+ }
281
+
282
+ // ─── Glob matcher (~30 lines, no deps) ────────────────────────────────────────
283
+
284
+ function globMatch(pattern: string, subject: string): boolean {
285
+ const regex = globToRegex(pattern);
286
+ return regex.test(subject);
287
+ }
288
+
289
+ function globToRegex(glob: string): RegExp {
290
+ let re = '';
291
+ let i = 0;
292
+ while (i < glob.length) {
293
+ const c = glob[i];
294
+ if (c === '*') {
295
+ if (glob[i + 1] === '*') {
296
+ // ** matches any depth
297
+ re += '.*';
298
+ i += 2;
299
+ if (glob[i] === '/') i++; // skip trailing /
300
+ } else {
301
+ // * matches single segment (no /)
302
+ re += '[^/]*';
303
+ i++;
304
+ }
305
+ } else if (c === '?') {
306
+ re += '[^/]';
307
+ i++;
308
+ } else if ('.+^${}()|[]\\'.includes(c!)) {
309
+ re += '\\' + c;
310
+ i++;
311
+ } else {
312
+ re += c;
313
+ i++;
314
+ }
315
+ }
316
+ return new RegExp(`^${re}$`);
317
+ }
318
+
319
+ // ─── YAML parsing helpers ─────────────────────────────────────────────────────
320
+
321
+ function parseFileRules(yaml: string, section: 'read' | 'write'): FileRule[] {
322
+ const rules: FileRule[] = [];
323
+ // Find the section within files:
324
+ const sectionRegex = new RegExp(`^\\s{2}${section}:\\s*$`, 'm');
325
+ const match = sectionRegex.exec(yaml);
326
+ if (!match) return rules;
327
+
328
+ const after = yaml.slice(match.index + match[0].length);
329
+ const itemRegex = /^\s{4}-\s+pattern:\s*"([^"]*)"\s*\n\s+allow:\s*(true|false)/gm;
330
+ let m;
331
+ while ((m = itemRegex.exec(after)) !== null) {
332
+ // Stop if we hit a less-indented line (next section)
333
+ const beforeMatch = after.slice(0, m.index);
334
+ if (/^\S/m.test(beforeMatch.split('\n').slice(1).join('\n'))) break;
335
+ rules.push({ pattern: m[1], allow: m[2] === 'true' });
336
+ }
337
+ return rules;
338
+ }
339
+
340
+ function parsePatternRules(yaml: string, section: string): ProcessRule[] {
341
+ const rules: ProcessRule[] = [];
342
+ const sectionRegex = new RegExp(`^${section}:\\s*$`, 'm');
343
+ const match = sectionRegex.exec(yaml);
344
+ if (!match) return rules;
345
+
346
+ const after = yaml.slice(match.index + match[0].length);
347
+ const lines = after.split('\n');
348
+ for (let i = 0; i < lines.length; i++) {
349
+ const line = lines[i];
350
+ // Stop at next top-level key
351
+ if (/^\S/.test(line) && line.trim() !== '') break;
352
+ const pm = line.match(/^\s+-\s+pattern:\s*"([^"]*)"/);
353
+ if (pm && i + 1 < lines.length) {
354
+ const am = lines[i + 1].match(/^\s+allow:\s*(true|false)/);
355
+ if (am) rules.push({ pattern: pm[1], allow: am[1] === 'true' });
356
+ }
357
+ }
358
+ return rules;
359
+ }
360
+
361
+ function parsePortRules(yaml: string): PortRule[] {
362
+ const rules: PortRule[] = [];
363
+ const match = /^ports:\s*$/m.exec(yaml);
364
+ if (!match) return rules;
365
+
366
+ const after = yaml.slice(match.index + match[0].length);
367
+ const lines = after.split('\n');
368
+ for (let i = 0; i < lines.length; i++) {
369
+ const line = lines[i];
370
+ if (/^\S/.test(line) && line.trim() !== '') break;
371
+ const pm = line.match(/^\s+-\s+port:\s*("?\*?"?|\d+)/);
372
+ if (pm && i + 1 < lines.length) {
373
+ const am = lines[i + 1].match(/^\s+allow:\s*(true|false)/);
374
+ if (am) {
375
+ const portVal = pm[1].replace(/"/g, '');
376
+ rules.push({
377
+ port: portVal === '*' ? '*' : Number(portVal),
378
+ allow: am[1] === 'true',
379
+ });
380
+ }
381
+ }
382
+ }
383
+ return rules;
384
+ }
385
+
386
+ function parseToolRules(yaml: string): ToolRule[] {
387
+ const rules: ToolRule[] = [];
388
+ const match = /^tools:\s*$/m.exec(yaml);
389
+ if (!match) return rules;
390
+
391
+ const after = yaml.slice(match.index + match[0].length);
392
+ const lines = after.split('\n');
393
+ for (let i = 0; i < lines.length; i++) {
394
+ const line = lines[i];
395
+ if (/^\S/.test(line) && line.trim() !== '') break;
396
+ const nm = line.match(/^\s+-\s+name:\s*"([^"]*)"/);
397
+ if (nm && i + 1 < lines.length) {
398
+ const am = lines[i + 1].match(/^\s+allow:\s*(true|false)/);
399
+ if (am) rules.push({ name: nm[1], allow: am[1] === 'true' });
400
+ }
401
+ }
402
+ return rules;
403
+ }