@pathbit/agentdevkit 0.0.10 → 0.0.12

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 (134) hide show
  1. package/.pbai4devs/COMPATIBILIDADE.md +6 -0
  2. package/.pbai4devs/INSTRUCTIONS.md +3 -0
  3. package/.pbai4devs/agents/README.md +19 -0
  4. package/.pbai4devs/agents/auditor-seguranca/AGENT.md +20 -0
  5. package/.pbai4devs/agents/auditor-seguranca/antigravity.yaml +8 -0
  6. package/.pbai4devs/agents/auditor-seguranca/claude.yaml +5 -0
  7. package/.pbai4devs/agents/auditor-seguranca/cursor.yaml +5 -0
  8. package/.pbai4devs/agents/auditor-seguranca/gemini.yaml +6 -0
  9. package/.pbai4devs/agents/auditor-seguranca/github.yaml +5 -0
  10. package/.pbai4devs/agents/auditor-seguranca/kimi-code.yaml +5 -0
  11. package/.pbai4devs/agents/auditor-seguranca/mimocode.yaml +8 -0
  12. package/.pbai4devs/agents/auditor-seguranca/opencode.yaml +11 -0
  13. package/.pbai4devs/agents/documentador/AGENT.md +19 -0
  14. package/.pbai4devs/agents/documentador/antigravity.yaml +8 -0
  15. package/.pbai4devs/agents/documentador/claude.yaml +5 -0
  16. package/.pbai4devs/agents/documentador/cursor.yaml +5 -0
  17. package/.pbai4devs/agents/documentador/gemini.yaml +6 -0
  18. package/.pbai4devs/agents/documentador/github.yaml +5 -0
  19. package/.pbai4devs/agents/documentador/kimi-code.yaml +5 -0
  20. package/.pbai4devs/agents/documentador/mimocode.yaml +8 -0
  21. package/.pbai4devs/agents/documentador/opencode.yaml +11 -0
  22. package/.pbai4devs/agents/implementador/AGENT.md +20 -0
  23. package/.pbai4devs/agents/implementador/antigravity.yaml +8 -0
  24. package/.pbai4devs/agents/implementador/claude.yaml +5 -0
  25. package/.pbai4devs/agents/implementador/cursor.yaml +5 -0
  26. package/.pbai4devs/agents/implementador/gemini.yaml +6 -0
  27. package/.pbai4devs/agents/implementador/github.yaml +5 -0
  28. package/.pbai4devs/agents/implementador/kimi-code.yaml +5 -0
  29. package/.pbai4devs/agents/implementador/mimocode.yaml +8 -0
  30. package/.pbai4devs/agents/implementador/opencode.yaml +11 -0
  31. package/.pbai4devs/agents/planejador/AGENT.md +19 -0
  32. package/.pbai4devs/agents/planejador/antigravity.yaml +8 -0
  33. package/.pbai4devs/agents/planejador/claude.yaml +5 -0
  34. package/.pbai4devs/agents/planejador/cursor.yaml +5 -0
  35. package/.pbai4devs/agents/planejador/gemini.yaml +6 -0
  36. package/.pbai4devs/agents/planejador/github.yaml +5 -0
  37. package/.pbai4devs/agents/planejador/kimi-code.yaml +5 -0
  38. package/.pbai4devs/agents/planejador/mimocode.yaml +8 -0
  39. package/.pbai4devs/agents/planejador/opencode.yaml +11 -0
  40. package/.pbai4devs/agents/validador/AGENT.md +19 -0
  41. package/.pbai4devs/agents/validador/antigravity.yaml +8 -0
  42. package/.pbai4devs/agents/validador/claude.yaml +5 -0
  43. package/.pbai4devs/agents/validador/cursor.yaml +5 -0
  44. package/.pbai4devs/agents/validador/gemini.yaml +6 -0
  45. package/.pbai4devs/agents/validador/github.yaml +5 -0
  46. package/.pbai4devs/agents/validador/kimi-code.yaml +5 -0
  47. package/.pbai4devs/agents/validador/mimocode.yaml +8 -0
  48. package/.pbai4devs/agents/validador/opencode.yaml +11 -0
  49. package/.pbai4devs/commands/README.md +13 -0
  50. package/.pbai4devs/commands/atualizar-documentacao/COMMAND.md +17 -0
  51. package/.pbai4devs/commands/auditar-seguranca/COMMAND.md +25 -0
  52. package/.pbai4devs/commands/implementar-entrega/COMMAND.md +20 -0
  53. package/.pbai4devs/commands/planejar-entrega/COMMAND.md +17 -0
  54. package/.pbai4devs/commands/preparar-seguranca/COMMAND.md +22 -0
  55. package/.pbai4devs/commands/validar-entrega/COMMAND.md +18 -0
  56. package/.pbai4devs/commands/verificar-dependencias/COMMAND.md +18 -0
  57. package/.pbai4devs/commands/verificar-segredos/COMMAND.md +19 -0
  58. package/.pbai4devs/external-skills/CATALOG.json +39 -1
  59. package/.pbai4devs/external-skills/README.md +13 -0
  60. package/.pbai4devs/rules/README.md +4 -0
  61. package/.pbai4devs/rules/documentacao/RULE.md +2 -0
  62. package/.pbai4devs/rules/execucao-local/RULE.md +20 -0
  63. package/.pbai4devs/rules/execucao-local/cursor.yaml +4 -0
  64. package/.pbai4devs/rules/seguranca/RULE.md +19 -0
  65. package/.pbai4devs/rules/seguranca/cursor.yaml +4 -0
  66. package/.pbai4devs/scripts/README.md +1 -0
  67. package/.pbai4devs/scripts/auditar-seguranca/README.md +57 -0
  68. package/.pbai4devs/scripts/auditar-seguranca/SCRIPT.mjs +411 -0
  69. package/.pbai4devs/scripts/auditar-seguranca/database-zip.mjs +143 -0
  70. package/.pbai4devs/scripts/auditar-seguranca/rules/javascript.yml +50 -0
  71. package/.pbai4devs/scripts/auditar-seguranca/tests/SMOKE.mjs +74 -0
  72. package/.pbai4devs/scripts/auditar-seguranca/tests/TEST.mjs +247 -0
  73. package/.pbai4devs/scripts/auditar-seguranca/tests/zip-fixture.mjs +45 -0
  74. package/.pbai4devs/scripts/lib/README.md +4 -0
  75. package/.pbai4devs/scripts/lib/gitignore.mjs +41 -4
  76. package/.pbai4devs/scripts/lib/managed-content.mjs +86 -0
  77. package/.pbai4devs/scripts/lib/managed-toml.mjs +192 -0
  78. package/.pbai4devs/scripts/lib/native-release.mjs +193 -0
  79. package/.pbai4devs/scripts/preparar-ferramentas/README.md +8 -2
  80. package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +46 -16
  81. package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +2 -0
  82. package/.pbai4devs/scripts/preparar-ferramentas/tests/native-release.test.mjs +103 -0
  83. package/.pbai4devs/scripts/sincronizar-agentes/README.md +9 -7
  84. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +133 -38
  85. package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +25 -15
  86. package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +7 -2
  87. package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +3 -3
  88. package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +318 -0
  89. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +47 -26
  90. package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +43 -7
  91. package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +3 -2
  92. package/.pbai4devs/skills/README.md +10 -2
  93. package/.pbai4devs/skills/projeto/preparar-ferramentas/SKILL.md +5 -1
  94. package/.pbai4devs/skills/seguranca/REFERENCE.md +104 -0
  95. package/.pbai4devs/skills/seguranca/corrigir-vulnerabilidade/SKILL.md +27 -0
  96. package/.pbai4devs/skills/seguranca/modelar-ameacas/SKILL.md +25 -0
  97. package/.pbai4devs/skills/seguranca/revisar-autorizacao/SKILL.md +29 -0
  98. package/.pbai4devs/skills/seguranca/revisar-seguranca/SKILL.md +27 -0
  99. package/.pbai4devs/templates/README.md +2 -0
  100. package/.pbai4devs/templates/revisao-seguranca/TEMPLATE.md +33 -0
  101. package/.pbai4devs/tools/README.md +11 -3
  102. package/.pbai4devs/tools/gitleaks/README.md +22 -0
  103. package/.pbai4devs/tools/gitleaks/SCRIPT.mjs +5 -0
  104. package/.pbai4devs/tools/gitleaks/TOOL.json +67 -0
  105. package/.pbai4devs/tools/opengrep/README.md +22 -0
  106. package/.pbai4devs/tools/opengrep/SCRIPT.mjs +5 -0
  107. package/.pbai4devs/tools/opengrep/TOOL.json +71 -0
  108. package/.pbai4devs/tools/osv-scanner/README.md +23 -0
  109. package/.pbai4devs/tools/osv-scanner/SCRIPT.mjs +5 -0
  110. package/.pbai4devs/tools/osv-scanner/TOOL.json +70 -0
  111. package/CHANGELOG.md +17 -0
  112. package/README.en.md +3 -2
  113. package/README.es.md +3 -2
  114. package/README.md +8 -3
  115. package/docs/README.md +6 -3
  116. package/docs/guides/README.md +1 -0
  117. package/docs/guides/cli.md +2 -1
  118. package/docs/guides/installation.md +25 -8
  119. package/docs/guides/resources.md +48 -1
  120. package/docs/guides/security-references.md +97 -0
  121. package/docs/guides/security.md +32 -0
  122. package/docs/guides/troubleshooting.md +8 -5
  123. package/docs/maintainers/maintenance.md +12 -0
  124. package/docs/maintainers/packages.md +2 -2
  125. package/docs/product/design.md +10 -3
  126. package/docs/product/prd.md +5 -1
  127. package/docs/product/roadmap.md +4 -0
  128. package/docs/specs/README.md +4 -0
  129. package/docs/specs/additive-integration/README.md +9 -0
  130. package/docs/specs/additive-integration/SPEC.md +53 -0
  131. package/docs/specs/security-ecosystem/README.md +9 -0
  132. package/docs/specs/security-ecosystem/SPEC.md +53 -0
  133. package/package.json +1 -1
  134. package/src/install.mjs +139 -63
