@principles/core 1.229.0 → 1.231.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.
- package/dist/runtime-v2/__tests__/architecture-regression.test.js +309 -0
- package/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
- package/dist/runtime-v2/adapter/__tests__/pi-ai-runtime-adapter.test.js +6 -6
- package/dist/runtime-v2/adapter/__tests__/pi-ai-runtime-adapter.test.js.map +1 -1
- package/dist/runtime-v2/internalization/__tests__/rule-code-dialect.test.js +29 -0
- package/dist/runtime-v2/internalization/__tests__/rule-code-dialect.test.js.map +1 -1
- package/dist/runtime-v2/internalization/rule-code-validator.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/rule-code-validator.js +7 -1
- package/dist/runtime-v2/internalization/rule-code-validator.js.map +1 -1
- package/package.json +1 -1
|
@@ -3441,4 +3441,313 @@ describe('PRI-450 / PRI-462: core I/O seam registry guard', () => {
|
|
|
3441
3441
|
expect(importsFsOrPath(`import type { Database } from 'better-sqlite3';`)).toBe(false);
|
|
3442
3442
|
});
|
|
3443
3443
|
});
|
|
3444
|
+
// ===========================================================================
|
|
3445
|
+
// SEC-BASE-1: Supply Chain Provenance Guards
|
|
3446
|
+
// Guards the npm publish / CI supply chain controls declared in
|
|
3447
|
+
// docs/architecture/SECURITY_BASELINE.md §3 (supply chain layer).
|
|
3448
|
+
// ===========================================================================
|
|
3449
|
+
describe('SEC-BASE-1: supply chain provenance guards', () => {
|
|
3450
|
+
const REPO_ROOT = pathSync.resolve(__dirname, '../../../../..');
|
|
3451
|
+
function readWorkflow(name) {
|
|
3452
|
+
const p = pathSync.join(REPO_ROOT, '.github', 'workflows', name);
|
|
3453
|
+
try {
|
|
3454
|
+
return fsSync.readFileSync(p, 'utf8');
|
|
3455
|
+
}
|
|
3456
|
+
catch {
|
|
3457
|
+
return null;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
it('publish-npm.yml uses --provenance', () => {
|
|
3461
|
+
const content = readWorkflow('publish-npm.yml');
|
|
3462
|
+
expect(content, 'publish-npm.yml should exist').not.toBeNull();
|
|
3463
|
+
expect(content).toMatch(/npm\s+publish\s+--provenance/);
|
|
3464
|
+
});
|
|
3465
|
+
it('publish-npm.yml uses npm ci --ignore-scripts', () => {
|
|
3466
|
+
const content = readWorkflow('publish-npm.yml');
|
|
3467
|
+
expect(content).not.toBeNull();
|
|
3468
|
+
expect(content).toMatch(/npm\s+ci\s+--ignore-scripts/);
|
|
3469
|
+
});
|
|
3470
|
+
it('SECURITY.md exists at .github/SECURITY.md', () => {
|
|
3471
|
+
const p = pathSync.join(REPO_ROOT, '.github', 'SECURITY.md');
|
|
3472
|
+
expect(fsSync.existsSync(p), `${p} should exist`).toBe(true);
|
|
3473
|
+
});
|
|
3474
|
+
it('CodeQL workflow targets javascript-typescript', () => {
|
|
3475
|
+
const content = readWorkflow('codeql.yml');
|
|
3476
|
+
expect(content, 'codeql.yml should exist').not.toBeNull();
|
|
3477
|
+
expect(content).toMatch(/javascript-typescript/);
|
|
3478
|
+
});
|
|
3479
|
+
it('dependabot.yml covers npm and github-actions', () => {
|
|
3480
|
+
const p = pathSync.join(REPO_ROOT, '.github', 'dependabot.yml');
|
|
3481
|
+
const content = fsSync.readFileSync(p, 'utf8');
|
|
3482
|
+
expect(content).toMatch(/package-ecosystem:\s*['"]?npm['"]?/m);
|
|
3483
|
+
expect(content).toMatch(/package-ecosystem:\s*['"]?github-actions['"]?/m);
|
|
3484
|
+
});
|
|
3485
|
+
});
|
|
3486
|
+
// ===========================================================================
|
|
3487
|
+
// SEC-BASE-2: Sandbox Escape Regression Guards
|
|
3488
|
+
// Guards the vm sandbox layer declared in SECURITY_BASELINE.md §4.
|
|
3489
|
+
// Escape payload regression lives in
|
|
3490
|
+
// packages/openclaw-plugin/tests/core/sandbox-escape-regression.test.ts
|
|
3491
|
+
// ===========================================================================
|
|
3492
|
+
describe('SEC-BASE-2: sandbox escape regression guards', () => {
|
|
3493
|
+
const REPO_ROOT = pathSync.resolve(__dirname, '../../../../..');
|
|
3494
|
+
it('rule-code-validator forbids import.meta', async () => {
|
|
3495
|
+
const { checkForbiddenPatterns } = await import('../internalization/rule-code-validator.js');
|
|
3496
|
+
const labels = checkForbiddenPatterns('const u = import.meta.url;');
|
|
3497
|
+
expect(labels).toContain('import.meta');
|
|
3498
|
+
});
|
|
3499
|
+
it('rule-code-validator forbids WeakRef / FinalizationRegistry', async () => {
|
|
3500
|
+
const { checkForbiddenPatterns } = await import('../internalization/rule-code-validator.js');
|
|
3501
|
+
expect(checkForbiddenPatterns('new WeakRef({});')).toContain('WeakRef');
|
|
3502
|
+
expect(checkForbiddenPatterns('new FinalizationRegistry(() => {});')).toContain('FinalizationRegistry');
|
|
3503
|
+
});
|
|
3504
|
+
it('rule-code-validator forbids SharedArrayBuffer / Atomics', async () => {
|
|
3505
|
+
const { checkForbiddenPatterns } = await import('../internalization/rule-code-validator.js');
|
|
3506
|
+
expect(checkForbiddenPatterns('new SharedArrayBuffer(8);')).toContain('SharedArrayBuffer');
|
|
3507
|
+
expect(checkForbiddenPatterns('Atomics.load(new Int32Array(1), 0);')).toContain('Atomics');
|
|
3508
|
+
});
|
|
3509
|
+
it('rule-implementation-runtime.ts uses spawnSync with bounded resources', () => {
|
|
3510
|
+
const sourcePath = pathSync.join(REPO_ROOT, 'packages', 'openclaw-plugin', 'src', 'core', 'rule-implementation-runtime.ts');
|
|
3511
|
+
const source = fsSync.readFileSync(sourcePath, 'utf8');
|
|
3512
|
+
expect(source).toContain('spawnSync');
|
|
3513
|
+
expect(source).toContain('windowsHide: true');
|
|
3514
|
+
expect(source).toContain('maxBuffer');
|
|
3515
|
+
expect(source).toContain('--max-old-space-size');
|
|
3516
|
+
expect(source).toMatch(/timeout:\s*\w+/);
|
|
3517
|
+
});
|
|
3518
|
+
});
|
|
3519
|
+
// ===========================================================================
|
|
3520
|
+
// SEC-BASE-3: PII Redaction Guards
|
|
3521
|
+
// Guards that the implemented PII sanitization patterns exist in
|
|
3522
|
+
// openclaw-plugin (trajectory.ts redactText) and that message-sanitize hook
|
|
3523
|
+
// applies sanitization to message content.
|
|
3524
|
+
//
|
|
3525
|
+
// IMPLEMENTED (MVP): <EMAIL>, <TOKEN> (sk/rk/pk prefix), <PATH>, <WINDOWS_PATH>
|
|
3526
|
+
// POST-MVP GAPS (documented in SECURITY_BASELINE.md): <PHONE>, <CARD>, <IP>
|
|
3527
|
+
// ===========================================================================
|
|
3528
|
+
describe('SEC-BASE-3: PII redaction guards', () => {
|
|
3529
|
+
const REPO_ROOT = pathSync.resolve(__dirname, '../../../../..');
|
|
3530
|
+
it('openclaw-plugin contains email redaction pattern with <EMAIL> replacement', () => {
|
|
3531
|
+
const pluginDir = pathSync.join(REPO_ROOT, 'packages', 'openclaw-plugin', 'src');
|
|
3532
|
+
const files = fsSync.readdirSync(pluginDir, { recursive: true, encoding: 'utf8' })
|
|
3533
|
+
.filter((f) => typeof f === 'string' && f.endsWith('.ts') && !f.includes('__tests__'));
|
|
3534
|
+
let foundEmail = false;
|
|
3535
|
+
for (const f of files) {
|
|
3536
|
+
const content = fsSync.readFileSync(pathSync.join(pluginDir, f), 'utf8');
|
|
3537
|
+
// Look for: <EMAIL> replacement token AND a regex containing @ (email pattern)
|
|
3538
|
+
if (/<EMAIL>/.test(content) && /\.\s*replace\s*\(\s*\/.*@.*\//.test(content)) {
|
|
3539
|
+
foundEmail = true;
|
|
3540
|
+
break;
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
expect(foundEmail, 'openclaw-plugin should contain email redaction pattern with <EMAIL> replacement').toBe(true);
|
|
3544
|
+
});
|
|
3545
|
+
it('openclaw-plugin contains API token redaction pattern (<TOKEN> with sk/rk/pk prefix)', () => {
|
|
3546
|
+
const pluginDir = pathSync.join(REPO_ROOT, 'packages', 'openclaw-plugin', 'src');
|
|
3547
|
+
const files = fsSync.readdirSync(pluginDir, { recursive: true, encoding: 'utf8' })
|
|
3548
|
+
.filter((f) => typeof f === 'string' && f.endsWith('.ts') && !f.includes('__tests__'));
|
|
3549
|
+
let foundToken = false;
|
|
3550
|
+
for (const f of files) {
|
|
3551
|
+
const content = fsSync.readFileSync(pathSync.join(pluginDir, f), 'utf8');
|
|
3552
|
+
// Look for: <TOKEN> replacement token AND a regex containing (sk|rk|pk)
|
|
3553
|
+
if (/<TOKEN>/.test(content) && /\(sk\|rk\|pk\)/.test(content)) {
|
|
3554
|
+
foundToken = true;
|
|
3555
|
+
break;
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
expect(foundToken, 'openclaw-plugin should contain API token redaction pattern with <TOKEN> replacement').toBe(true);
|
|
3559
|
+
});
|
|
3560
|
+
it('openclaw-plugin message-sanitize hook applies sanitization', () => {
|
|
3561
|
+
const sourcePath = pathSync.join(REPO_ROOT, 'packages', 'openclaw-plugin', 'src', 'hooks', 'message-sanitize.ts');
|
|
3562
|
+
const source = fsSync.readFileSync(sourcePath, 'utf8');
|
|
3563
|
+
// Delegate pattern: import from core + apply to message content
|
|
3564
|
+
expect(source).toMatch(/coreSanitize|sanitizeValue|sanitiz/i);
|
|
3565
|
+
});
|
|
3566
|
+
});
|
|
3567
|
+
// ===========================================================================
|
|
3568
|
+
// SEC-BASE-4: Log Secret Redaction & SQLite SQL Injection Guards
|
|
3569
|
+
// Guards that:
|
|
3570
|
+
// (a) core does not emit raw secret-named fields in logger output
|
|
3571
|
+
// (b) core does not string-concatenate SQL (uses parameterized queries)
|
|
3572
|
+
// ===========================================================================
|
|
3573
|
+
describe('SEC-BASE-4: log secret redaction & SQL injection guards', () => {
|
|
3574
|
+
const REPO_ROOT = pathSync.resolve(__dirname, '../../../../..');
|
|
3575
|
+
it('core source does not log raw api_key / token / secret / password fields', () => {
|
|
3576
|
+
const coreDir = pathSync.join(REPO_ROOT, 'packages', 'principles-core', 'src');
|
|
3577
|
+
const files = fsSync.readdirSync(coreDir, { recursive: true, encoding: 'utf8' })
|
|
3578
|
+
.filter((f) => typeof f === 'string' && f.endsWith('.ts') && !f.includes('__tests__'));
|
|
3579
|
+
const suspicious = [];
|
|
3580
|
+
for (const f of files) {
|
|
3581
|
+
const content = fsSync.readFileSync(pathSync.join(coreDir, f), 'utf8');
|
|
3582
|
+
const lines = content.split('\n');
|
|
3583
|
+
lines.forEach((line, idx) => {
|
|
3584
|
+
if (/log(ger)?\./i.test(line) || /console\./.test(line)) {
|
|
3585
|
+
if (/\b(api[_-]?key|token|secret|password|credential)\b/i.test(line) && !/<REDACTED>/.test(line) && !/sanitiz/i.test(line)) {
|
|
3586
|
+
suspicious.push(`${f}:${idx + 1}: ${line.trim()}`);
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
});
|
|
3590
|
+
}
|
|
3591
|
+
expect(suspicious, `Suspicious unredacted logger lines in core:\n${suspicious.join('\n')}`).toEqual([]);
|
|
3592
|
+
});
|
|
3593
|
+
it('core source does not string-concatenate SQL statements', () => {
|
|
3594
|
+
const coreDir = pathSync.join(REPO_ROOT, 'packages', 'principles-core', 'src');
|
|
3595
|
+
const files = fsSync.readdirSync(coreDir, { recursive: true, encoding: 'utf8' })
|
|
3596
|
+
.filter((f) => typeof f === 'string'
|
|
3597
|
+
&& f.endsWith('.ts')
|
|
3598
|
+
&& !f.endsWith('.test.ts')
|
|
3599
|
+
&& !f.includes('__tests__'));
|
|
3600
|
+
const suspicious = [];
|
|
3601
|
+
// Known-safe pre-built clause variables (constructed from controlled
|
|
3602
|
+
// conditions with `?` placeholders for values — not user input).
|
|
3603
|
+
// `placeholders` = e.g. `ids.map(() => '?').join(',')` → generates `?,?,?`
|
|
3604
|
+
// (a string of `?` placeholder characters, NOT user values).
|
|
3605
|
+
const SAFE_CLAUSE_VARS = new Set([
|
|
3606
|
+
'whereClause', 'orderByClause', 'limitClause', 'setClause',
|
|
3607
|
+
'conflictClause', 'returningClause', 'onConflictClause',
|
|
3608
|
+
'placeholders',
|
|
3609
|
+
]);
|
|
3610
|
+
// DML statements that MUST use parameterized values (`?` placeholders).
|
|
3611
|
+
// DDL (ALTER TABLE / CREATE TABLE / CREATE INDEX / DROP) is excluded
|
|
3612
|
+
// because SQLite does not accept `?` placeholders for identifiers (column
|
|
3613
|
+
// names, table names) — DDL string-concat with controlled constants is
|
|
3614
|
+
// the only valid pattern and is verified separately.
|
|
3615
|
+
const DML_RE = /\b(SELECT|INSERT\s+INTO|UPDATE\s+\w|DELETE\s+FROM)\b/i;
|
|
3616
|
+
for (const f of files) {
|
|
3617
|
+
const content = fsSync.readFileSync(pathSync.join(coreDir, f), 'utf8');
|
|
3618
|
+
// Find line number offset for reporting.
|
|
3619
|
+
const lines = content.split('\n');
|
|
3620
|
+
// Match .prepare(`...`) and .exec(`...`) template-literal SQL arguments.
|
|
3621
|
+
// Uses [\s\S]*? (non-greedy, dotAll-equivalent) to capture multi-line
|
|
3622
|
+
// template literals. Only the FIRST argument (the SQL string) is inspected.
|
|
3623
|
+
const templateSqlRe = /\.(prepare|exec)\s*\(\s*`([\s\S]*?)`/g;
|
|
3624
|
+
let m;
|
|
3625
|
+
while ((m = templateSqlRe.exec(content)) !== null) {
|
|
3626
|
+
// rc-2-no-as-bypass: RegExpExecArray indices are `string | undefined`
|
|
3627
|
+
// under noUncheckedIndexedAccess. Use array destructuring (ESLint
|
|
3628
|
+
// prefer-destructuring) + explicit undefined check (fail-loud).
|
|
3629
|
+
const [, , sqlBody] = m;
|
|
3630
|
+
if (sqlBody === undefined)
|
|
3631
|
+
continue;
|
|
3632
|
+
if (!DML_RE.test(sqlBody))
|
|
3633
|
+
continue; // DDL-only is allowed
|
|
3634
|
+
// Find line number of the match start.
|
|
3635
|
+
const matchOffset = m.index;
|
|
3636
|
+
const lineNum = content.slice(0, matchOffset).split('\n').length;
|
|
3637
|
+
// Check for `${X}` interpolations inside the SQL template body.
|
|
3638
|
+
const interpRe = /\$\{([^}]+)\}/g;
|
|
3639
|
+
let im;
|
|
3640
|
+
while ((im = interpRe.exec(sqlBody)) !== null) {
|
|
3641
|
+
const [, expr] = im;
|
|
3642
|
+
if (expr === undefined)
|
|
3643
|
+
continue;
|
|
3644
|
+
const exprTrimmed = expr.trim();
|
|
3645
|
+
// Safe: ends with .join(...) — column-list / placeholder-list pattern.
|
|
3646
|
+
if (/\.join\s*\(/.test(exprTrimmed))
|
|
3647
|
+
continue;
|
|
3648
|
+
// Safe: known pre-built clause variable.
|
|
3649
|
+
const rootVar = exprTrimmed.split('.')[0]?.split('[')[0]?.split('(')[0];
|
|
3650
|
+
if (rootVar !== undefined && SAFE_CLAUSE_VARS.has(rootVar.trim()))
|
|
3651
|
+
continue;
|
|
3652
|
+
// Safe: numeric / length / index expressions (not user data).
|
|
3653
|
+
if (/^\d+$/.test(exprTrimmed) || /\.length$/.test(exprTrimmed))
|
|
3654
|
+
continue;
|
|
3655
|
+
suspicious.push(`${f}:${lineNum}: interpolation \${${exprTrimmed}} in DML template`);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
// Also check for string-concatenation pattern: .prepare('...' + var + '...')
|
|
3659
|
+
// This is a single-line check (string concat SQL is rarely multi-line).
|
|
3660
|
+
const concatSqlRe = /\.(prepare|exec)\s*\(\s*['"][^'"]*['"]\s*\+/g;
|
|
3661
|
+
let cm;
|
|
3662
|
+
while ((cm = concatSqlRe.exec(content)) !== null) {
|
|
3663
|
+
const lineNum = content.slice(0, cm.index).split('\n').length;
|
|
3664
|
+
const line = lines[lineNum - 1] || '';
|
|
3665
|
+
// Only flag if the concatenated string contains DML keywords.
|
|
3666
|
+
if (DML_RE.test(line)) {
|
|
3667
|
+
suspicious.push(`${f}:${lineNum}: string-concat in DML: ${line.trim()}`);
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
expect(suspicious, `Suspicious string-concatenated or interpolated DML in core:\n${suspicious.join('\n')}`).toEqual([]);
|
|
3672
|
+
});
|
|
3673
|
+
});
|
|
3674
|
+
// ===========================================================================
|
|
3675
|
+
// SEC-BASE-5: Network & Config Isolation Guards
|
|
3676
|
+
// Guards:
|
|
3677
|
+
// (a) core does not import node:http / node:https / undici / fetch (NET-1)
|
|
3678
|
+
// (b) pd-console server config does not default to 0.0.0.0 (NET-5)
|
|
3679
|
+
// (c) code_tool_hook config defaults to require_approval (CFG-4)
|
|
3680
|
+
// ===========================================================================
|
|
3681
|
+
describe('SEC-BASE-5: network & config isolation guards', () => {
|
|
3682
|
+
const REPO_ROOT = pathSync.resolve(__dirname, '../../../../..');
|
|
3683
|
+
it('core does not import node:http / node:https / undici / fetch for business', () => {
|
|
3684
|
+
const coreDir = pathSync.join(REPO_ROOT, 'packages', 'principles-core', 'src');
|
|
3685
|
+
const files = fsSync.readdirSync(coreDir, { recursive: true, encoding: 'utf8' })
|
|
3686
|
+
.filter((f) => typeof f === 'string' && f.endsWith('.ts') && !f.includes('__tests__'));
|
|
3687
|
+
const violations = [];
|
|
3688
|
+
for (const f of files) {
|
|
3689
|
+
const content = fsSync.readFileSync(pathSync.join(coreDir, f), 'utf8');
|
|
3690
|
+
const lines = content.split('\n');
|
|
3691
|
+
lines.forEach((line, idx) => {
|
|
3692
|
+
if (/^import\s+type/.test(line.trim()))
|
|
3693
|
+
return;
|
|
3694
|
+
if (/from\s+['"]node:http['"]/.test(line) || /from\s+['"]node:https['"]/.test(line)
|
|
3695
|
+
|| /from\s+['"]undici['"]/.test(line) || /from\s+['"]node-fetch['"]/.test(line)) {
|
|
3696
|
+
violations.push(`${f}:${idx + 1}: ${line.trim()}`);
|
|
3697
|
+
}
|
|
3698
|
+
if (!/^\s*(\/\/|\/\*|\*)/.test(line) && /\bfetch\s*\(/.test(line) && !/import/.test(line)) {
|
|
3699
|
+
violations.push(`${f}:${idx + 1}: ${line.trim()}`);
|
|
3700
|
+
}
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
expect(violations, `core should not import network modules (NET-1):\n${violations.join('\n')}`).toEqual([]);
|
|
3704
|
+
});
|
|
3705
|
+
it('pd-console server config does not default to 0.0.0.0', () => {
|
|
3706
|
+
const consoleDir = pathSync.join(REPO_ROOT, 'packages', 'pd-console', 'src');
|
|
3707
|
+
if (!fsSync.existsSync(consoleDir)) {
|
|
3708
|
+
// pd-console may not exist in this worktree — skip with explicit note
|
|
3709
|
+
console.warn('SEC-BASE-5: pd-console src dir not found, skipping bind_host check');
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
const files = fsSync.readdirSync(consoleDir, { recursive: true, encoding: 'utf8' })
|
|
3713
|
+
.filter((f) => typeof f === 'string' && f.endsWith('.ts'));
|
|
3714
|
+
const violations = [];
|
|
3715
|
+
for (const f of files) {
|
|
3716
|
+
const content = fsSync.readFileSync(pathSync.join(consoleDir, f), 'utf8');
|
|
3717
|
+
const lines = content.split('\n');
|
|
3718
|
+
lines.forEach((line, idx) => {
|
|
3719
|
+
// rc-9-no-silent-fallback: previously used `!/comment/.test(line)` which
|
|
3720
|
+
// matched any line containing the substring "comment" (e.g. a variable
|
|
3721
|
+
// named `commentField`), silently skipping real 0.0.0.0 defaults.
|
|
3722
|
+
// Only skip genuine comment lines.
|
|
3723
|
+
const trimmed = line.trim();
|
|
3724
|
+
const isCommentLine = trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*');
|
|
3725
|
+
if (/host\s*[:=]\s*['"]0\.0\.0\.0['"]/.test(line) && !isCommentLine) {
|
|
3726
|
+
violations.push(`${f}:${idx + 1}: ${line.trim()}`);
|
|
3727
|
+
}
|
|
3728
|
+
});
|
|
3729
|
+
}
|
|
3730
|
+
expect(violations, `pd-console should not default to 0.0.0.0 (NET-5):\n${violations.join('\n')}`).toEqual([]);
|
|
3731
|
+
});
|
|
3732
|
+
it('code_tool_hook config defaults to require_approval', () => {
|
|
3733
|
+
// rc-9-no-silent-fallback: this was previously `expect(true).toBe(true)`
|
|
3734
|
+
// (a tautology that always passed, giving false confidence). Now it fails
|
|
3735
|
+
// loud if the require_approval decision is removed from the codebase.
|
|
3736
|
+
// The `requireApproval` decision is enforced in gate.ts and is the
|
|
3737
|
+
// MVP-Core safety boundary for code_tool_hook.
|
|
3738
|
+
const pluginDir = pathSync.join(REPO_ROOT, 'packages', 'openclaw-plugin', 'src');
|
|
3739
|
+
const files = fsSync.readdirSync(pluginDir, { recursive: true, encoding: 'utf8' })
|
|
3740
|
+
.filter((f) => typeof f === 'string' && f.endsWith('.ts') && !f.includes('__tests__'));
|
|
3741
|
+
let foundRequireApproval = false;
|
|
3742
|
+
for (const f of files) {
|
|
3743
|
+
const content = fsSync.readFileSync(pathSync.join(pluginDir, f), 'utf8');
|
|
3744
|
+
if (/code[_-]?tool[_-]?hook/i.test(content) && /require[_-]?approval/i.test(content)) {
|
|
3745
|
+
foundRequireApproval = true;
|
|
3746
|
+
break;
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
expect(foundRequireApproval, 'code_tool_hook should reference require_approval decision in openclaw-plugin src ' +
|
|
3750
|
+
'(expected in hooks/gate.ts). If removed, the code_tool_hook safety boundary is broken.').toBe(true);
|
|
3751
|
+
});
|
|
3752
|
+
});
|
|
3444
3753
|
//# sourceMappingURL=architecture-regression.test.js.map
|