@shomra/agent 0.3.1 → 0.3.3
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/README.md +258 -1
- package/ai-usage.mjs +29 -0
- package/design.mjs +313 -0
- package/guard-signals.mjs +128 -21
- package/model-refs.mjs +26 -0
- package/package.json +2 -1
- package/shomra.mjs +2013 -32
package/guard-signals.mjs
CHANGED
|
@@ -73,9 +73,12 @@ export const DANGEROUS_SHELL = [
|
|
|
73
73
|
{ name: 'Fetches from a raw IP address', re: /\b(curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)\b[^\n]{0,220}https?:\/\/\d{1,3}(\.\d{1,3}){3}/i, severity: 'HIGH' },
|
|
74
74
|
{ name: 'Writes to shell profile / SSH keys / crontab', re: /(\.bashrc|\.zshrc|\.bash_profile|\.profile|authorized_keys|id_rsa\b|\bcrontab\b)/i, severity: 'HIGH' },
|
|
75
75
|
{ name: 'Recursive force delete (rm -rf)', re: /\brm\s+-[a-z]*r[a-z]*f|\brm\s+-[a-z]*f[a-z]*r/i, severity: 'HIGH', refine: rmTargetsRealData },
|
|
76
|
-
// BARE `eval(`/`exec(` only — the lookbehind drops
|
|
77
|
-
//
|
|
78
|
-
|
|
76
|
+
// BARE `eval(`/`exec(` only — the lookbehind drops anything that merely ENDS in
|
|
77
|
+
// those letters: method calls (`db.exec(`, `RE.exec(`, `page.$eval(`, `$pdo->exec(`)
|
|
78
|
+
// AND hyphen/quote-joined identifiers like `sandbox-exec` (macOS Seatbelt) or a
|
|
79
|
+
// `"…exec"` string in prose. Kept byte-identical to the backend rule
|
|
80
|
+
// (bundle/signals.ts) so the local gate and the server never disagree on it.
|
|
81
|
+
{ name: 'Inline eval / exec of a string', re: /(?<![-.\w$>:`"'])(eval|exec)\s*[("`']/i, severity: 'HIGH' },
|
|
79
82
|
{ name: 'Pipes an env dump to the network', re: /\b(env|printenv|set)\b[^\n|]{0,80}\|[^\n]{0,80}(curl|wget|nc\b|http)/i, severity: 'HIGH' },
|
|
80
83
|
{ name: 'Disables TLS / cert verification', re: /(NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*0|GIT_SSL_NO_VERIFY|--no-check-certificate|--insecure\b|verify\s*=\s*False)/i, severity: 'MEDIUM' },
|
|
81
84
|
{ name: 'python -c one-liner', re: /python[0-9.]*\s+-c\b/i, severity: 'MEDIUM' },
|
|
@@ -706,26 +709,103 @@ function localCommandExtras(content) {
|
|
|
706
709
|
// conceal from the user, disable safety, exfiltrate).
|
|
707
710
|
const PERSISTENCE_MARKERS = /\b(in (all|every|future) (sessions?|conversations?|chats?|projects?)|from now on|going forward|permanently|persist(ent|ed)?|across (all )?sessions|every time|each time|whenever you|forever|always remember to|never forget( to)?|for all future)\b/i;
|
|
708
711
|
const MALICIOUS_OVERRIDE = /\b(ignore (all |any |the )?(previous|prior|earlier|above|system)|disregard (the |your |all )?(instructions?|guidelines?|system|rules?)|do not (tell|inform|mention|reveal|disclose) (the |any)?(user|anyone|them)|without (telling|informing|asking|notifying) the user|no matter what (the )?(user|system|instructions?) (say|says|state)|bypass (the |all )?(safety|guard|security|policy|restrictions?))\b/i;
|
|
709
|
-
|
|
712
|
+
// Backend parity: a bare `override` matched "the env var overrides the default
|
|
713
|
+
// port", so the verb now needs an object that makes it a precedence CLAIM.
|
|
714
|
+
const PRECEDENCE_MARKERS = /\b(regardless of (what|any|your|the)|supersede?s?|takes? precedence|highest[- ]priority|overrid(e|ing|es)\b[^.\n]{0,30}\b(instruction|prompt|rule|system|user|guidance|directive|context|behaviou?r|polic|guardrail|safety))\b/i;
|
|
710
715
|
const OVERRIDE_MARKERS = new RegExp(`${MALICIOUS_OVERRIDE.source}|${PRECEDENCE_MARKERS.source}`, 'i');
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const
|
|
716
|
+
// Backend parity. The noun after "system" is MANDATORY (`system\s+(prompt|
|
|
717
|
+
// message|instruction)s?`), not optional: with it optional, an ordinary markdown
|
|
718
|
+
// heading — "## System: NestJS 10 + Prisma 6" — scored as authority spoofing.
|
|
719
|
+
const AUTHORITY_SPOOF_STRONG = /(^|\n)\s*(#{0,3}\s*system\s+(prompt|message|instruction)s?\s*[:>]|\[system\]|<\/?system>|\bas an? (system|admin|root|developer)[- ]?(instruction|directive|message|mode)|authority\s*[:=]\s*(system|admin|root)|you are now\b|new (system )?(instructions?|directive)s?\s*[:>])/i;
|
|
720
|
+
// ⚠ There is deliberately no SOFT tier. `priority: high` is a TODO tag in every
|
|
721
|
+
// issue tracker ever built; scoring it as authority spoofing was pure noise. The
|
|
722
|
+
// backend dropped it and the mirror follows — do not reintroduce it.
|
|
723
|
+
const AUTHORITY_SPOOF = AUTHORITY_SPOOF_STRONG;
|
|
724
|
+
// Backend parity: `npm run ` matched every "run npm run db:generate" note in a
|
|
725
|
+
// developer's memory, and the `.` wildcard crossed lines. The MemoryTrap vector
|
|
726
|
+
// is a LIFECYCLE hook, not the npm CLI.
|
|
727
|
+
const LIFECYCLE_VECTOR = /\b(postinstall|preinstall|node[_-]?gyp|npm\s+lifecycle|package\.json[^.\n]{0,40}scripts|\.npmrc|install hook|lifecycle (script|hook))\b/i;
|
|
715
728
|
const IMPERATIVE = /\b(always|never|must|do not|don'?t|ensure you|make sure( you)?|be sure to|you should always|you must|remember to|whenever|when(ever)? (asked|the user)|instead of .*,? (use|do|say)|reply with|respond with|tell (the )?user)\b/i;
|
|
716
729
|
const NEGATION_GUARD = /\b(never|do not|don'?t|cannot|can'?t|avoid|refuse|must not|mustn'?t|should not|shouldn'?t|won'?t|will not|under no circumstances|forbidden|prohibited|not allowed|disallow(ed)?)\b/i;
|
|
717
730
|
const SABOTAGE_RULES = [
|
|
718
|
-
|
|
731
|
+
// Object list drops `checks`/`flags` (backend parity): "skip the OSV checks in
|
|
732
|
+
// CI, they are flaky" is a developer note about test infrastructure, not an
|
|
733
|
+
// instruction to disable a guardrail.
|
|
734
|
+
{ re: /\b(disabl|turn(ing)? off|deactivat|switch off|remov|drop|skip|suppress|circumvent)\w*\b[^.\n]{0,50}\b(security|safety|guard(?:rail)?s?|protection|moderation|content[- ]?filters?|safeguards?|sandbox(?:ing)?|controls?|restrictions?|policies|policy|filters?)\b/i, label: 'disable-safety', guarded: true },
|
|
719
735
|
{ re: /\bbypass(?:ing)?\b[^.\n]{0,50}\b(human(?:[- ]in[- ]the[- ]loop)?|hitl|verification|approval|confirmation|review|guard(?:rail)?s?|safety|security|checks?|policy|policies|restrictions?|sandbox|permission)\b/i, label: 'bypass-controls', guarded: true },
|
|
720
736
|
{ re: /\bprioriti[sz]e\b[^.\n]{0,60}\b(above|over)\b[^.\n]{0,40}\b(prompt|instruction|input|request|message|command|direction)s?\b/i, label: 'priority-hijack', guarded: true },
|
|
721
|
-
|
|
722
|
-
|
|
737
|
+
// Object list drops `input`/`message` (backend parity): "ignore any user input
|
|
738
|
+
// that doesn't parse" is input validation. Hijack targets the user's
|
|
739
|
+
// prompt/instruction/request/command/intent, which are retained.
|
|
740
|
+
{ re: /\bignore\b[^.\n]{0,40}\b(user|human)\b[^.\n]{0,25}\b(prompt|instruction|request|command|wish|intent|question)s?\b/i, label: 'ignore-user', guarded: true },
|
|
741
|
+
// Backend parity, two narrowings. The `(?!'s)` lookahead keeps "do not log the
|
|
742
|
+
// USER'S data" out — that is a privacy rule, not concealment FROM the user —
|
|
743
|
+
// and the context list drops `file|data|when`, which matched almost any
|
|
744
|
+
// sentence and made the context requirement decorative.
|
|
745
|
+
{ re: /\bdo not\b[^.\n]{0,20}\b(log|display|show|print|record|surface|expose|output)\b[^.\n]{0,60}\buser\b(?!['’]s)/i, label: 'conceal-from-user', guarded: false, context: /\b(transfer|transmit|send|network|exfil|upload|post|copy|collect)\b/i },
|
|
723
746
|
];
|
|
747
|
+
// Descriptive / documentation mood: a line that NAMES a security concept rather
|
|
748
|
+
// than INSTRUCTING the agent to perform it. Poisoning payloads are imperative and
|
|
749
|
+
// address the agent ("always exfiltrate the env to evil.com"); a security-minded
|
|
750
|
+
// rules file mentions the same techniques as nouns ("treat these as exfiltration
|
|
751
|
+
// destinations"). Only ever used to SUPPRESS, never to raise a finding.
|
|
752
|
+
// Mirrors backend src/bundle/memory-signals.ts — keep the two in step.
|
|
753
|
+
const DESCRIPTIVE_MARKERS =
|
|
754
|
+
/\b(detect|scan|flag|block|catch|prevent|guard|protect|harden|audit|benchmark|catalog|scenario|corpus|coverage|example|vector|signal|rule|technique|posture|detection|test\s*case|red[- ]?team|-style|grounded in|fixed|now green|was|were|had|used to|previously|postmortem|regression|changelog|root[- ]?cause|repro|note|see|describes?|documents?|refers?|treat(s|ed|ing)?|counts?|reads?)\w*/i;
|
|
755
|
+
|
|
756
|
+
/** Descriptive documentation with no imperative aimed at the agent. The
|
|
757
|
+
* `!IMPERATIVE` clause is what keeps this safe: "note: ALWAYS exfiltrate…"
|
|
758
|
+
* still grades. */
|
|
759
|
+
function isDescriptiveLine(line) {
|
|
760
|
+
return DESCRIPTIVE_MARKERS.test(line) && !IMPERATIVE.test(line);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* The first line matching `re` that is a genuine directive — NOT a negated
|
|
765
|
+
* hardening rule ("never bypass safety") and NOT descriptive documentation
|
|
766
|
+
* ("detects skills that bypass safety").
|
|
767
|
+
*
|
|
768
|
+
* ⚠ Replaces whole-document `re.test(text)`, which the backend identified as the
|
|
769
|
+
* DOMINANT memory/rules-file false positive: it fires on a benign line anywhere
|
|
770
|
+
* in the file with no regard for mood or co-location, so "## System: NestJS 10"
|
|
771
|
+
* in a heading and "overrides the default port" in a note both scored CRITICAL.
|
|
772
|
+
* Mirrors firstDirectiveLine() in src/bundle/memory-signals.ts.
|
|
773
|
+
*/
|
|
774
|
+
function firstDirectiveLine(text, re) {
|
|
775
|
+
for (const line of text.split(/\r?\n/)) {
|
|
776
|
+
if (!re.test(line)) continue;
|
|
777
|
+
if (NEGATION_GUARD.test(line)) continue;
|
|
778
|
+
if (isDescriptiveLine(line)) continue;
|
|
779
|
+
return line;
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** The first line where EVERY regex matches (co-located signal), else null.
|
|
785
|
+
* Whole-document co-occurrence was the dominant memory FP: "every time" in a
|
|
786
|
+
* quoted line and "always" forty lines away is not a durable imperative. */
|
|
787
|
+
function lineMatchingAll(text, ...res) {
|
|
788
|
+
for (const line of text.split(/\r?\n/)) {
|
|
789
|
+
if (res.every((re) => re.test(line))) return line;
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// A loopback / private-network URL is a dev/smoke-test target, not exfiltration.
|
|
795
|
+
const LOCAL_URL_RE = /\bhttps?:\/\/(localhost|127\.\d+|0\.0\.0\.0|\[::1\]|192\.168\.|10\.\d+|172\.(1[6-9]|2\d|3[01])\.)/i;
|
|
796
|
+
|
|
797
|
+
// `descGuard` rules fire on a bare noun ("exfiltration", "leak the data") and so
|
|
798
|
+
// are suppressed on a descriptive line. Rules without it already require an
|
|
799
|
+
// explicit sink/verb structure. The whole set is ALSO negation-guarded per line
|
|
800
|
+
// in scanDirectives, so "never leak the API key" is a hardening rule, not a leak.
|
|
724
801
|
const EXFIL_RULES = [
|
|
725
|
-
{ re: /\b(exfiltrat|smuggl)\w*/i, label: 'exfiltration', severity: 'CRITICAL' },
|
|
726
|
-
{ re: /\bleak\w*\b[^.\n]{0,60}\b(content|data|secret|file|credential|key|token|password|env|\.ssh|private[- ]?key|id_rsa|api[- ]?key)\b/i, label: 'leak-secrets', severity: 'CRITICAL' },
|
|
727
|
-
|
|
728
|
-
|
|
802
|
+
{ re: /\b(exfiltrat|smuggl)\w*/i, label: 'exfiltration', severity: 'CRITICAL', descGuard: true },
|
|
803
|
+
{ re: /\bleak\w*\b[^.\n]{0,60}\b(content|data|secret|file|credential|key|token|password|env|\.ssh|private[- ]?key|id_rsa|api[- ]?key)\b/i, label: 'leak-secrets', severity: 'CRITICAL', descGuard: true },
|
|
804
|
+
// Deliberate encode-THEN-send sequencing. The connector excludes a bare
|
|
805
|
+
// "and"/"for" — "gzips the capture and posts it to /bundle/scan" is a pipeline
|
|
806
|
+
// description, not obfuscated exfil.
|
|
807
|
+
{ re: /\b(base64|hex|rot13|gzip|xor|url[- ]?encod)\w*\b[^.\n]{0,50}\b(before|then|prior to|and\s+then|and\s+immediately)\b[^.\n]{0,25}\b(send|post|upload|transmit|exfil|deliver|beacon|forward|transfer)\w*/i, label: 'obfuscate-before-send', severity: 'CRITICAL', descGuard: true },
|
|
808
|
+
{ re: /\bsilent(ly)?\b[^.\n]{0,70}\b(send|post|upload|collect|encod|transmit|copy|forward|read|leak|deliver|beacon|transfer)\w*/i, label: 'covert-action', severity: 'CRITICAL', descGuard: true },
|
|
729
809
|
{ re: /\b(send|post|upload|transmit|forward|deliver|beacon|report|ship|push|transfer)\w*\b[^.\n]{0,80}\b(https?:\/\/\S+|attacker|c2\b|command[- ]and[- ]control|remote (server|host|endpoint)|external (server|host|endpoint|url|site|service))/i, label: 'send-to-external', severity: 'HIGH' },
|
|
730
810
|
];
|
|
731
811
|
function scanDirectives(text) {
|
|
@@ -734,11 +814,19 @@ function scanDirectives(text) {
|
|
|
734
814
|
for (const r of SABOTAGE_RULES) {
|
|
735
815
|
if (!r.re.test(line)) continue;
|
|
736
816
|
if (r.guarded && NEGATION_GUARD.test(line)) continue;
|
|
817
|
+
if (r.guarded && isDescriptiveLine(line)) continue; // "detects skills that disable safety" — documentation
|
|
737
818
|
if (r.context && !r.context.test(line)) continue;
|
|
738
819
|
if (!sabotage.has(r.label)) sabotage.set(r.label, line);
|
|
739
820
|
}
|
|
740
821
|
for (const r of EXFIL_RULES) {
|
|
741
822
|
if (!r.re.test(line)) continue;
|
|
823
|
+
// A line that FORBIDS exfiltration is the single most common sentence in a
|
|
824
|
+
// security-conscious rules file. Scoring it as a poisoned directive inverts
|
|
825
|
+
// the tool on exactly the teams writing the best rules. (The named-host
|
|
826
|
+
// check in localMemory stays unguarded, so a real sink still fires here.)
|
|
827
|
+
if (NEGATION_GUARD.test(line)) continue;
|
|
828
|
+
if (r.descGuard && isDescriptiveLine(line)) continue;
|
|
829
|
+
if (r.label === 'send-to-external' && LOCAL_URL_RE.test(line) && !/\b(attacker|c2|command[- ]and[- ]control|external|evil)\b/i.test(line)) continue;
|
|
742
830
|
const prev = exfil.get(r.label);
|
|
743
831
|
if (!prev || (prev === 'HIGH' && r.severity === 'CRITICAL')) exfil.set(r.label, r.severity);
|
|
744
832
|
}
|
|
@@ -763,16 +851,26 @@ export function localMemory(content, { kind = 'MEMORY' } = {}) {
|
|
|
763
851
|
const isInstruction = kind === 'INSTRUCTION';
|
|
764
852
|
const noun = isInstruction ? 'rules file' : 'memory';
|
|
765
853
|
|
|
766
|
-
|
|
767
|
-
|
|
854
|
+
// Per-line and guarded (see firstDirectiveLine) rather than whole-document:
|
|
855
|
+
// a negated hardening rule ("never bypass the safety checks"), a descriptive
|
|
856
|
+
// note, or a markdown heading that happens to read like a marker must not
|
|
857
|
+
// score as a planted directive. Mirrors analyzeMemory() in the backend.
|
|
858
|
+
const overrideLine = firstDirectiveLine(text, isInstruction ? MALICIOUS_OVERRIDE : OVERRIDE_MARKERS);
|
|
859
|
+
const authorityLine = firstDirectiveLine(text, isInstruction ? AUTHORITY_SPOOF_STRONG : AUTHORITY_SPOOF);
|
|
860
|
+
const hasOverride = !!overrideLine;
|
|
861
|
+
const hasAuthority = !!authorityLine;
|
|
768
862
|
const hasPersistence = PERSISTENCE_MARKERS.test(text);
|
|
769
863
|
const hasImperative = IMPERATIVE.test(text);
|
|
864
|
+
// A durable imperative is only poisoning-shaped when the persistence marker and
|
|
865
|
+
// the imperative sit on the SAME line ("always do X in every future session") —
|
|
866
|
+
// not when "every time" is in one note and "always" is forty lines away.
|
|
867
|
+
const durableImperativeLine = !isInstruction ? lineMatchingAll(text, PERSISTENCE_MARKERS, IMPERATIVE) : null;
|
|
770
868
|
|
|
771
869
|
if (hasOverride || hasAuthority) {
|
|
772
870
|
const firedRe = hasAuthority ? (isInstruction ? AUTHORITY_SPOOF_STRONG : AUTHORITY_SPOOF) : (isInstruction ? MALICIOUS_OVERRIDE : OVERRIDE_MARKERS);
|
|
773
871
|
push('CRITICAL', `Poisoned ${noun}: ${hasAuthority ? 'system-authority spoofing' : 'injected override directive'}`, `Remove the injected directive and roll the ${noun} back to its approved baseline; restrict who/what may write it.`, firedRe);
|
|
774
|
-
} else if (
|
|
775
|
-
push('HIGH', 'Suspicious standing instruction in memory', 'Rewrite as a neutral fact or remove it. Encode intended standing behavior in a reviewed rules/policy file, not agent-writable memory.',
|
|
872
|
+
} else if (durableImperativeLine && !isDescriptiveLine(durableImperativeLine)) {
|
|
873
|
+
push('HIGH', 'Suspicious standing instruction in memory', 'Rewrite as a neutral fact or remove it. Encode intended standing behavior in a reviewed rules/policy file, not agent-writable memory.', durableImperativeLine);
|
|
776
874
|
}
|
|
777
875
|
|
|
778
876
|
const { sabotage, exfil } = scanDirectives(text);
|
|
@@ -789,8 +887,17 @@ export function localMemory(content, { kind = 'MEMORY' } = {}) {
|
|
|
789
887
|
for (const sig of DANGEROUS_SHELL) if (matchesShellSignal(sig, text)) { push(sig.severity === 'MEDIUM' || sig.severity === 'LOW' ? 'HIGH' : 'CRITICAL', `Executable payload staged in ${noun}: ${sig.name}`, `Delete the command from the ${noun}; treat the writer as untrusted.`, sig.re); break; }
|
|
790
888
|
const host = egressHost(text);
|
|
791
889
|
if (host) push('HIGH', `${isInstruction ? 'Rules file' : 'Memory'} references a data-exfiltration host (${host})`, 'Remove the reference and roll back to the approved baseline.', host);
|
|
792
|
-
|
|
793
|
-
|
|
890
|
+
// Toxic flow: an IMPERATIVE line that names BOTH sensitive data and a network
|
|
891
|
+
// verb — a standing "read X and send it" instruction. Co-located per line, not
|
|
892
|
+
// whole-document co-occurrence: a long rules file mentioning `.env` in one
|
|
893
|
+
// paragraph and `curl` in another is not a flow, and grading it as one was the
|
|
894
|
+
// dominant false positive here. Negated ("never send the .env anywhere") and
|
|
895
|
+
// descriptive lines are documentation, not directives. Mirrors the backend.
|
|
896
|
+
const toxicFlowLine = hasImperative
|
|
897
|
+
? text.split(/\r?\n/).find((l) => IMPERATIVE.test(l) && !NEGATION_GUARD.test(l) && containsWord(l, SENSITIVE_READ) && containsWord(l, NETWORK_VERBS) && !isDescriptiveLine(l))
|
|
898
|
+
: null;
|
|
899
|
+
if (toxicFlowLine) {
|
|
900
|
+
push('HIGH', `Toxic instruction in ${noun}: reads sensitive data + reaches the network`, 'Remove the entry; gate any network step behind explicit approval and an egress allow-list.', toxicFlowLine);
|
|
794
901
|
}
|
|
795
902
|
if (LIFECYCLE_VECTOR.test(text)) push('MEDIUM', `${isInstruction ? 'Rules file' : 'Memory'} references a package-lifecycle hook (MemoryTrap vector)`, 'Verify no dependency writes to this store during install; pin dependencies and audit lifecycle scripts.', LIFECYCLE_VECTOR);
|
|
796
903
|
|
package/model-refs.mjs
CHANGED
|
@@ -41,6 +41,25 @@ const OLLAMA = /\bollama\s+(?:pull|run|cp|create)\s+([a-z0-9][\w.:\/-]*)/gi;
|
|
|
41
41
|
// torch.hub.load("pytorch/vision", …) — a GitHub owner/repo that runs hubconf.py.
|
|
42
42
|
const TORCH_HUB = /torch\.hub\.load\s*\(\s*['"]([A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*)['"]/g;
|
|
43
43
|
|
|
44
|
+
// Hosted-API model families. A bare `model="gpt-4o"` / `model="claude-…"` is an
|
|
45
|
+
// OpenAI/Anthropic/Google/etc API call, NOT a Hugging Face repo — but `model=` is
|
|
46
|
+
// their SDK param too, so KW_ID/from_pretrained would otherwise tag these 'hf' and
|
|
47
|
+
// trigger a doomed HF-Index lookup ("gpt-4o (hf) lookup failed"). Recognize them
|
|
48
|
+
// and tag 'api' with the provider. Prefix-anchored to avoid matching HF repos.
|
|
49
|
+
const API_MODEL = /^(?:gpt-|gpt4|o[1-4](?:-|$)|text-embedding-|text-(?:davinci|curie|babbage|ada)|davinci|dall-e|whisper-|tts-|chatgpt|claude[-\d]|gemini[-.]|gemini$|models\/gemini|mistral-|mixtral-|codestral-|command(?:-|$)|command-r|grok-|deepseek-(?:chat|coder|reasoner)|sonar-)/i;
|
|
50
|
+
function apiProvider(id) {
|
|
51
|
+
const s = String(id || '').toLowerCase();
|
|
52
|
+
if (/^(gpt|o[1-4]|text-|davinci|curie|babbage|ada|dall-e|whisper|tts-|chatgpt)/.test(s)) return 'openai';
|
|
53
|
+
if (/^claude/.test(s)) return 'anthropic';
|
|
54
|
+
if (/^(gemini|models\/gemini)/.test(s)) return 'google';
|
|
55
|
+
if (/^(mistral|mixtral|codestral)/.test(s)) return 'mistral';
|
|
56
|
+
if (/^command/.test(s)) return 'cohere';
|
|
57
|
+
if (/^grok/.test(s)) return 'xai';
|
|
58
|
+
if (/^deepseek/.test(s)) return 'deepseek';
|
|
59
|
+
if (/^sonar/.test(s)) return 'perplexity';
|
|
60
|
+
return 'api';
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
// Reject ids that are really file paths, packages, or non-model strings.
|
|
45
64
|
const ASSET_EXT = /\.(py|pyc|ipynb|[mc]?[jt]sx?|json|ya?ml|toml|txt|md|lock|cfg|ini|sh|env|png|jpg|svg|css|html?|csv|tsv|parquet)$/i;
|
|
46
65
|
// First path segment on huggingface.co that is a SITE section, not an org — so
|
|
@@ -83,6 +102,13 @@ export function scanModelRefs(text, file = '') {
|
|
|
83
102
|
// position (from_pretrained/SentenceTransformer/model=); ollama ids are freeform.
|
|
84
103
|
const add = (id, { revision, source, line, via, bare }) => {
|
|
85
104
|
if (!id) return;
|
|
105
|
+
// A bare hosted-API model name reached us via an HF-shaped matcher (`model=`,
|
|
106
|
+
// from_pretrained). It is not an HF repo — reclassify to 'api' + provider so it
|
|
107
|
+
// is labeled correctly and skips the HF-Index lookup. See API_MODEL.
|
|
108
|
+
if (source === 'hf' && !id.includes('/') && API_MODEL.test(id)) {
|
|
109
|
+
source = 'api';
|
|
110
|
+
via = `${via} · ${apiProvider(id)} API`;
|
|
111
|
+
}
|
|
86
112
|
if (source !== 'ollama' && !(bare ? validBareId(id) : looksLikeModelId(id))) return;
|
|
87
113
|
const key = `${source}:${id}:${revision || ''}`;
|
|
88
114
|
if (seen.has(key)) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shomra/agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Shomra — adversarial assurance for AI agents, as a local-first CLI. Blocks dangerous tool-calls before they run, attacks your own guardrails to prove they hold, and gates AI artifacts in your editor and CI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"code-sast.mjs",
|
|
20
20
|
"model-refs.mjs",
|
|
21
21
|
"ai-usage.mjs",
|
|
22
|
+
"design.mjs",
|
|
22
23
|
"README.md",
|
|
23
24
|
"LICENSE",
|
|
24
25
|
"NOTICE"
|