@@ -0,0 +1,74 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { createHash } from 'node:crypto';
6
+ import { SecurityAudit } from '../SCRIPT.mjs';
7
+ import { ToolManager } from '../../preparar-ferramentas/SCRIPT.mjs';
8
+ import { zipFixture } from './zip-fixture.mjs';
9
+
10
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
11
+ const manager = new ToolManager({ root });
12
+ for (const name of ['gitleaks', 'osv-scanner', 'opengrep']) manager.check(name);
13
+ const base = join(root, '.agent-tests/security-smoke'); fs.mkdirSync(base, { recursive: true });
14
+ const work = fs.mkdtempSync(join(base, 'run-'));
15
+ const fixture = join(work, 'project'); fs.mkdirSync(fixture);
16
+ const write = (file, bytes) => { fs.mkdirSync(dirname(file), { recursive: true }); fs.writeFileSync(file, bytes); };
17
+ for (const file of ['scripts/preparar-ferramentas/SCRIPT.mjs', 'scripts/lib/native-release.mjs', 'scripts/lib/runtime.mjs', 'scripts/auditar-seguranca/rules/javascript.yml']) write(join(fixture, '.pbai4devs', file), fs.readFileSync(join(root, '.pbai4devs', file)));
18
+ for (const name of ['gitleaks', 'osv-scanner', 'opengrep']) {
19
+ const source = join(root, '.pbai4devs/tools', name), target = join(fixture, '.pbai4devs/tools', name);
20
+ for (const file of ['TOOL.json', 'SCRIPT.mjs']) write(join(target, file), fs.readFileSync(join(source, file)));
21
+ const current = JSON.parse(fs.readFileSync(join(source, '.local/CURRENT.json')));
22
+ const definition = JSON.parse(fs.readFileSync(join(source, 'TOOL.json')));
23
+ for (const path of Object.values(definition.envPaths || {})) fs.mkdirSync(join(target, '.local', path), { recursive: true });
24
+ write(join(target, '.local/CURRENT.json'), fs.readFileSync(join(source, '.local/CURRENT.json')));
25
+ const version = `${current.version}-${current.platform}`;
26
+ fs.cpSync(join(source, '.local/versions', version), join(target, '.local/versions', version), { recursive: true });
27
+ }
28
+ const syntheticSecret = 'ghp_' + 'a2f80b357bba4272fc7bb1382e82cdb36045';
29
+ write(join(fixture, 'app/index.js'), `import { exec } from 'node:child_process';
30
+ function unsafe(req, db) {
31
+ eval(req.query.code);
32
+ exec(req.body.command);
33
+ db.query('select * from users where id=' + req.params.id);
34
+ const tls = { rejectUnauthorized: false };
35
+ return tls;
36
+ }
37
+ const apiKey = '${syntheticSecret}';
38
+ `);
39
+ const lock = version => JSON.stringify({ name: 'fixture', version: '1.0.0', lockfileVersion: 3, packages: { '': { name: 'fixture', version: '1.0.0', dependencies: { 'security-fixture-lib': version } }, 'node_modules/security-fixture-lib': { version, resolved: `https://registry.npmjs.org/security-fixture-lib/-/security-fixture-lib-${version}.tgz` } } });
40
+ write(join(fixture, 'app/package-lock.json'), lock('1.0.0'));
41
+ const audit = new SecurityAudit({ root: fixture });
42
+ const vulnerability = { schema_version: '1.6.0', id: 'TEST-2099-0001', modified: '2025-01-01T00:00:00Z', published: '2025-01-01T00:00:00Z', summary: 'Synthetic vulnerability used only by local tests', affected: [{ package: { ecosystem: 'npm', name: 'security-fixture-lib' }, ranges: [{ type: 'SEMVER', events: [{ introduced: '0' }, { fixed: '1.0.1' }] }], versions: ['1.0.0'] }] };
43
+ const archive = zipFixture({ 'TEST-2099-0001.json': JSON.stringify(vulnerability) });
44
+ audit.fetcher = async () => new Response(archive);
45
+ await audit.updateData(['npm']);
46
+ const digest = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
47
+ const before = digest(join(fixture, 'app/index.js'));
48
+ const bad = await audit.run({ target: 'app' });
49
+ write(join(work, 'unsafe-report.json'), JSON.stringify(bad, null, 2));
50
+ assert.equal(bad.status, 'findings', JSON.stringify(bad.checks));
51
+ for (const check of bad.checks) assert.equal(check.status, 'findings', check.tool);
52
+ assert.equal(digest(join(fixture, 'app/index.js')), before);
53
+ assert.ok(!JSON.stringify(bad).includes(syntheticSecret));
54
+ assert.ok(bad.findings.some(finding => finding.rule === 'TEST-2099-0001'));
55
+ for (const id of ['pbai-eval-input', 'pbai-shell-input', 'pbai-sql-input', 'pbai-tls-verification-disabled']) assert.ok(bad.findings.some(finding => finding.rule === id), id);
56
+ write(join(fixture, 'app/index.js'), `import { execFile } from 'node:child_process';
57
+ function safe(req, db) {
58
+ JSON.parse(req.query.data);
59
+ execFile('printf', ['%s', req.body.message]);
60
+ db.query('select * from users where id = ?', [req.params.id]);
61
+ return { rejectUnauthorized: true };
62
+ }
63
+ `);
64
+ write(join(fixture, 'app/package-lock.json'), lock('1.0.1'));
65
+ const good = await audit.run({ target: 'app' });
66
+ write(join(work, 'safe-report.json'), JSON.stringify(good, null, 2));
67
+ assert.equal(good.status, 'passed', JSON.stringify(good.checks)); assert.equal(good.findings.length, 0);
68
+ write(join(fixture, 'app/package-lock.json'), JSON.stringify({ name: 'fixture', version: '1.0.0', lockfileVersion: 3, packages: { '': { name: 'fixture', version: '1.0.0' } } }));
69
+ const empty = await audit.run({ target: 'app', checks: ['dependencies'] });
70
+ write(join(work, 'no-dependencies-report.json'), JSON.stringify(empty, null, 2));
71
+ assert.equal(empty.status, 'not-applicable', JSON.stringify(empty.checks));
72
+ const report = { status: 'passed', work, tools: bad.checks.map(item => ({ tool: item.tool, version: item.version })), positiveFindings: bad.findings.length, negativeFindings: good.findings.length, noDependencies: empty.status, offline: true, sourcePreserved: true };
73
+ write(join(work, 'REPORT.json'), JSON.stringify(report, null, 2));
74
+ console.log(JSON.stringify(report));
@@ -0,0 +1,247 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { SecurityAudit, parseArgs, parseGitleaks, parseOsv, parseOpengrep, sanitizedEnvironment, safePath, snapshotSources, verifyDatabase } from '../SCRIPT.mjs';
8
+ import { zipFixture } from './zip-fixture.mjs';
9
+ import { validateOsvDatabase } from '../database-zip.mjs';
10
+
11
+ const write = (path, text) => { fs.mkdirSync(dirname(path), { recursive: true }); fs.writeFileSync(path, text); };
12
+ const toolPath = args => args[0].replaceAll('\\', '/');
13
+ const advisory = (id = 'TEST-1', extra = {}) => JSON.stringify({ id, modified: '2026-01-01T00:00:00Z', affected: [], ...extra });
14
+ const validZip = () => zipFixture({ 'TEST-1.json': advisory() });
15
+ function fixture(t) {
16
+ const root = fs.realpathSync(fs.mkdtempSync(join(tmpdir(), 'pbai-security-')));
17
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
18
+ assert.equal(spawnSync('git', ['init', '--quiet', root]).status, 0);
19
+ write(join(root, '.gitignore'), '/.agent-tests/\n');
20
+ write(join(root, 'app/index.js'), 'export const answer = 42;\n');
21
+ write(join(root, '.pbai4devs/scripts/auditar-seguranca/rules/javascript.yml'), 'rules: []\n');
22
+ for (const name of ['gitleaks', 'osv-scanner', 'opengrep']) write(join(root, `.pbai4devs/tools/${name}/SCRIPT.mjs`), '// fixture\n');
23
+ const manager = { root, check: () => ({ version: '1.0.0' }), entry: () => ({ envPaths: { OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY: 'data/osv' } }), local: () => join(root, '.pbai4devs/tools/osv-scanner/.local'), withLock: (_name, action) => action() };
24
+ const calls = [];
25
+ const execute = (_node, args, options) => {
26
+ calls.push({ args, options });
27
+ const payload = toolPath(args).includes('/gitleaks/') ? [] : toolPath(args).includes('/osv-scanner/') ? { results: [] }
28
+ : { results: [], errors: [], paths: { scanned: [join(options.cwd, 'source/index.js')] } };
29
+ const output = args.find(arg => arg.startsWith('--report-path=') || arg.startsWith('--output-file='));
30
+ if (output) fs.writeFileSync(output.slice(output.indexOf('=') + 1), JSON.stringify(payload));
31
+ return { status: 0, stdout: JSON.stringify(payload), stderr: '' };
32
+ };
33
+ return { root, manager, calls, execute, audit: new SecurityAudit({ root, manager, execute }) };
34
+ }
35
+
36
+ test('argumentos limitam checks, destinos e operacoes', () => {
37
+ assert.equal(parseArgs([]).command, 'help');
38
+ assert.deepEqual(parseArgs(['run', '--checks', 'sast,secrets,secrets']).checks, ['sast', 'secrets']);
39
+ assert.deepEqual(parseArgs(['update-data', '--ecosystems', 'NPM,go']).ecosystems, ['npm', 'Go']);
40
+ for (const args of [['scan'], ['run', '--checks', 'cloud'], ['run', '--checks', ''], ['check', '--target', '.'], ['run', '--ecosystems', 'npm'], ['update-data'], ['update-data', '--ecosystems', '../../etc'], ['run', '--target', '.', '--target', '.'], ['run', '--checks', 'sast', '--include-history']]) assert.throws(() => parseArgs(args));
41
+ });
42
+
43
+ test('ambiente nao encaminha tokens, proxies, Node options ou configuracoes externas', () => {
44
+ const env = sanitizedEnvironment({ PATH: '/bin', HOME: '/home/person', SystemRoot: 'C:\\Windows', AWS_ACCESS_KEY_ID: 'secret', GOOGLE_APPLICATION_CREDENTIALS: 'secret', SECRET: 'secret', NPM_TOKEN: 'secret', NODE_OPTIONS: '--require malicious', HTTPS_PROXY: 'secret', SEMGREP_RULES: 'https://remote', GITLEAKS_CONFIG: 'secret' });
45
+ assert.equal(env.PATH, '/bin'); assert.equal(env.HOME, '/home/person'); assert.equal(env.SystemRoot, 'C:\\Windows');
46
+ assert.ok(!JSON.stringify(env).includes('secret')); assert.equal(env.NODE_OPTIONS, undefined); assert.equal(env.SEMGREP_RULES, undefined);
47
+ assert.equal(env.OPENGREP_ENABLE_VERSION_CHECK, '0');
48
+ });
49
+
50
+ test('caminhos recusam escapes, links e output rastreado antes de executar', async t => {
51
+ const f = fixture(t); const outside = fs.mkdtempSync(join(tmpdir(), 'pbai-outside-')); t.after(() => fs.rmSync(outside, { recursive: true, force: true }));
52
+ fs.symlinkSync(outside, join(f.root, 'link'), process.platform === 'win32' ? 'junction' : undefined);
53
+ assert.throws(() => safePath(f.root, '../outside')); assert.throws(() => safePath(f.root, 'link/file.json'));
54
+ await assert.rejects(f.audit.run({ target: 'app', checks: ['secrets'], output: 'report.json' }));
55
+ fs.mkdirSync(join(f.root, '.agent-tests/security'), { recursive: true });
56
+ await assert.rejects(f.audit.run({ target: '.agent-tests', checks: ['secrets'] }), /conflita/);
57
+ await assert.rejects(f.audit.run({ target: '.agent-tests/security', checks: ['secrets'] }), /conflita/);
58
+ write(join(f.root, '.agent-tests/tracked.json'), '{}');
59
+ assert.equal(spawnSync('git', ['-C', f.root, 'add', '-f', '.agent-tests/tracked.json']).status, 0);
60
+ await assert.rejects(f.audit.run({ target: 'app', checks: ['secrets'], output: '.agent-tests/tracked.json' }));
61
+ assert.equal(f.calls.length, 0);
62
+ });
63
+
64
+ test('snapshot preserva originais e nao atravessa links nem diretorios gerados', t => {
65
+ const f = fixture(t); write(join(f.root, 'app/node_modules/excluded.js'), 'secret'); write(join(f.root, 'app/.semgrepignore'), '*');
66
+ fs.symlinkSync(join(f.root, 'app'), join(f.root, 'app/link'), process.platform === 'win32' ? 'junction' : 'dir');
67
+ const destination = join(f.root, '.agent-tests/snapshot'); fs.mkdirSync(destination, { recursive: true });
68
+ const data = snapshotSources(f.root, join(f.root, 'app'), destination);
69
+ assert.deepEqual(data.files.map(file => file.path), ['index.js']);
70
+ assert.equal(data.excluded.length, 3); assert.equal(fs.readFileSync(join(f.root, 'app/index.js'), 'utf8'), fs.readFileSync(join(destination, 'index.js'), 'utf8'));
71
+ assert.throws(() => snapshotSources(f.root, join(f.root, 'app'), destination, { maxFileBytes: 1 }));
72
+ });
73
+
74
+ test('parsers redigem segredos e recusam saidas invalidas ou falsas aprovacoes', () => {
75
+ const secret = 'a-secret-value-that-must-not-escape';
76
+ const gitleaks = parseGitleaks([{ RuleID: 'key', File: 'a.js', StartLine: 3, Secret: secret, Match: secret, Line: secret, Description: secret }], 1, '/tmp');
77
+ assert.equal(gitleaks.status, 'findings'); assert.ok(!JSON.stringify(gitleaks).includes(secret));
78
+ assert.throws(() => parseGitleaks([], 1, '/tmp')); assert.throws(() => parseGitleaks([], 2, '/tmp')); assert.throws(() => parseGitleaks([{ RuleID: 'x', File: '../escape' }], 1, '/tmp'));
79
+ assert.throws(() => parseGitleaks([{ RuleID: 'x', File: 'src/../../escape' }], 1, '/tmp'));
80
+ assert.equal(parseOsv({ results: [] }, 128, '/tmp').status, 'not-applicable'); assert.throws(() => parseOsv({ results: [] }, 127, '/tmp')); assert.throws(() => parseOsv({ results: [] }, 1, '/tmp'));
81
+ const osv = parseOsv({ results: [{ source: { path: 'package-lock.json' }, packages: [{ package: { name: 'demo', version: '1.0.0', ecosystem: 'npm' }, vulnerabilities: [{ id: 'TEST-1', details: secret }] }] }] }, 1, '/tmp');
82
+ assert.equal(osv.findings.length, 1); assert.ok(!JSON.stringify(osv).includes(secret));
83
+ const sast = parseOpengrep({ results: [{ check_id: 'pbai-eval-input', path: 'a.js', start: { line: 2 }, extra: { lines: secret, message: secret } }], errors: [], paths: { scanned: ['a.js'] } }, 1, '/tmp');
84
+ assert.equal(sast.findings.length, 1); assert.ok(!JSON.stringify(sast).includes(secret));
85
+ assert.throws(() => parseOpengrep({ results: [], errors: [{}], paths: { scanned: [] } }, 0, '/tmp'));
86
+ assert.throws(() => parseOpengrep({ results: [], errors: [] }, 0, '/tmp'));
87
+ });
88
+
89
+ test('Opengrep usa IDs e fingerprints estaveis sem caminhos de Linux, Windows ou macOS', () => {
90
+ const rules = ['pbai-eval-input', 'pbai-shell-input', 'pbai-sql-input', 'pbai-tls-verification-disabled'];
91
+ const prefixes = ['', 'home.runner.work.AgentDevKit.AgentDevKit..agent-tests.security-smoke.run-eu1Fdm..pbai4devs.scripts.auditar-seguranca.rules.',
92
+ 'D.a.AgentDevKit.AgentDevKit..agent-tests.security-smoke.run-TpubGc..pbai4devs.scripts.auditar-seguranca.rules.',
93
+ 'Users.person.Library.Project..agent-tests.security-smoke.run-macABC..pbai4devs.scripts.auditar-seguranca.rules.'];
94
+ const reports = prefixes.map(prefix => parseOpengrep({
95
+ results: rules.map((rule, index) => ({ check_id: `${prefix}${rule}`, path: 'src/handler.js', start: { line: index + 1 }, extra: { severity: 'ERROR' } })),
96
+ errors: [], paths: { scanned: ['src/handler.js'] },
97
+ }, 1, '/unused'));
98
+ for (const report of reports) {
99
+ assert.deepEqual(report, reports[0]);
100
+ assert.deepEqual(report.findings.map(item => item.rule), rules);
101
+ assert.equal(new Set(report.findings.map(item => item.fingerprint)).size, rules.length);
102
+ assert.doesNotMatch(JSON.stringify(report), /runner|person|run-eu1Fdm|run-TpubGc|run-macABC|AgentDevKit|pbai4devs|D\.a\./);
103
+ }
104
+ });
105
+
106
+ test('Opengrep rejeita regras desconhecidas e semelhantes sem divulgar o ID bruto', () => {
107
+ const unknown = ['unknown', 'pbai-eval-input-extra', 'prefix.pbai-eval-input.extra', 'prefix.pbai-unknown', '/private/person/pbai-eval-input', ''];
108
+ for (const check_id of unknown) {
109
+ assert.throws(() => parseOpengrep({ results: [{ check_id, path: 'src/handler.js', start: { line: 1 } }], errors: [], paths: { scanned: ['src/handler.js'] } }, 1, '/unused'),
110
+ error => error.message === 'Identificador de regra Opengrep desconhecido.' && error.publicMessage === error.message);
111
+ }
112
+ });
113
+
114
+ test('regra Opengrep desconhecida torna a auditoria incompleta sem vazar caminho local', async t => {
115
+ const f = fixture(t), check_id = 'Users.private-person.project.run-private.rules.unrecognized';
116
+ f.audit.execute = () => ({ status: 1, stdout: JSON.stringify({ results: [{ check_id, path: 'index.js', start: { line: 1 } }], errors: [], paths: { scanned: ['index.js'] } }), stderr: '' });
117
+ const report = await f.audit.run({ target: 'app', checks: ['sast'] });
118
+ assert.equal(report.status, 'incomplete'); assert.equal(report.exitCode, 2);
119
+ assert.deepEqual(report.findings, []); assert.equal(report.checks[0].status, 'not-run');
120
+ assert.doesNotMatch(JSON.stringify(report), /private-person|run-private|unrecognized/);
121
+ });
122
+
123
+ test('run usa argumentos locais e fecha incompleto quando ferramenta ou DB falta', async t => {
124
+ const f = fixture(t);
125
+ const result = await f.audit.run({ target: 'app', checks: ['secrets', 'sast'] });
126
+ assert.equal(result.status, 'passed'); assert.equal(result.exitCode, 0);
127
+ assert.ok(f.calls.every(call => call.options.shell === false && call.options.timeout > 0));
128
+ assert.ok(f.calls.some(call => call.args.includes('--disable-version-check') && call.args.includes('--no-git-ignore')));
129
+ assert.ok(f.calls.some(call => call.args.includes('--redact=100')));
130
+ assert.ok(!f.calls.some(call => call.args.some(arg => /metrics|https:\/\/|--autofix/.test(arg))));
131
+ assert.ok(fs.existsSync(join(f.root, result.report))); assert.ok(!fs.readdirSync(join(f.root, '.agent-tests/security')).some(name => name.startsWith('.work-')));
132
+ write(join(f.root, 'app/package-lock.json'), '{}');
133
+ const missingDb = await f.audit.run({ target: 'app', checks: ['dependencies'] });
134
+ assert.equal(missingDb.status, 'incomplete'); assert.equal(missingDb.exitCode, 2);
135
+ f.manager.check = () => { throw Error('Missing TOKEN=secret'); };
136
+ const missingTool = await f.audit.run({ target: 'app', checks: ['secrets'] });
137
+ assert.equal(missingTool.status, 'incomplete'); assert.ok(!JSON.stringify(missingTool).includes('TOKEN'));
138
+ });
139
+
140
+ test('alteracao da fonte durante a ferramenta invalida o resultado', async t => {
141
+ const f = fixture(t), execute = f.execute;
142
+ f.audit.execute = (...args) => { const result = execute(...args); write(join(f.root, 'app/index.js'), 'changed'); return result; };
143
+ const result = await f.audit.run({ target: 'app', checks: ['secrets'] });
144
+ assert.equal(result.status, 'incomplete'); assert.equal(result.exitCode, 2);
145
+ });
146
+
147
+ test('saida quebrada da ferramenta nao vira relatorio limpo', async t => {
148
+ const f = fixture(t); f.audit.execute = () => ({ status: 2, stdout: 'secret', stderr: 'secret' });
149
+ const result = await f.audit.run({ target: 'app', checks: ['sast'] });
150
+ assert.equal(result.exitCode, 2); assert.ok(!JSON.stringify(result).includes('secret'));
151
+ });
152
+
153
+ test('bases OSV atualizam explicitamente, validam hash e preservam anterior em falha', async t => {
154
+ const f = fixture(t); const zip = validZip(); let calls = 0;
155
+ f.audit.fetcher = async (url, options) => { calls++; assert.equal(url, 'https://osv-vulnerabilities.storage.googleapis.com/npm/all.zip'); assert.equal(options.redirect, 'error'); assert.equal(options.credentials, 'omit'); assert.deepEqual(Object.keys(options.headers), ['Accept']); return new Response(zip); };
156
+ assert.equal((await f.audit.updateData(['npm'])).status, 'passed');
157
+ const cache = join(f.manager.local(), 'data/osv'); const before = verifyDatabase(f.root, cache, 'npm'); assert.equal(calls, 1);
158
+ f.audit.fetcher = async () => new Response('broken'); await assert.rejects(f.audit.updateData(['npm']));
159
+ assert.deepEqual(verifyDatabase(f.root, cache, 'npm'), before);
160
+ const fake = Buffer.alloc(22); fake.writeUInt32LE(0x04034b50); fake.writeUInt32LE(0x06054b50, 4);
161
+ f.audit.fetcher = async () => new Response(fake); await assert.rejects(f.audit.updateData(['npm']));
162
+ assert.deepEqual(verifyDatabase(f.root, cache, 'npm'), before);
163
+ f.audit.fetcher = async () => new Response(zipFixture({ 'TEST-1.json': '{}' })); await assert.rejects(f.audit.updateData(['npm']));
164
+ assert.deepEqual(verifyDatabase(f.root, cache, 'npm'), before);
165
+ fs.appendFileSync(join(cache, 'osv-scalibr/npm/all.zip'), 'tampered'); assert.throws(() => verifyDatabase(f.root, cache, 'npm'));
166
+ });
167
+
168
+ test('query check nao escreve relatorios nem instala ferramentas', t => {
169
+ const f = fixture(t); const before = fs.readdirSync(f.root);
170
+ const result = f.audit.check(['secrets', 'sast']); assert.equal(result.exitCode, 0); assert.deepEqual(fs.readdirSync(f.root), before); assert.equal(f.calls.length, 0);
171
+ });
172
+
173
+ test('historico nunca amplia silenciosamente um target de subpasta', async t => {
174
+ const f = fixture(t);
175
+ await assert.rejects(f.audit.run({ target: 'app', checks: ['secrets'], includeHistory: true }), /raiz do repositorio/);
176
+ assert.equal(f.calls.length, 0);
177
+ });
178
+
179
+ test('bundles de texto continuam incluidos no snapshot de segredos', t => {
180
+ const f = fixture(t); write(join(f.root, 'app/dist/app.js'), 'bundle'); write(join(f.root, 'app/build/server.js'), 'server');
181
+ const destination = join(f.root, '.agent-tests/snapshot'); fs.mkdirSync(destination, { recursive: true });
182
+ const data = snapshotSources(f.root, join(f.root, 'app'), destination);
183
+ assert.ok(data.files.some(file => file.path === 'dist/app.js')); assert.ok(data.files.some(file => file.path === 'build/server.js'));
184
+ });
185
+
186
+ test('OSV recebe modo offline estrito e erro de outro scanner prevalece sobre achados', async t => {
187
+ const f = fixture(t); const zip = validZip(); f.audit.fetcher = async () => new Response(zip); await f.audit.updateData(['npm']);
188
+ write(join(f.root, 'app/package-lock.json'), '{}');
189
+ const execute = f.execute;
190
+ f.audit.execute = (node, args, options) => {
191
+ if (toolPath(args).includes('/osv-scanner/')) {
192
+ assert.ok(args.includes('--offline')); assert.ok(args.includes('--no-resolve')); assert.ok(args.includes('--all-packages'));
193
+ assert.ok(!args.some(arg => arg.includes('call-analysis') || arg.includes('licenses')));
194
+ }
195
+ return execute(node, args, options);
196
+ };
197
+ const result = await f.audit.run({ target: 'app', checks: ['dependencies'] }); assert.equal(result.status, 'not-applicable');
198
+ f.audit.execute = (_node, args) => {
199
+ if (toolPath(args).includes('/gitleaks/')) { const output = args.find(arg => arg.startsWith('--report-path=')); fs.writeFileSync(output.slice(output.indexOf('=') + 1), JSON.stringify([{ RuleID: 'key', File: 'index.js', StartLine: 1 }])); return { status: 1, stdout: '', stderr: '' }; }
200
+ return { status: 2, stdout: '{}', stderr: 'failure' };
201
+ };
202
+ const mixed = await f.audit.run({ target: 'app', checks: ['secrets', 'sast'] }); assert.equal(mixed.exitCode, 2); assert.equal(mixed.findings.length, 1);
203
+ });
204
+
205
+ test('OSV sem pacotes usa 128 explicito; erro sem JSON permanece incompleto', async t => {
206
+ const f = fixture(t); const zip = validZip(); f.audit.fetcher = async () => new Response(zip); await f.audit.updateData(['npm']);
207
+ write(join(f.root, 'app/package-lock.json'), '{}');
208
+ f.audit.execute = () => ({ status: 128, stdout: '', stderr: '' });
209
+ assert.equal((await f.audit.run({ target: 'app', checks: ['dependencies'] })).status, 'not-applicable');
210
+ f.audit.execute = () => ({ status: 127, stdout: '', stderr: '' });
211
+ assert.equal((await f.audit.run({ target: 'app', checks: ['dependencies'] })).status, 'incomplete');
212
+ });
213
+
214
+ test('ZIP e ZIP64 exigem estrutura, CRC, JSON OSV e limites validos', () => {
215
+ for (const options of [{}, { stored: true }, { descriptor: true }, { zip64: true }, { zip64: true, descriptor: true }]) {
216
+ const zip = zipFixture({ 'TEST-1.json': advisory(), 'TEST-2.json': advisory('TEST-2', { withdrawn: '2026-02-01T00:00:00Z', affected: undefined }) }, options);
217
+ assert.equal(validateOsvDatabase(zip).records, 2);
218
+ }
219
+ const zero = Buffer.alloc(22); zero.writeUInt32LE(0x04034b50); zero.writeUInt32LE(0x06054b50, 4);
220
+ assert.throws(() => validateOsvDatabase(zero));
221
+ assert.throws(() => validateOsvDatabase(zipFixture({ 'TEST-1.json': 'invalid' })));
222
+ assert.throws(() => validateOsvDatabase(zipFixture({ 'TEST-1.json': '{}' })));
223
+ assert.throws(() => validateOsvDatabase(zipFixture({ '../TEST-1.json': advisory() })));
224
+ assert.throws(() => validateOsvDatabase(zipFixture([['TEST-1.json', advisory()], ['TEST-1.json', advisory()]])));
225
+ const brokenCrc = validZip(); const central = brokenCrc.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02]));
226
+ brokenCrc.writeUInt32LE(1, central + 16); assert.throws(() => validateOsvDatabase(brokenCrc));
227
+ const brokenPayload = validZip(); const payload = 30 + brokenPayload.readUInt16LE(26) + brokenPayload.readUInt16LE(28); brokenPayload[payload] ^= 0xff;
228
+ assert.throws(() => validateOsvDatabase(brokenPayload));
229
+ assert.throws(() => validateOsvDatabase(validZip(), { maxEntryBytes: 2 }));
230
+ assert.throws(() => validateOsvDatabase(validZip(), { maxTotalBytes: 2 }));
231
+ });
232
+
233
+ test('inventario final recusa adicao, remocao, renomeacao e mudanca de tipo', async t => {
234
+ const changes = {
235
+ addition: root => write(join(root, 'app/added-during-scan.js'), 'new'),
236
+ removal: root => fs.unlinkSync(join(root, 'app/index.js')),
237
+ rename: root => fs.renameSync(join(root, 'app/index.js'), join(root, 'app/renamed.js')),
238
+ type: root => { fs.unlinkSync(join(root, 'app/index.js')); fs.mkdirSync(join(root, 'app/index.js')); },
239
+ directory: root => fs.mkdirSync(join(root, 'app/new-folder')),
240
+ };
241
+ for (const [name, change] of Object.entries(changes)) await t.test(name, async subtest => {
242
+ const f = fixture(subtest), execute = f.execute;
243
+ f.audit.execute = (...args) => { const result = execute(...args); change(f.root); return result; };
244
+ const result = await f.audit.run({ target: 'app', checks: ['secrets'] });
245
+ assert.equal(result.status, 'incomplete'); assert.equal(result.exitCode, 2);
246
+ });
247
+ });
@@ -0,0 +1,45 @@
1
+ import { deflateRawSync } from 'node:zlib';
2
+
3
+ const crc32 = bytes => {
4
+ let crc = 0xffffffff;
5
+ for (const byte of bytes) {
6
+ crc ^= byte;
7
+ for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
8
+ }
9
+ return (crc ^ 0xffffffff) >>> 0;
10
+ };
11
+
12
+ // Minimal deterministic ZIP builder for synthetic OSV database fixtures.
13
+ export function zipFixture(entries, { zip64 = false, descriptor = false, stored = false } = {}) {
14
+ const files = [], index = []; let offset = 0;
15
+ const rows = Array.isArray(entries) ? entries : Object.entries(entries);
16
+ for (const [name, text] of rows) {
17
+ const filename = Buffer.from(name), bytes = Buffer.from(text), zipped = stored ? bytes : deflateRawSync(bytes), crc = crc32(bytes);
18
+ const localExtra = Buffer.alloc(zip64 ? 20 : 0), centralExtra = Buffer.alloc(zip64 ? 28 : 0);
19
+ if (zip64) {
20
+ localExtra.writeUInt16LE(1); localExtra.writeUInt16LE(16, 2); localExtra.writeBigUInt64LE(BigInt(bytes.length), 4); localExtra.writeBigUInt64LE(BigInt(zipped.length), 12);
21
+ centralExtra.writeUInt16LE(1); centralExtra.writeUInt16LE(24, 2); centralExtra.writeBigUInt64LE(BigInt(bytes.length), 4); centralExtra.writeBigUInt64LE(BigInt(zipped.length), 12); centralExtra.writeBigUInt64LE(BigInt(offset), 20);
22
+ }
23
+ const trailer = Buffer.alloc(descriptor ? zip64 ? 24 : 16 : 0);
24
+ if (descriptor) {
25
+ trailer.writeUInt32LE(0x08074b50); trailer.writeUInt32LE(crc, 4);
26
+ if (zip64) { trailer.writeBigUInt64LE(BigInt(zipped.length), 8); trailer.writeBigUInt64LE(BigInt(bytes.length), 16); }
27
+ else { trailer.writeUInt32LE(zipped.length, 8); trailer.writeUInt32LE(bytes.length, 12); }
28
+ }
29
+ const local = Buffer.alloc(30); local.writeUInt32LE(0x04034b50); local.writeUInt16LE(zip64 ? 45 : 20, 4); local.writeUInt16LE(descriptor ? 8 : 0, 6); local.writeUInt16LE(stored ? 0 : 8, 8);
30
+ local.writeUInt32LE(descriptor ? 0 : crc, 14); local.writeUInt32LE(zip64 ? 0xffffffff : descriptor ? 0 : zipped.length, 18); local.writeUInt32LE(zip64 ? 0xffffffff : descriptor ? 0 : bytes.length, 22); local.writeUInt16LE(filename.length, 26); local.writeUInt16LE(localExtra.length, 28);
31
+ files.push(local, filename, localExtra, zipped, trailer);
32
+ const central = Buffer.alloc(46); central.writeUInt32LE(0x02014b50); central.writeUInt16LE(zip64 ? 45 : 20, 4); central.writeUInt16LE(zip64 ? 45 : 20, 6); central.writeUInt16LE(descriptor ? 8 : 0, 8); central.writeUInt16LE(stored ? 0 : 8, 10);
33
+ central.writeUInt32LE(crc, 16); central.writeUInt32LE(zip64 ? 0xffffffff : zipped.length, 20); central.writeUInt32LE(zip64 ? 0xffffffff : bytes.length, 24); central.writeUInt16LE(filename.length, 28); central.writeUInt16LE(centralExtra.length, 30); central.writeUInt32LE(zip64 ? 0xffffffff : offset, 42);
34
+ index.push(central, filename, centralExtra); offset += local.length + filename.length + localExtra.length + zipped.length + trailer.length;
35
+ }
36
+ const table = Buffer.concat(index), end = Buffer.alloc(22); end.writeUInt32LE(0x06054b50);
37
+ end.writeUInt16LE(zip64 ? 0xffff : rows.length, 8); end.writeUInt16LE(zip64 ? 0xffff : rows.length, 10); end.writeUInt32LE(zip64 ? 0xffffffff : table.length, 12); end.writeUInt32LE(zip64 ? 0xffffffff : offset, 16);
38
+ const tail = [];
39
+ if (zip64) {
40
+ const wide = Buffer.alloc(56), locator = Buffer.alloc(20); wide.writeUInt32LE(0x06064b50); wide.writeBigUInt64LE(44n, 4); wide.writeUInt16LE(45, 12); wide.writeUInt16LE(45, 14);
41
+ wide.writeBigUInt64LE(BigInt(rows.length), 24); wide.writeBigUInt64LE(BigInt(rows.length), 32); wide.writeBigUInt64LE(BigInt(table.length), 40); wide.writeBigUInt64LE(BigInt(offset), 48);
42
+ locator.writeUInt32LE(0x07064b50); locator.writeBigUInt64LE(BigInt(offset + table.length), 8); locator.writeUInt32LE(1, 16); tail.push(wide, locator);
43
+ }
44
+ return Buffer.concat([...files, table, ...tail, end]);
45
+ }
@@ -7,3 +7,7 @@ description: Utilitários mínimos compartilhados das automações Node.js.
7
7
  `runtime.mjs` centraliza o requisito Node.js 24+, a raiz do repositório e a
