agent-gov-core 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/jsonc.js ADDED
@@ -0,0 +1,117 @@
1
+ import { readFileSync } from 'node:fs';
2
+ /**
3
+ * Strip `//` line comments, `/* ... *\/` block comments, and trailing commas from JSONC,
4
+ * preserving byte offsets (replacement is space-filled, newlines preserved) so downstream
5
+ * line locators still match the original `text`.
6
+ */
7
+ export function stripJsonComments(input) {
8
+ const len = input.length;
9
+ const out = new Array(len);
10
+ let i = 0;
11
+ let inString = null;
12
+ let escape = false;
13
+ while (i < len) {
14
+ const ch = input[i];
15
+ if (inString) {
16
+ out[i] = ch;
17
+ if (escape) {
18
+ escape = false;
19
+ }
20
+ else if (ch === '\\') {
21
+ escape = true;
22
+ }
23
+ else if (ch === inString) {
24
+ inString = null;
25
+ }
26
+ i++;
27
+ continue;
28
+ }
29
+ if (ch === '"') {
30
+ inString = '"';
31
+ out[i] = ch;
32
+ i++;
33
+ continue;
34
+ }
35
+ if (ch === '/' && i + 1 < len) {
36
+ const next = input[i + 1];
37
+ if (next === '/') {
38
+ // line comment until newline (exclusive)
39
+ let j = i;
40
+ while (j < len && input[j] !== '\n' && input[j] !== '\r') {
41
+ out[j] = ' ';
42
+ j++;
43
+ }
44
+ i = j;
45
+ continue;
46
+ }
47
+ if (next === '*') {
48
+ let j = i;
49
+ // replace until end of block comment (inclusive of */)
50
+ const end = input.indexOf('*/', i + 2);
51
+ const stop = end === -1 ? len : end + 2;
52
+ while (j < stop) {
53
+ const c = input[j];
54
+ out[j] = c === '\n' || c === '\r' ? c : ' ';
55
+ j++;
56
+ }
57
+ i = j;
58
+ continue;
59
+ }
60
+ }
61
+ out[i] = ch;
62
+ i++;
63
+ }
64
+ let result = out.join('');
65
+ // Strip trailing commas: `,` followed by optional whitespace then `}` or `]`.
66
+ // Only outside strings — but at this point we've reconstructed the source character-by-character,
67
+ // and the only commas we want to elide are structural ones. A safe pass: walk again with string-state.
68
+ result = stripTrailingCommas(result);
69
+ return result;
70
+ }
71
+ function stripTrailingCommas(input) {
72
+ const len = input.length;
73
+ const out = input.split('');
74
+ let inString = null;
75
+ let escape = false;
76
+ for (let i = 0; i < len; i++) {
77
+ const ch = out[i];
78
+ if (inString) {
79
+ if (escape)
80
+ escape = false;
81
+ else if (ch === '\\')
82
+ escape = true;
83
+ else if (ch === inString)
84
+ inString = null;
85
+ continue;
86
+ }
87
+ if (ch === '"') {
88
+ inString = '"';
89
+ continue;
90
+ }
91
+ if (ch === ',') {
92
+ let j = i + 1;
93
+ while (j < len && /\s/.test(out[j]))
94
+ j++;
95
+ if (j < len && (out[j] === '}' || out[j] === ']')) {
96
+ out[i] = ' ';
97
+ }
98
+ }
99
+ }
100
+ return out.join('');
101
+ }
102
+ /**
103
+ * Read a JSONC file and return both the parsed value and the original text.
104
+ * The original text is preserved exactly so line locators can operate on it.
105
+ */
106
+ export function readJsonObjectWithSource(path) {
107
+ const text = readFileSync(path, 'utf8');
108
+ try {
109
+ const stripped = stripJsonComments(text);
110
+ const json = JSON.parse(stripped);
111
+ return { json, text };
112
+ }
113
+ catch (err) {
114
+ return { json: undefined, text, parseError: err };
115
+ }
116
+ }
117
+ //# sourceMappingURL=jsonc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonc.js","sourceRoot":"","sources":["../src/jsonc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Line locators operate on the *original* source text — JSONC strip-comments
3
+ * is position-preserving so offsets line up.
4
+ *
5
+ * All returned line numbers are 1-based. `0` is reserved for "not found"; callers
6
+ * generally treat that as "fall back to file-level annotation".
7
+ */
8
+ export interface ByteRange {
9
+ /** Inclusive start offset. */
10
+ start: number;
11
+ /** Exclusive end offset. */
12
+ end: number;
13
+ }
14
+ /** 1-based line number for the first occurrence of `"key"` followed by `:`. */
15
+ export declare function lineOfJsonKey(text: string, key: string, scope?: ByteRange): number;
16
+ /**
17
+ * 1-based line number for the first JSON string value equal to `value`.
18
+ * If `scope` is supplied (a byte range), only matches inside that range count —
19
+ * this is the fix for the multi-server-ambiguity bug.
20
+ */
21
+ export declare function lineOfJsonStringValue(text: string, value: string, scope?: ByteRange): number;
22
+ /**
23
+ * 1-based line number for a TOML key. Supports dotted keys (`a.b.c`) — the
24
+ * locator points to the line where the *leaf* key is defined, scanning forward
25
+ * from the line of the matching `[a.b]` table header (or the start of the file
26
+ * for top-level keys). Bare and quoted leaf keys are both handled.
27
+ */
28
+ export declare function lineOfTomlKey(text: string, dottedKey: string): number;
29
+ //# sourceMappingURL=locators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locators.d.ts","sourceRoot":"","sources":["../src/locators.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,SAAS;IACxB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,MAAM,CAGlF;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,MAAM,CAG5F;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAwCrE"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Line locators operate on the *original* source text — JSONC strip-comments
3
+ * is position-preserving so offsets line up.
4
+ *
5
+ * All returned line numbers are 1-based. `0` is reserved for "not found"; callers
6
+ * generally treat that as "fall back to file-level annotation".
7
+ */
8
+ /** 1-based line number for the first occurrence of `"key"` followed by `:`. */
9
+ export function lineOfJsonKey(text, key, scope) {
10
+ const needle = `"${escapeForRegex(key)}"\\s*:`;
11
+ return findLineByRegex(text, new RegExp(needle), scope);
12
+ }
13
+ /**
14
+ * 1-based line number for the first JSON string value equal to `value`.
15
+ * If `scope` is supplied (a byte range), only matches inside that range count —
16
+ * this is the fix for the multi-server-ambiguity bug.
17
+ */
18
+ export function lineOfJsonStringValue(text, value, scope) {
19
+ const needle = `"${escapeForRegex(value)}"`;
20
+ return findLineByRegex(text, new RegExp(needle), scope);
21
+ }
22
+ /**
23
+ * 1-based line number for a TOML key. Supports dotted keys (`a.b.c`) — the
24
+ * locator points to the line where the *leaf* key is defined, scanning forward
25
+ * from the line of the matching `[a.b]` table header (or the start of the file
26
+ * for top-level keys). Bare and quoted leaf keys are both handled.
27
+ */
28
+ export function lineOfTomlKey(text, dottedKey) {
29
+ const parts = splitTomlDottedKey(dottedKey);
30
+ if (parts.length === 0)
31
+ return 0;
32
+ const leaf = parts[parts.length - 1];
33
+ const prefix = parts.slice(0, -1);
34
+ const lines = text.split(/\r?\n/);
35
+ // Find header range we're inside of.
36
+ let inTargetTable = prefix.length === 0;
37
+ let currentTable = [];
38
+ const targetHeader = prefix.join('.');
39
+ for (let i = 0; i < lines.length; i++) {
40
+ const raw = lines[i];
41
+ const trimmed = raw.trim();
42
+ const headerMatch = /^\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/.exec(trimmed);
43
+ if (headerMatch) {
44
+ currentTable = splitTomlDottedKey(headerMatch[1]);
45
+ inTargetTable = currentTable.join('.') === targetHeader;
46
+ continue;
47
+ }
48
+ if (!inTargetTable)
49
+ continue;
50
+ if (trimmed === '' || trimmed.startsWith('#'))
51
+ continue;
52
+ // Match leaf key at start of line: bare, "quoted", or 'literal'
53
+ const leafPattern = new RegExp(`^\\s*(?:${escapeForRegex(leaf)}|"${escapeForRegex(leaf)}"|'${escapeForRegex(leaf)}')\\s*(?:\\.|=)`);
54
+ if (leafPattern.test(raw))
55
+ return i + 1;
56
+ // Also: dotted key like `prefix.leaf = ...` defined at top-level
57
+ if (prefix.length > 0 && currentTable.length === 0) {
58
+ const dottedPattern = new RegExp(`^\\s*${escapeForRegex(dottedKey)}\\s*=`);
59
+ if (dottedPattern.test(raw))
60
+ return i + 1;
61
+ }
62
+ }
63
+ return 0;
64
+ }
65
+ function findLineByRegex(text, regex, scope) {
66
+ const haystack = scope ? text.slice(scope.start, scope.end) : text;
67
+ const m = regex.exec(haystack);
68
+ if (!m)
69
+ return 0;
70
+ const offset = (scope ? scope.start : 0) + m.index;
71
+ return lineOfOffset(text, offset);
72
+ }
73
+ function lineOfOffset(text, offset) {
74
+ let line = 1;
75
+ for (let i = 0; i < offset && i < text.length; i++) {
76
+ if (text[i] === '\n')
77
+ line++;
78
+ }
79
+ return line;
80
+ }
81
+ function escapeForRegex(s) {
82
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
83
+ }
84
+ /** Split a TOML dotted key, honoring "quoted" and 'literal' parts. */
85
+ function splitTomlDottedKey(input) {
86
+ const parts = [];
87
+ let i = 0;
88
+ const len = input.length;
89
+ while (i < len) {
90
+ while (i < len && (input[i] === ' ' || input[i] === '\t'))
91
+ i++;
92
+ if (i >= len)
93
+ break;
94
+ const c = input[i];
95
+ if (c === '"') {
96
+ i++;
97
+ const start = i;
98
+ while (i < len && input[i] !== '"') {
99
+ if (input[i] === '\\')
100
+ i++;
101
+ i++;
102
+ }
103
+ parts.push(input.slice(start, i));
104
+ i++;
105
+ }
106
+ else if (c === "'") {
107
+ i++;
108
+ const start = i;
109
+ while (i < len && input[i] !== "'")
110
+ i++;
111
+ parts.push(input.slice(start, i));
112
+ i++;
113
+ }
114
+ else {
115
+ const start = i;
116
+ while (i < len && input[i] !== '.' && input[i] !== ' ' && input[i] !== '\t')
117
+ i++;
118
+ parts.push(input.slice(start, i));
119
+ }
120
+ while (i < len && (input[i] === ' ' || input[i] === '\t'))
121
+ i++;
122
+ if (i < len && input[i] === '.')
123
+ i++;
124
+ }
125
+ return parts;
126
+ }
127
+ //# sourceMappingURL=locators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locators.js","sourceRoot":"","sources":["../src/locators.ts"],"names":[],"mappings":"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"}
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ export interface McpCommandSpec {
2
+ command?: string;
3
+ args?: readonly string[];
4
+ /** Some configs use a URL (SSE/remote MCP). */
5
+ url?: string;
6
+ env?: Readonly<Record<string, string>>;
7
+ cwd?: string;
8
+ }
9
+ /**
10
+ * Returns a canonical identity string for an MCP server command.
11
+ *
12
+ * Goals:
13
+ * - Two specs that differ only in cosmetic ways (flag reordering, `.cmd`/`.exe`
14
+ * on Windows, equivalent env var ordering) hash the same.
15
+ * - Specs that differ in anything *load-bearing* (the executable, the URL, the
16
+ * cwd, any env value, any non-neutral arg) hash differently.
17
+ *
18
+ * Non-goals:
19
+ * - Understanding tool semantics. Two truly-different `--flag value` pairs hash
20
+ * differently even if the tool would treat them equivalently.
21
+ *
22
+ * @example
23
+ * normalizeMcpCommand({ command: 'npx.cmd', args: ['-y', 'mcp-foo', '--token', 'abc'] });
24
+ * normalizeMcpCommand({ command: 'npx', args: ['mcp-foo', '--token', 'abc'] });
25
+ * // → both produce the same canonical string
26
+ *
27
+ * @example
28
+ * normalizeMcpCommand({ url: 'https://example.com/mcp/' });
29
+ * // → 'url=https://example.com/mcp\nargs='
30
+ */
31
+ export declare function normalizeMcpCommand(spec: McpCommandSpec): string;
32
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CA0BhE"}
package/dist/mcp.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Returns a canonical identity string for an MCP server command.
3
+ *
4
+ * Goals:
5
+ * - Two specs that differ only in cosmetic ways (flag reordering, `.cmd`/`.exe`
6
+ * on Windows, equivalent env var ordering) hash the same.
7
+ * - Specs that differ in anything *load-bearing* (the executable, the URL, the
8
+ * cwd, any env value, any non-neutral arg) hash differently.
9
+ *
10
+ * Non-goals:
11
+ * - Understanding tool semantics. Two truly-different `--flag value` pairs hash
12
+ * differently even if the tool would treat them equivalently.
13
+ *
14
+ * @example
15
+ * normalizeMcpCommand({ command: 'npx.cmd', args: ['-y', 'mcp-foo', '--token', 'abc'] });
16
+ * normalizeMcpCommand({ command: 'npx', args: ['mcp-foo', '--token', 'abc'] });
17
+ * // → both produce the same canonical string
18
+ *
19
+ * @example
20
+ * normalizeMcpCommand({ url: 'https://example.com/mcp/' });
21
+ * // → 'url=https://example.com/mcp\nargs='
22
+ */
23
+ export function normalizeMcpCommand(spec) {
24
+ const parts = [];
25
+ if (spec.url) {
26
+ parts.push(`url=${spec.url.trim().replace(/\/$/, '')}`);
27
+ }
28
+ if (spec.command) {
29
+ parts.push(`cmd=${normalizeExecutable(spec.command)}`);
30
+ }
31
+ const args = spec.args ?? [];
32
+ parts.push(`args=${canonicalizeArgs(args).join(' ')}`);
33
+ if (spec.cwd) {
34
+ parts.push(`cwd=${normalizePath(spec.cwd)}`);
35
+ }
36
+ if (spec.env) {
37
+ const env = Object.entries(spec.env)
38
+ .map(([k, v]) => `${k}=${v}`)
39
+ .sort();
40
+ parts.push(`env=${env.join('|')}`);
41
+ }
42
+ return parts.join('\n');
43
+ }
44
+ /** Strip `.cmd`/`.exe`/`.bat`/`.ps1` suffix and lowercase on Windows-style paths. */
45
+ function normalizeExecutable(cmd) {
46
+ const trimmed = cmd.trim();
47
+ const base = trimmed.replace(/\\/g, '/');
48
+ const withoutSuffix = base.replace(/\.(cmd|exe|bat|ps1)$/i, '');
49
+ return withoutSuffix;
50
+ }
51
+ function normalizePath(p) {
52
+ return p.trim().replace(/\\/g, '/').replace(/\/+$/, '');
53
+ }
54
+ /**
55
+ * Boolean flags that don't change *what* runs, just confirmation/verbosity.
56
+ * Dropped before canonicalization so e.g. `npx -y foo@1.2.3` and `npx foo@1.2.3`
57
+ * normalize identically. Keep this list conservative — only flags whose presence
58
+ * vs. absence is provably neutral across the runners that show up in MCP configs
59
+ * (npx, uvx, pipx, node).
60
+ */
61
+ const NEUTRAL_BOOLEAN_FLAGS = new Set(['-y', '--yes']);
62
+ /**
63
+ * Sort *neutral* flag/value pairs so reordering doesn't change identity, but
64
+ * preserve the order of positional arguments (which are usually load-bearing —
65
+ * e.g. `npx <package> <subcommand>`).
66
+ *
67
+ * Heuristic: an argument starting with `-` is a flag. A flag followed by a
68
+ * non-flag is treated as `--flag value` and the pair is sorted together. We
69
+ * keep them in two buckets: positional (order-preserved) and flag-pairs (sorted).
70
+ *
71
+ * Neutral boolean flags (see NEUTRAL_BOOLEAN_FLAGS) are dropped entirely so they
72
+ * never absorb a trailing positional as a fake `--flag value` pair.
73
+ */
74
+ function canonicalizeArgs(args) {
75
+ const filtered = args.filter((a) => !NEUTRAL_BOOLEAN_FLAGS.has(a));
76
+ const positional = [];
77
+ const flagPairs = [];
78
+ let sawFlag = false;
79
+ for (let i = 0; i < filtered.length; i++) {
80
+ const a = filtered[i];
81
+ if (a.startsWith('-')) {
82
+ sawFlag = true;
83
+ // `--key=value`
84
+ const eq = a.indexOf('=');
85
+ if (eq !== -1) {
86
+ flagPairs.push([a.slice(0, eq), a.slice(eq + 1)]);
87
+ continue;
88
+ }
89
+ const next = filtered[i + 1];
90
+ if (next !== undefined && !next.startsWith('-')) {
91
+ flagPairs.push([a, next]);
92
+ i++;
93
+ }
94
+ else {
95
+ flagPairs.push([a, null]);
96
+ }
97
+ continue;
98
+ }
99
+ if (sawFlag) {
100
+ // After flags have started, an unattached positional gets a deterministic position
101
+ // — push it into the sorted-pair bucket as a value-only entry keyed by itself.
102
+ flagPairs.push([`__pos__${a}`, null]);
103
+ }
104
+ else {
105
+ positional.push(a);
106
+ }
107
+ }
108
+ flagPairs.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
109
+ const out = [...positional];
110
+ for (const [k, v] of flagPairs) {
111
+ if (k.startsWith('__pos__'))
112
+ out.push(k.slice('__pos__'.length));
113
+ else if (v === null)
114
+ out.push(k);
115
+ else
116
+ out.push(`${k}=${v}`);
117
+ }
118
+ return out;
119
+ }
120
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Quote-aware split of a shell command into subcommands on `;`, `|`, `&&`, `||`.
3
+ * Does NOT execute the shell. Designed for static detection: SessionTrail's
4
+ * shell detector and future per-line CapabilityEcho rules call this to identify
5
+ * suspicious subcommands hidden behind chaining and basic obfuscation.
6
+ *
7
+ * @example
8
+ * tokenizeShell('echo hi && curl https://x.com/install.sh | bash');
9
+ * // → ['echo hi', 'curl https://x.com/install.sh', 'bash']
10
+ *
11
+ * tokenizeShell('echo "; not a separator"');
12
+ * // → ['echo "; not a separator"']
13
+ */
14
+ export declare function tokenizeShell(command: string): string[];
15
+ /**
16
+ * Returns the resolved command verb for a subcommand string. Strips wrapping
17
+ * quotes, escape backslashes, and the inert-double-quote obfuscation
18
+ * (`c""url` → `curl`, `c\\url` → `curl`).
19
+ *
20
+ * Returns an empty string if the input has no recognizable command head.
21
+ *
22
+ * @example
23
+ * getCommandHead('FOO=bar sudo curl -fsSL https://x.com');
24
+ * // → 'curl'
25
+ *
26
+ * getCommandHead('c""url -X POST');
27
+ * // → 'curl'
28
+ *
29
+ * getCommandHead('"/usr/bin/env" python3 -c "..."');
30
+ * // → '/usr/bin/env'
31
+ */
32
+ export declare function getCommandHead(subcommand: string): string;
33
+ //# sourceMappingURL=shell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAuFvD;AAOD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAiBzD"}