@shomra/agent 0.2.10 → 0.2.11

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 (2) hide show
  1. package/package.json +1 -1
  2. package/shomra.mjs +104 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shomra/agent",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "Shomra — a local-first security scanner and runtime firewall for AI agents, MCP servers, prompts, and models. Gates AI artifacts in your editor and CI.",
5
5
  "type": "module",
6
6
  "bin": {
package/shomra.mjs CHANGED
@@ -1656,6 +1656,108 @@ function printWhy(res) {
1656
1656
  console.log('');
1657
1657
  }
1658
1658
 
1659
+ // ── shomra provenance: which of these changed files did an AI agent write? ──
1660
+ //
1661
+ // shomra provenance [--staged | --base main] [--trailer] [--fail-on-blocked] [--json]
1662
+ //
1663
+ // Every mutating tool call the runtime firewall screened was recorded with its
1664
+ // target path and ALLOW/FLAG/BLOCK decision. This joins a real git diff against
1665
+ // that record, so a commit can carry an EVIDENCE-BACKED statement of authorship
1666
+ // instead of a "Co-Authored-By" line anyone can type.
1667
+ //
1668
+ // ⚠ "Unattributed" means the firewall has no record — NOT that a human wrote it.
1669
+ // With the hook uninstalled every file is unattributed, so the output always
1670
+ // states its coverage and never claims human authorship. Don't rewrite that copy.
1671
+
1672
+ /** All changed paths (not just AI artifacts) for provenance. */
1673
+ function gitChangedPaths(root, { staged, base }) {
1674
+ const run = (args) => {
1675
+ try {
1676
+ return execSync(`git ${args}`, { cwd: root, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }).toString();
1677
+ } catch {
1678
+ return null;
1679
+ }
1680
+ };
1681
+ let out = null;
1682
+ if (staged) {
1683
+ out = run('diff --cached --name-only --relative --diff-filter=ACM');
1684
+ } else if (base) {
1685
+ for (const b of [`origin/${base}`, base]) {
1686
+ out = run(`diff --name-only --relative --diff-filter=ACM ${b}...HEAD`);
1687
+ if (out !== null) break;
1688
+ }
1689
+ }
1690
+ if (out === null) out = run('diff HEAD~1 --name-only --relative --diff-filter=ACM');
1691
+ if (out === null) return null;
1692
+ return out.split('\n').map((s) => s.trim()).filter(Boolean);
1693
+ }
1694
+
1695
+ async function cmdProvenance(flags, positional) {
1696
+ const root = path.resolve(flags.path || positional[0] || '.');
1697
+ const staged = !!flags.staged;
1698
+ const base = flags.base || (staged ? null : process.env.GITHUB_BASE_REF || 'main');
1699
+
1700
+ const paths = gitChangedPaths(root, { staged, base });
1701
+ if (paths === null) {
1702
+ console.error(red('✗') + ' Not a git repository (or no diff available). Run inside a repo, or pass --base <ref>.');
1703
+ process.exit(1);
1704
+ }
1705
+ if (!paths.length) {
1706
+ if (flags.json) console.log(JSON.stringify({ files: [], agentAuthored: 0, coverage: 'NO_TELEMETRY', summary: 'no changed files' }, null, 2));
1707
+ else console.log(green('\n ✓ No changed files to attribute.\n'));
1708
+ return;
1709
+ }
1710
+
1711
+ const cfg = loadConfig();
1712
+ const { apiKey, url } = resolveSettings(cfg);
1713
+ let res;
1714
+ try {
1715
+ res = await api(url, apiKey, '/gate/provenance', {
1716
+ paths,
1717
+ repo: flags.repo || process.env.GITHUB_REPOSITORY || undefined,
1718
+ sessionId: flags.session || undefined,
1719
+ sinceHours: flags.since ? Number(flags.since) : undefined,
1720
+ });
1721
+ } catch (e) {
1722
+ // Provenance is an evidence lookup, not a guard — a backend outage must not
1723
+ // block a commit. Say so plainly instead of silently reporting "no agents".
1724
+ console.error(yellow('!') + ` Provenance unavailable (${e.message}). Authorship not established.`);
1725
+ process.exit(flags['fail-on-blocked'] ? 1 : 0);
1726
+ }
1727
+
1728
+ if (flags.json) {
1729
+ console.log(JSON.stringify(res, null, 2));
1730
+ } else if (flags.trailer) {
1731
+ for (const t of res.trailers || []) console.log(t);
1732
+ } else {
1733
+ const noTel = res.coverage === 'NO_TELEMETRY';
1734
+ console.log('');
1735
+ console.log(` ${bold('Commit provenance')} ${dim(`· ${res.files.length} changed file(s)`)}`);
1736
+ console.log(` ${noTel ? yellow('⚠ ' + res.summary) : res.summary}`);
1737
+ if (noTel) {
1738
+ console.log(dim(' No firewall telemetry for this range — this is NOT a claim that a human wrote them.'));
1739
+ console.log(dim(' Install the runtime hook with ') + bold('shomra protect') + dim(' to attribute future work.'));
1740
+ }
1741
+ console.log('');
1742
+ for (const f of res.files.slice(0, 40)) {
1743
+ const tag =
1744
+ f.authorship === 'AGENT' ? cyan('agent') : f.authorship === 'BLOCKED_ATTEMPT' ? red('blocked') : dim('unattributed');
1745
+ const who = f.agents?.length ? dim(` ${f.agents.join(', ')}`) : '';
1746
+ const amb = f.ambiguous ? yellow(' ~ambiguous') : '';
1747
+ console.log(` ${tag.padEnd(22)} ${f.path}${who}${amb}`);
1748
+ }
1749
+ if (res.files.length > 40) console.log(dim(` …and ${res.files.length - 40} more`));
1750
+ console.log('');
1751
+ }
1752
+
1753
+ // A path the firewall BLOCKED that changed anyway is the signal worth failing
1754
+ // on: either the guard was bypassed, or something wrote it outside the agent.
1755
+ if (flags['fail-on-blocked'] && res.blockedAttempts > 0) {
1756
+ console.error(red('✗') + ` ${res.blockedAttempts} file(s) the firewall blocked were modified anyway.`);
1757
+ process.exit(1);
1758
+ }
1759
+ }
1760
+
1659
1761
  // ── shomra install-precommit: gate staged AI artifacts at commit time ──
1660
1762
  //
1661
1763
  // shomra install-precommit [dir] [--force]
@@ -3951,6 +4053,7 @@ ${bold('COMMANDS')}
3951
4053
  ${cyan('init')} Configure + enroll this machine ${dim('--key shm_live_… [--url <backend>]')}
3952
4054
  ${cyan('protect')} Wire the runtime firewall for every coding agent ${dim('[--local] [--force]')}
3953
4055
  ${cyan('install-hook')} Wire the runtime firewall into ONE agent ${dim('[--agent claude|cursor|windsurf|gemini|codex|copilot|cline|aider|all] [--global]')}
4056
+ ${cyan('provenance')} Which changed files an AI agent wrote ${dim('[--staged | --base main] [--trailer] [--fail-on-blocked] [--json]')}
3954
4057
  ${cyan('install-precommit')} Gate staged AI artifacts on git commit ${dim('[dir] [--force]')}
3955
4058
  ${cyan('doctor')} ${bold('Am I safe?')} Posture of this machine's AI setup ${dim('[--json]')}
3956
4059
 
@@ -4163,6 +4266,7 @@ const COMMANDS = {
4163
4266
  baseline: (f, p) => cmdBaseline(f, p),
4164
4267
  fix: (f, p) => cmdFix(f, p),
4165
4268
  why: (f, p) => cmdWhy(f, p),
4269
+ provenance: (f, p) => cmdProvenance(f, p),
4166
4270
  'install-precommit': (f, p) => cmdInstallPrecommit(f, p),
4167
4271
  'scan-zip': (f, p) => cmdScanZip(f, p),
4168
4272
  'model-scan': (f, p) => cmdModelScan(f, p),