agent-gov-core 0.4.1 → 0.4.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/CHANGELOG.md +35 -0
- package/README.md +3 -3
- package/dist/finding.d.ts +16 -0
- package/dist/finding.js +10 -0
- package/dist/locators.d.ts +13 -1
- package/dist/locators.js +80 -6
- package/dist/shell.js +31 -3
- package/dist/toml.js +0 -0
- package/package.json +1 -1
- package/schemas/finding.schema.json +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Under v1.0, minor versions may include breaking changes — see [CONTRIBUTING.md](./CONTRIBUTING.md#backwards-compatibility) for the rules.
|
|
4
4
|
|
|
5
|
+
## [0.4.3] — 2026-05-22
|
|
6
|
+
|
|
7
|
+
Third Gemini-inspection round caught one confirmed bug, one disguised-as-suggestion bug, and three feature opportunities. Both bugs fixed here; the feature work is queued for v0.5.0.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `Finding.salientKey?: string` — optional discriminator that participates in the fingerprint hash. Set this when a single (kind, file, line) site can produce multiple distinct findings (e.g. two suspicious imports on the same line, two MCP servers in the same JSON object). Without it, the meta-reviewer would dedupe them into one. Stable values only — package name, server name, rule id; not timestamps or counters.
|
|
11
|
+
- `CreateFindingSpec.salientKey` — pass-through to the new field.
|
|
12
|
+
- Schema gained `salientKey` under properties (still optional, schema's `additionalProperties: false` updated to permit it).
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- `fingerprintFinding` now includes `salientKey` in the hash. Two distinct findings of the same kind on the same line with different `salientKey` values now produce different fingerprints. Backwards-compatible: findings without `salientKey` still produce stable, identical fingerprints to v0.4.2 for the (kind, file, line, column) tuple.
|
|
16
|
+
- `lineOfTomlKey` now tracks multi-line basic (`"""`) and literal (`'''`) string state and skips key matching on lines that fall inside one. Previously a decoy key inside a multi-line string value could be matched as if it were a real assignment — confirmed bug with sharper reproduction than Gemini's first-round example.
|
|
17
|
+
|
|
18
|
+
### Tests
|
|
19
|
+
- 7 new regression cases. 102 total (up from 95). Covers salientKey discrimination, backwards-compat for fingerprints without salientKey, validateFinding type check, decoy-in-`"""`, decoy-in-`'''`, single-line `"""..."""` (must NOT enter multiline state), and a plain-TOML sanity check that the fix doesn't over-correct.
|
|
20
|
+
|
|
21
|
+
## [0.4.2] — 2026-05-22
|
|
22
|
+
|
|
23
|
+
External code review (Gemini, second pass) caught four correctness bugs and one source-cleanliness issue. All five fixed here.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `lineOfJsonKey` and `lineOfJsonStringValue` now JSON-encode the search input before building the regex. A caller passing the *decoded* value (e.g. `C:\Temp` from a Windows-path field) now correctly locates the JSON source bytes (`"C:\\Temp"`) instead of returning 0. Affects CapabilityEcho's `package-scripts` detector for scripts containing quotes/backslashes.
|
|
27
|
+
- `lineOfJsonKey` and `lineOfJsonStringValue` now scan over `stripJsonComments(text)` instead of raw text. A commented-out `"command": "fake"` no longer shadows the real key on a later line. The strip is position-preserving so returned line numbers still reference the original source.
|
|
28
|
+
- `getCommandHead` now strips wrapper flags (`sudo -E`, `env -i`) after recognizing a wrapper, so `sudo -E curl ...` returns `curl` instead of `-E`. SessionTrail/CapabilityEcho shell detectors no longer miss wrapped curl/wget invocations. Known limitation: short flags taking a value (`sudo -u user curl`) still misclassify as the value — documented and pinned by test.
|
|
29
|
+
- TOML parser now rejects a standard table header (`[items]`) that follows an array-of-tables header (`[[items]]`) with a `Cannot redefine array-of-tables` error. Previously the standard table silently descended into the array's last entry, letting writes leak into `items[0]`. Spec compliance fix.
|
|
30
|
+
- TOML inline-table parser now rejects duplicate keys with `Duplicate key in inline table: ...`. Previously `server = { host = "a", host = "b" }` parsed as `{ host: "b" }` — the standard-table guard wasn't mirrored on inline tables. Spec compliance fix.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- Source cleanup: the two `keys.join` calls in `src/toml.ts` now use a named `PATH_KEY_SEPARATOR = ''` constant instead of literal NUL bytes embedded in the source. Same runtime behavior (NUL as the delimiter, which is illegal in TOML keys so collision-proof), but `rg`/`grep` no longer treat the file as binary and `file(1)` reports it as proper text.
|
|
34
|
+
- README: `rankSeverity` doc corrected — was `none=0…critical=4`, actually `low=1, medium=2, high=3, critical=4`. The schema has no `none` severity.
|
|
35
|
+
- README: `normalizeMcpCommand` signature and behavior description corrected — was listing a non-existent `serverUrl` field and claiming "resolves npx/uvx invocations" which doesn't happen. Now accurately lists: drops neutral confirm flags, strips Windows executable suffixes, sorts non-neutral flags alphabetically, preserves positional argument order, includes env + cwd in identity.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- 7 new regression tests: encoded-value lookup, commented-out shadow, wrapper-flag unwrap (+ edge-case pin), AOT-vs-table mixing, inline-table duplicate keys.
|
|
39
|
+
|
|
5
40
|
## [0.4.1] — 2026-05-22
|
|
6
41
|
|
|
7
42
|
### Fixed
|
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ The JSON schema at [`schemas/finding.schema.json`](./schemas/finding.schema.json
|
|
|
66
66
|
- `isSeverity(v)`, `isToolKind(v)`, `isNamespacedKind(v)` — type guards
|
|
67
67
|
- `kind(tool, name)` — build a namespaced kind without hand-assembling the dotted string
|
|
68
68
|
- `createFinding({tool, name, severity, message, ...})` — convenience constructor that calls `kind()` and `fingerprintFinding()` for you
|
|
69
|
-
- `fingerprintFinding(finding)` — 16-character hex hash of `(kind, file, line, column)`. Stable across runs and message rewordings, so a meta-reviewer can dedupe
|
|
69
|
+
- `fingerprintFinding(finding)` — 16-character hex hash of `(kind, file, line, column, salientKey?)`. Stable across runs and message rewordings, so a meta-reviewer can dedupe. Pass `salientKey` (since v0.4.3) when multiple distinct findings can fire at the same site
|
|
70
70
|
- `validateFinding(value)` — runtime check against `schemas/finding.schema.json`, returns `{ ok, errors[] }`
|
|
71
71
|
|
|
72
72
|
### Config readers
|
|
@@ -81,14 +81,14 @@ The JSON schema at [`schemas/finding.schema.json`](./schemas/finding.schema.json
|
|
|
81
81
|
- `lineOfTomlKey(text, dottedKey, scope?)` — 1-based line of a TOML key, optionally scoped to a byte range. Use scope to disambiguate `[[array]]`-of-tables entries that share the same leaf key.
|
|
82
82
|
|
|
83
83
|
### MCP command normalization
|
|
84
|
-
- `normalizeMcpCommand({ command, args, url,
|
|
84
|
+
- `normalizeMcpCommand({ command, args, url, env, cwd })` — canonical identity string for an MCP server entry. Drops neutral confirm flags (`-y`, `--yes`), strips Windows executable suffixes (`.cmd`, `.exe`, `.bat`, `.ps1`), sorts non-neutral flags alphabetically, preserves positional argument order, and includes env + cwd in the identity. Used to dedupe `mcp_command_mismatch` false positives when servers are equivalent but syntactically different (`npx -y foo@1.2.3` vs `npx foo@1.2.3`). Does not interpret what npx/uvx invocations resolve to at runtime — that's outside the substrate's scope.
|
|
85
85
|
|
|
86
86
|
### Shell tokenization
|
|
87
87
|
- `tokenizeShell(command)` — quote-aware split on `;`, `|`, `&&`, `||` plus trivial obfuscation neutralization (`c""url` → `curl`, `c\\url` → `curl`)
|
|
88
88
|
- `getCommandHead(subcommand)` — extract the leading verb after tokenization
|
|
89
89
|
|
|
90
90
|
### GitHub Action helpers
|
|
91
|
-
- `rankSeverity(s)` — numeric rank `
|
|
91
|
+
- `rankSeverity(s)` — numeric rank `low=1, medium=2, high=3, critical=4` (matches the schema's closed severity enum; there is no `none`)
|
|
92
92
|
- `passesSeverityThreshold(s, threshold)`, `anyAtOrAbove(findings, threshold)` — fail-on plumbing
|
|
93
93
|
- `emitFindingAnnotation(f)` — render a Finding as a `::warning file=…,line=…,title=…::…` GitHub workflow annotation
|
|
94
94
|
|
package/dist/finding.d.ts
CHANGED
|
@@ -26,6 +26,16 @@ export interface Finding {
|
|
|
26
26
|
location?: FindingLocation;
|
|
27
27
|
/** Stable identifier for dedupe across runs. Recommended: hash of (kind, location, salient fields). */
|
|
28
28
|
fingerprint?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Optional discriminator that participates in the fingerprint hash. Set this
|
|
31
|
+
* when a single (kind, file, line) site can legitimately host multiple distinct
|
|
32
|
+
* findings — e.g. two suspicious imports on the same line, two MCP servers in
|
|
33
|
+
* the same JSON object, two npm dependencies declared in one package.json line.
|
|
34
|
+
* Without it, the meta-reviewer would dedupe them into one. Use a stable value
|
|
35
|
+
* that doesn't drift across reruns (package name, server name, rule id) — not
|
|
36
|
+
* a timestamp or counter.
|
|
37
|
+
*/
|
|
38
|
+
salientKey?: string;
|
|
29
39
|
/** Optional structured metadata; downstream meta-reviewers may inspect it. */
|
|
30
40
|
data?: Record<string, unknown>;
|
|
31
41
|
}
|
|
@@ -57,6 +67,12 @@ export interface CreateFindingSpec {
|
|
|
57
67
|
detail?: string;
|
|
58
68
|
location?: FindingLocation;
|
|
59
69
|
data?: Record<string, unknown>;
|
|
70
|
+
/**
|
|
71
|
+
* See {@link Finding.salientKey}. Pass when the same (kind, file, line) site
|
|
72
|
+
* can produce multiple distinct findings that must not collapse to one
|
|
73
|
+
* fingerprint.
|
|
74
|
+
*/
|
|
75
|
+
salientKey?: string;
|
|
60
76
|
/** Optional explicit fingerprint. If omitted, {@link fingerprintFinding} is computed. */
|
|
61
77
|
fingerprint?: string;
|
|
62
78
|
}
|
package/dist/finding.js
CHANGED
|
@@ -62,6 +62,8 @@ export function createFinding(spec) {
|
|
|
62
62
|
finding.detail = spec.detail;
|
|
63
63
|
if (spec.location !== undefined)
|
|
64
64
|
finding.location = spec.location;
|
|
65
|
+
if (spec.salientKey !== undefined)
|
|
66
|
+
finding.salientKey = spec.salientKey;
|
|
65
67
|
if (spec.data !== undefined)
|
|
66
68
|
finding.data = spec.data;
|
|
67
69
|
finding.fingerprint = spec.fingerprint ?? fingerprintFinding(finding);
|
|
@@ -98,6 +100,10 @@ export function fingerprintFinding(finding) {
|
|
|
98
100
|
fileNormalized,
|
|
99
101
|
finding.location?.line ?? '',
|
|
100
102
|
finding.location?.column ?? '',
|
|
103
|
+
// salientKey lets multiple distinct findings at the same (kind, file, line)
|
|
104
|
+
// site keep separate fingerprints. Empty string when absent so the hash
|
|
105
|
+
// shape is stable across findings that don't need a discriminator.
|
|
106
|
+
finding.salientKey ?? '',
|
|
101
107
|
];
|
|
102
108
|
return createHash('sha256').update(parts.join('|')).digest('hex').slice(0, 16);
|
|
103
109
|
}
|
|
@@ -109,6 +115,7 @@ const FINDING_ALLOWED_KEYS = new Set([
|
|
|
109
115
|
'detail',
|
|
110
116
|
'location',
|
|
111
117
|
'fingerprint',
|
|
118
|
+
'salientKey',
|
|
112
119
|
'data',
|
|
113
120
|
]);
|
|
114
121
|
const LOCATION_ALLOWED_KEYS = new Set(['file', 'line', 'column', 'endLine', 'endColumn']);
|
|
@@ -145,6 +152,9 @@ export function validateFinding(value) {
|
|
|
145
152
|
if (v.fingerprint !== undefined && typeof v.fingerprint !== 'string') {
|
|
146
153
|
errors.push('fingerprint must be a string when present');
|
|
147
154
|
}
|
|
155
|
+
if (v.salientKey !== undefined && typeof v.salientKey !== 'string') {
|
|
156
|
+
errors.push('salientKey must be a string when present');
|
|
157
|
+
}
|
|
148
158
|
if (v.data !== undefined && (v.data === null || typeof v.data !== 'object' || Array.isArray(v.data))) {
|
|
149
159
|
errors.push('data must be an object when present');
|
|
150
160
|
}
|
package/dist/locators.d.ts
CHANGED
|
@@ -11,12 +11,24 @@ export interface ByteRange {
|
|
|
11
11
|
/** Exclusive end offset. */
|
|
12
12
|
end: number;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* 1-based line number for the first occurrence of `"key"` followed by `:`.
|
|
16
|
+
*
|
|
17
|
+
* The key is JSON-encoded before matching so keys containing backslashes or
|
|
18
|
+
* quotes (rare but legal) are located in the source bytes. The scan ignores
|
|
19
|
+
* lines inside JSONC `//` and `/* *\/` comments so a commented-out `"key":`
|
|
20
|
+
* does not shadow the real one.
|
|
21
|
+
*/
|
|
15
22
|
export declare function lineOfJsonKey(text: string, key: string, scope?: ByteRange): number;
|
|
16
23
|
/**
|
|
17
24
|
* 1-based line number for the first JSON string value equal to `value`.
|
|
18
25
|
* If `scope` is supplied (a byte range), only matches inside that range count —
|
|
19
26
|
* this is the fix for the multi-server-ambiguity bug.
|
|
27
|
+
*
|
|
28
|
+
* The value is JSON-encoded before matching so values containing backslashes
|
|
29
|
+
* (e.g. Windows paths like `C:\Temp` written as `"C:\\Temp"` in JSON) are
|
|
30
|
+
* located correctly. The scan ignores JSONC comments so a commented-out
|
|
31
|
+
* matching value does not shadow the real one.
|
|
20
32
|
*/
|
|
21
33
|
export declare function lineOfJsonStringValue(text: string, value: string, scope?: ByteRange): number;
|
|
22
34
|
/**
|
package/dist/locators.js
CHANGED
|
@@ -5,19 +5,41 @@
|
|
|
5
5
|
* All returned line numbers are 1-based. `0` is reserved for "not found"; callers
|
|
6
6
|
* generally treat that as "fall back to file-level annotation".
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
import { stripJsonComments } from './jsonc.js';
|
|
9
|
+
/**
|
|
10
|
+
* 1-based line number for the first occurrence of `"key"` followed by `:`.
|
|
11
|
+
*
|
|
12
|
+
* The key is JSON-encoded before matching so keys containing backslashes or
|
|
13
|
+
* quotes (rare but legal) are located in the source bytes. The scan ignores
|
|
14
|
+
* lines inside JSONC `//` and `/* *\/` comments so a commented-out `"key":`
|
|
15
|
+
* does not shadow the real one.
|
|
16
|
+
*/
|
|
9
17
|
export function lineOfJsonKey(text, key, scope) {
|
|
10
|
-
const
|
|
11
|
-
return findLineByRegex(text, new RegExp(
|
|
18
|
+
const encoded = jsonEncodeForRegex(key);
|
|
19
|
+
return findLineByRegex(text, new RegExp(`"${encoded}"\\s*:`), scope);
|
|
12
20
|
}
|
|
13
21
|
/**
|
|
14
22
|
* 1-based line number for the first JSON string value equal to `value`.
|
|
15
23
|
* If `scope` is supplied (a byte range), only matches inside that range count —
|
|
16
24
|
* this is the fix for the multi-server-ambiguity bug.
|
|
25
|
+
*
|
|
26
|
+
* The value is JSON-encoded before matching so values containing backslashes
|
|
27
|
+
* (e.g. Windows paths like `C:\Temp` written as `"C:\\Temp"` in JSON) are
|
|
28
|
+
* located correctly. The scan ignores JSONC comments so a commented-out
|
|
29
|
+
* matching value does not shadow the real one.
|
|
17
30
|
*/
|
|
18
31
|
export function lineOfJsonStringValue(text, value, scope) {
|
|
19
|
-
const
|
|
20
|
-
return findLineByRegex(text, new RegExp(
|
|
32
|
+
const encoded = jsonEncodeForRegex(value);
|
|
33
|
+
return findLineByRegex(text, new RegExp(`"${encoded}"`), scope);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Convert a string to the form it would appear in JSON source bytes, then
|
|
37
|
+
* regex-escape. `JSON.stringify('C:\\Temp')` yields `'"C:\\\\Temp"'` — slice
|
|
38
|
+
* off the surrounding quotes to get the inner byte sequence.
|
|
39
|
+
*/
|
|
40
|
+
function jsonEncodeForRegex(input) {
|
|
41
|
+
const jsonBody = JSON.stringify(input).slice(1, -1);
|
|
42
|
+
return escapeForRegex(jsonBody);
|
|
21
43
|
}
|
|
22
44
|
/**
|
|
23
45
|
* 1-based line number for a TOML key. Supports dotted keys (`a.b.c`) — the
|
|
@@ -42,9 +64,19 @@ export function lineOfTomlKey(text, dottedKey, scope) {
|
|
|
42
64
|
let inTargetTable = prefix.length === 0;
|
|
43
65
|
let currentTable = [];
|
|
44
66
|
const targetHeader = prefix.join('.');
|
|
67
|
+
// Track multi-line basic (`"""`) and literal (`'''`) string state. A leaf-key
|
|
68
|
+
// pattern can otherwise match against decoy text inside a multi-line string
|
|
69
|
+
// value — see lineOfTomlKey regression tests.
|
|
70
|
+
let inMultilineString = null;
|
|
45
71
|
for (let i = 0; i < lines.length; i++) {
|
|
46
72
|
const lineNumber = i + 1;
|
|
47
73
|
const raw = lines[i];
|
|
74
|
+
const stateAtLineStart = inMultilineString;
|
|
75
|
+
inMultilineString = updateMultilineStringState(raw, inMultilineString);
|
|
76
|
+
// If we entered this line inside a multi-line string, never match. The key
|
|
77
|
+
// pattern there is part of a string literal, not a real assignment.
|
|
78
|
+
if (stateAtLineStart !== null)
|
|
79
|
+
continue;
|
|
48
80
|
const trimmed = raw.trim();
|
|
49
81
|
const headerMatch = /^\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/.exec(trimmed);
|
|
50
82
|
if (headerMatch) {
|
|
@@ -71,6 +103,43 @@ export function lineOfTomlKey(text, dottedKey, scope) {
|
|
|
71
103
|
}
|
|
72
104
|
return 0;
|
|
73
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Walk a line and update multi-line string state. Each unescaped occurrence of
|
|
108
|
+
* `"""` toggles basic-multiline; each `'''` toggles literal-multiline; the
|
|
109
|
+
* other delimiter is inert while we're inside the first. Returns the state at
|
|
110
|
+
* end-of-line so the next iteration knows whether it's inside a string.
|
|
111
|
+
*/
|
|
112
|
+
function updateMultilineStringState(line, current) {
|
|
113
|
+
let state = current;
|
|
114
|
+
let pos = 0;
|
|
115
|
+
while (pos <= line.length - 3) {
|
|
116
|
+
const window = line.substr(pos, 3);
|
|
117
|
+
if (state === null) {
|
|
118
|
+
if (window === '"""') {
|
|
119
|
+
state = '"""';
|
|
120
|
+
pos += 3;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (window === "'''") {
|
|
124
|
+
state = "'''";
|
|
125
|
+
pos += 3;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (state === '"""' && window === '"""') {
|
|
130
|
+
state = null;
|
|
131
|
+
pos += 3;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
else if (state === "'''" && window === "'''") {
|
|
135
|
+
state = null;
|
|
136
|
+
pos += 3;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
pos++;
|
|
140
|
+
}
|
|
141
|
+
return state;
|
|
142
|
+
}
|
|
74
143
|
function scopeLineFilter(text, scope) {
|
|
75
144
|
if (!scope)
|
|
76
145
|
return () => true;
|
|
@@ -79,7 +148,12 @@ function scopeLineFilter(text, scope) {
|
|
|
79
148
|
return (line) => line >= startLine && line <= endLine;
|
|
80
149
|
}
|
|
81
150
|
function findLineByRegex(text, regex, scope) {
|
|
82
|
-
|
|
151
|
+
// stripJsonComments is position-preserving: it replaces comment bytes with
|
|
152
|
+
// spaces while leaving newlines intact. Offsets in the stripped text map
|
|
153
|
+
// 1:1 to offsets in the original text, so line numbers stay correct, but
|
|
154
|
+
// commented-out keys/values no longer match.
|
|
155
|
+
const searchable = stripJsonComments(text);
|
|
156
|
+
const haystack = scope ? searchable.slice(scope.start, scope.end) : searchable;
|
|
83
157
|
const m = regex.exec(haystack);
|
|
84
158
|
if (!m)
|
|
85
159
|
return 0;
|
package/dist/shell.js
CHANGED
|
@@ -127,15 +127,43 @@ export function getCommandHead(subcommand) {
|
|
|
127
127
|
break;
|
|
128
128
|
s = s.slice(m[0].length);
|
|
129
129
|
}
|
|
130
|
-
// Strip leading sudo / env wrappers
|
|
131
|
-
|
|
130
|
+
// Strip leading sudo / env wrappers, then also strip any wrapper flags
|
|
131
|
+
// (`sudo -E`, `env -i`) and embedded env vars (`env FOO=1 BAZ=qux curl`)
|
|
132
|
+
// before recursing. Without this, `sudo -E curl` would return `-E`.
|
|
133
|
+
const wrapperMatch = /^(sudo|nohup|env|exec|command|builtin|stdbuf|nice|ionice|setsid)\s+(.*)$/.exec(s);
|
|
132
134
|
if (wrapperMatch) {
|
|
133
|
-
return getCommandHead(wrapperMatch[2]);
|
|
135
|
+
return getCommandHead(stripWrapperPrefixes(wrapperMatch[2]));
|
|
134
136
|
}
|
|
135
137
|
// Now extract first token, honoring quoting and obfuscation neutralization.
|
|
136
138
|
const head = readFirstToken(s);
|
|
137
139
|
return deobfuscate(head);
|
|
138
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Consume any leading flags (`-x`, `--xxx`, `--xxx=value`) and env var
|
|
143
|
+
* assignments (`FOO=bar`) so the next recursion finds the real command. We
|
|
144
|
+
* intentionally do NOT consume a non-flag token after a short flag (so
|
|
145
|
+
* `sudo -u user curl` still misclassifies as `user` — a known edge case
|
|
146
|
+
* that we accept rather than maintain a per-wrapper flag database).
|
|
147
|
+
*/
|
|
148
|
+
function stripWrapperPrefixes(input) {
|
|
149
|
+
let s = input.trimStart();
|
|
150
|
+
while (s.length > 0) {
|
|
151
|
+
if (s.startsWith('-')) {
|
|
152
|
+
const flagMatch = /^\S+\s*/.exec(s);
|
|
153
|
+
if (!flagMatch)
|
|
154
|
+
break;
|
|
155
|
+
s = s.slice(flagMatch[0].length);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const envMatch = /^([A-Za-z_][A-Za-z0-9_]*)=([^\s'"]*|"[^"]*"|'[^']*')\s+/.exec(s);
|
|
159
|
+
if (envMatch) {
|
|
160
|
+
s = s.slice(envMatch[0].length);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
return s;
|
|
166
|
+
}
|
|
139
167
|
function readFirstToken(s) {
|
|
140
168
|
let out = '';
|
|
141
169
|
let i = 0;
|
package/dist/toml.js
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-gov-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Shared primitives for the AI-agent governance suite: Finding schema, JSONC/TOML readers, line locators, MCP command normalization, shell tokenization, and GitHub Action helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"fingerprint": { "type": "string" },
|
|
44
|
+
"salientKey": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Optional discriminator that participates in the fingerprint hash. Set when a single (kind, file, line) site can produce multiple distinct findings (e.g. two suspicious imports on one line). Use a stable value — package name, server name, rule id — not a timestamp."
|
|
47
|
+
},
|
|
44
48
|
"data": { "type": "object" }
|
|
45
49
|
}
|
|
46
50
|
}
|