8
8
  identificação do arquivo executado. Não instala runtimes nem configura a máquina.
9
9
  Também procura executáveis opcionais no PATH sem iniciar um shell.
10
+
11
+ `managed-content.mjs` limita a propriedade do kit aos seus blocos Markdown;
12
+ `managed-toml.mjs` aplica o mesmo contrato ao `prompt` de comandos Gemini.
13
+ Texto externo, nomes existentes e metadados humanos permanecem preservados.
@@ -125,6 +125,34 @@ function artifactPolicy(templateFile) {
125
125
  return patterns;
126
126
  }
127
127
 
128
+ function privateClientPolicy(templateFile, roots) {
129
+ const adjacent = join(dirname(templateFile), 'agents.ignore');
130
+ const source = checkedLocalPath(localStat(adjacent) ? adjacent : fileURLToPath(new URL('../../templates/gitignore/agents.ignore', import.meta.url)));
131
+ const section = readFileSync(source, 'utf8').split(/^## Estado local, caches e credenciais conhecidas\r?$/m)[1]?.split(/^## /m)[0];
132
+ const patterns = section?.split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith('#'));
133
+ if (!patterns?.length || patterns.some(pattern => !/^(?:!?\*\*\/|\/)/.test(pattern) || /[\\\x00-\x1f]/.test(pattern) || pattern.split('/').includes('..'))) throw new Error('Secao de estado local invalida em agents.ignore.');
134
+ const rules = [], probes = [];
135
+ for (const root of roots) for (const pattern of patterns) {
136
+ const negative = pattern.startsWith('!'), rule = pattern.replace(/^!/, '').replace(/^\//, '');
137
+ rules.push(`${negative ? '!' : ''}/${escapePattern(root)}/${rule}`);
138
+ if (negative) continue;
139
+ const stem = rule.replace(/^\*\*\//, '').replace(/\[([^\]]+)\]/g, (_match, letters) => letters[0]).replace(/\*/g, 'agentdevkit-private-probe').replace(/\?/g, 'x');
140
+ probes.push(`${root}/${stem}${stem.endsWith('/') ? '.agentdevkit-ignore-probe' : ''}`);
141
+ if (rule.startsWith('**/')) probes.push(`${root}/.agentdevkit-ignore-probes/${stem}${stem.endsWith('/') ? '.agentdevkit-ignore-probe' : ''}`);
142
+ }
143
+ return { rules, probes };
144
+ }
145
+
146
+ function safeIgnoreProbe(root, path) {
147
+ let parent = root;
148
+ for (const part of path.split('/').slice(0, -1)) {
149
+ parent = join(parent, part);
150
+ const item = localStat(parent);
151
+ if (item && !item.isDirectory()) return false;
152
+ }
153
+ return true;
154
+ }
155
+
128
156
  function artifactProbes(root, patterns) {
129
157
  const probes = new Set();
130
158
  for (const pattern of patterns) {
@@ -233,10 +261,14 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
233
261
  const artifacts = artifactPolicy(templateFile);
234
262
  if (template.split('{{OUTPUTS}}').length !== 2) throw new Error('local.ignore exige um unico {{OUTPUTS}}.');
235
263
  if (template.split('{{ROOTS}}').length > 2) throw new Error('local.ignore permite um unico {{ROOTS}}.');
264
+ // A client directory can also contain human instructions and settings. Only
265
+ // whole files owned by the kit belong in the local output exclusion list.
266
+ const roots = [];
236
267
  const localPolicy = join(templateRoot || root, '.pbai4devs/templates/clients/ADAPTERS.json');
237
268
  const policy = JSON.parse(readFileSync(checkedLocalPath(localStat(localPolicy) ? localPolicy : fileURLToPath(new URL('../../templates/clients/ADAPTERS.json', import.meta.url))), 'utf8'));
238
- const roots = [...new Set(policy.adapters.flatMap(adapter => [adapter.root, ...(adapter.extraRoots || [])]))].filter(path => !['.github', '.vscode'].includes(path));
239
- if (roots.some(path => typeof path !== 'string' || !/^\.[a-z0-9][a-z0-9-]*$/.test(path) || ['.git', '.pbai4devs', '.agent-tests'].includes(path))) throw new Error('Raiz de cliente invalida para exclusao.');
269
+ const clientRoots = [...new Set(policy.adapters.flatMap(adapter => [adapter.root, ...(adapter.extraRoots || [])]))];
270
+ if (clientRoots.some(path => typeof path !== 'string' || !/^\.[a-z0-9][a-z0-9-]*$/.test(path) || ['.git', '.pbai4devs', '.agent-tests'].includes(path))) throw new Error('Raiz de cliente invalida para exclusao.');
271
+ const privateClients = privateClientPolicy(templateFile, clientRoots);
240
272
  const paths = [...new Set([...outputs.map(checkedOutput), ...template.split(/\r?\n/).filter(line => line.startsWith('/')).map(line => checkedOutput(line.slice(1)))])].sort();
241
273
  const info = gitCommand(resolved.parent, ['rev-parse', '--show-toplevel', '--git-path', 'info/exclude']);
242
274
  let location = null;
@@ -256,13 +288,18 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
256
288
  const id = createHash('sha256').update(root).digest('hex').slice(0, 16);
257
289
  const first = `# BEGIN AGENTDEVKIT LOCAL ${id}`, last = `# END AGENTDEVKIT LOCAL ${id}`;
258
290
  const fixed = template.replace('{{ROOTS}}', roots.map(path => `/${path}/`).join('\n')).replace('{{OUTPUTS}}', paths.filter(path => !roots.some(root => path === root || path.startsWith(root + '/'))).map(path => `/${escapePattern(path)}`).join('\n'))
291
+ + '\n# Estado privado dos clientes\n' + privateClients.rules.join('\n') + '\n'
259
292
  + '\n# Resultados e arquivos temporarios\n' + artifacts.map(pattern => `/${pattern}`).join('\n') + '\n';
260
- const body = fixed.split(/\r?\n/).map(line => location?.prefix && line.startsWith('/') ? `/${escapePattern(location.prefix)}${line}` : line).join('\n');
293
+ const body = fixed.split(/\r?\n/).map(line => {
294
+ if (!location?.prefix) return line;
295
+ if (line.startsWith('!/')) return `!/${escapePattern(location.prefix)}${line.slice(1)}`;
296
+ return line.startsWith('/') ? `/${escapePattern(location.prefix)}${line}` : line;
297
+ }).join('\n');
261
298
  const target = location?.exclude ?? checkedLocalPath(join(root, '.gitignore'));
262
299
  const before = localRead(target), after = localBlock(before, first, last, body);
263
300
  const edits = before === after ? [] : [{ path: target, before, after, beforeMode: localMode(target) }];
264
301
  const probes = [...paths, ...rootProbes(root, roots)];
265
- const artifactPaths = [...new Set([...artifactProbes(root, artifacts), ...(location ? visibleArtifactProbes(root, location, artifacts) : [])])];
302
+ const artifactPaths = [...new Set([...artifactProbes(root, artifacts), ...privateClients.probes.filter(path => safeIgnoreProbe(root, path)), ...(location ? visibleArtifactProbes(root, location, artifacts) : [])])];
266
303
  const wanted = new Set(probes.map(path => location?.prefix ? `${location.prefix}/${path}` : path));
267
304
  if (location && !before?.includes(first) && !localRead(join(root, '.gitignore'))?.includes(first)) {
268
305
  const ignored = ignoreMatches(location, [...probes, ...artifactPaths]);
@@ -0,0 +1,86 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readdirSync } from 'node:fs';
3
+
4
+ export const managedBegin = '<!-- BEGIN AGENTDEVKIT -->';
5
+ export const managedEnd = '<!-- END AGENTDEVKIT -->';
6
+ const digest = value => createHash('sha256').update(value).digest('hex');
7
+ const fail = message => { throw Object.assign(new Error(message), { code: 'E_MANAGED_BLOCK' }); };
8
+
9
+ export function canonicalEntrypoint(path) {
10
+ return ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'].find(name => name.toLowerCase() === path.toLowerCase()) || null;
11
+ }
12
+
13
+ export function resolveEntrypoint(root, canonical) {
14
+ if (!canonicalEntrypoint(canonical)) fail(`Entrada desconhecida: ${canonical}`);
15
+ let names;
16
+ try { names = readdirSync(root); }
17
+ catch (error) { if (error.code === 'ENOENT') return canonical; throw error; }
18
+ const matches = names.filter(name => name.toLowerCase() === canonical.toLowerCase());
19
+ if (matches.length > 1) fail(`Entradas ambiguas: ${matches.sort().join(', ')}`);
20
+ return matches[0] || canonical;
21
+ }
22
+
23
+ export function entrypointReference(root, path) {
24
+ const canonical = canonicalEntrypoint(path);
25
+ if (canonical === 'AGENTS.md') return 'Leia e siga [.pbai4devs/INSTRUCTIONS.md](.pbai4devs/INSTRUCTIONS.md).';
26
+ if (canonical === 'CLAUDE.md') return `@${resolveEntrypoint(root, 'AGENTS.md')}`;
27
+ if (canonical === 'GEMINI.md') return `@./${resolveEntrypoint(root, 'AGENTS.md')}`;
28
+ fail(`Entrada desconhecida: ${path}`);
29
+ }
30
+
31
+ export function renderEntrypointImports(root, text) {
32
+ const name = resolveEntrypoint(root, 'AGENTS.md');
33
+ return text.replace(/^(@(?:\.\/)?)AGENTS\.md(?=\r?$)/gim, (_match, prefix) => `${prefix}${name}`);
34
+ }
35
+
36
+ export function managedText(value) {
37
+ const text = Buffer.isBuffer(value) ? new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(value) : String(value ?? '');
38
+ if (text.includes('\0')) fail('Conteudo binario nao pode receber instrucoes Markdown.');
39
+ return text;
40
+ }
41
+
42
+ function locate(value) {
43
+ const text = managedText(value);
44
+ let fence = null, start = null, finish = null, offset = 0;
45
+ for (const match of text.matchAll(/([^\r\n]*)(\r\n|\r|\n|$)/g)) {
46
+ if (!match[0]) continue;
47
+ const line = match[1];
48
+ const marker = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
49
+ if (fence) {
50
+ if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && !marker[2].trim()) fence = null;
51
+ } else if (marker) fence = marker[1];
52
+ else if (line === managedBegin) {
53
+ if (start !== null || finish !== null) fail('Marcadores AgentDevKit duplicados.');
54
+ start = offset;
55
+ } else if (line === managedEnd) {
56
+ if (start === null || finish !== null) fail('Marcadores AgentDevKit incompletos.');
57
+ finish = offset + line.length;
58
+ }
59
+ offset += match[0].length;
60
+ }
61
+ if ((start === null) !== (finish === null)) fail('Marcadores AgentDevKit incompletos.');
62
+ return { text, start, finish, fence, block: start === null ? null : text.slice(start, finish) };
63
+ }
64
+
65
+ export function extractManagedBlock(value) { return locate(value).block; }
66
+ export function managedBlockDigest(value) { const block = extractManagedBlock(value); return block === null ? null : digest(block); }
67
+
68
+ export function mergeManagedContent(value, body, { expectedHash } = {}) {
69
+ const current = locate(value), eol = current.text.match(/\r\n|\n|\r/)?.[0] || '\n';
70
+ const content = managedText(body).trim().replace(/\r\n|\r|\n/g, eol);
71
+ if (!content || content.includes(managedBegin) || content.includes(managedEnd)) fail('Referencia AgentDevKit invalida.');
72
+ const block = `${managedBegin}${eol}${content}${eol}${managedEnd}`;
73
+ if (current.block !== null && (expectedHash ? digest(current.block) !== expectedHash : current.block !== block)) fail('Bloco AgentDevKit alterado; conteudo preservado.');
74
+ if (current.fence && current.block === null) fail('Bloco de codigo Markdown nao fechado; conteudo preservado.');
75
+ const separator = !current.text || current.text.endsWith(eol + eol) ? '' : /[\r\n]$/.test(current.text) ? eol : eol + eol;
76
+ const text = current.block === null ? `${current.text}${separator}${block}${eol}`
77
+ : current.text.slice(0, current.start) + block + current.text.slice(current.finish);
78
+ return { text, block, sha256: digest(block), changed: text !== current.text };
79
+ }
80
+
81
+ export function stripManagedContent(value, expectedHash) {
82
+ const current = locate(value);
83
+ if (current.block === null) return current.text;
84
+ if (!expectedHash || digest(current.block) !== expectedHash) fail('Bloco AgentDevKit alterado; conteudo preservado.');
85
+ return current.text.slice(0, current.start) + current.text.slice(current.finish);
86
+ }