open-multi-agent-kit 0.90.5 → 0.90.7
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 +39 -1
- package/README.md +9 -9
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/codexbar-cli.d.ts +39 -0
- package/dist/codexbar-cli.d.ts.map +1 -0
- package/dist/codexbar-cli.js +230 -0
- package/dist/codexbar-cli.js.map +1 -0
- package/dist/commands/doctor-provider.d.ts.map +1 -1
- package/dist/commands/doctor-provider.js +16 -3
- package/dist/commands/doctor-provider.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +30 -8
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/codexbar-adapter.d.ts +44 -0
- package/dist/core/codexbar-adapter.d.ts.map +1 -0
- package/dist/core/codexbar-adapter.js +211 -0
- package/dist/core/codexbar-adapter.js.map +1 -0
- package/dist/core/command-safety.d.ts.map +1 -1
- package/dist/core/command-safety.js +7 -0
- package/dist/core/command-safety.js.map +1 -1
- package/dist/core/compaction/index.d.ts +2 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +2 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/model-policy.d.ts +9 -0
- package/dist/core/compaction/model-policy.d.ts.map +1 -0
- package/dist/core/compaction/model-policy.js +37 -0
- package/dist/core/compaction/model-policy.js.map +1 -0
- package/dist/core/compaction/resume-policy.d.ts +6 -0
- package/dist/core/compaction/resume-policy.d.ts.map +1 -0
- package/dist/core/compaction/resume-policy.js +8 -0
- package/dist/core/compaction/resume-policy.js.map +1 -0
- package/dist/core/domain-loadouts.d.ts.map +1 -1
- package/dist/core/domain-loadouts.js +68 -1
- package/dist/core/domain-loadouts.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +12 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +3 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +15 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/grok-harness-dispatch.d.ts +22 -0
- package/dist/core/grok-harness-dispatch.d.ts.map +1 -0
- package/dist/core/grok-harness-dispatch.js +44 -0
- package/dist/core/grok-harness-dispatch.js.map +1 -0
- package/dist/core/grok-harness.d.ts +25 -0
- package/dist/core/grok-harness.d.ts.map +1 -0
- package/dist/core/grok-harness.js +61 -0
- package/dist/core/grok-harness.js.map +1 -0
- package/dist/core/grok-playbook.d.ts +7 -0
- package/dist/core/grok-playbook.d.ts.map +1 -0
- package/dist/core/grok-playbook.js +34 -0
- package/dist/core/grok-playbook.js.map +1 -0
- package/dist/core/reasoning-router-v4-normalize.d.ts +11 -0
- package/dist/core/reasoning-router-v4-normalize.d.ts.map +1 -0
- package/dist/core/reasoning-router-v4-normalize.js +224 -0
- package/dist/core/reasoning-router-v4-normalize.js.map +1 -0
- package/dist/core/reasoning-router-v4-weights.d.ts +13 -5
- package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4-weights.js +8 -4
- package/dist/core/reasoning-router-v4-weights.js.map +1 -1
- package/dist/core/reasoning-router-v4.d.ts +3 -3
- package/dist/core/reasoning-router-v4.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4.js +394 -58
- package/dist/core/reasoning-router-v4.js.map +1 -1
- package/dist/core/router-feedback-collector.d.ts +1 -1
- package/dist/core/router-feedback-collector.d.ts.map +1 -1
- package/dist/core/router-feedback-collector.js +9 -1
- package/dist/core/router-feedback-collector.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +31 -12
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +1 -0
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +2 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +3 -2
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +42 -17
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/adaptorch-preview.md +84 -0
- package/docs/correctness-wall.md +117 -0
- package/docs/grok-harness.md +63 -0
- package/docs/loadout-domains/README.md +2 -1
- package/docs/loadout-domains/ai-agent-ops.md +8 -1
- package/docs/loadout-domains/frontend-ui.md +5 -1
- package/docs/loadout-domains/grok-harness.md +74 -0
- package/docs/providers.md +4 -0
- package/examples/extensions/correctness-wall/LOADOUT.md +87 -0
- package/examples/extensions/correctness-wall/README.md +111 -0
- package/examples/extensions/correctness-wall/adjudication-fixture.ts +151 -0
- package/examples/extensions/correctness-wall/index.ts +470 -0
- package/examples/extensions/correctness-wall/repair-state.ts +77 -0
- package/examples/extensions/correctness-wall/wall-cache.ts +81 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/preset.ts +2 -2
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +5 -5
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type CodexBarProvider = string;
|
|
2
|
+
export type CodexBarUsageWindowSummary = {
|
|
3
|
+
readonly usedPercent?: number;
|
|
4
|
+
readonly resetsAt?: string;
|
|
5
|
+
};
|
|
6
|
+
export type CodexBarStatusSummary = {
|
|
7
|
+
readonly indicator?: string;
|
|
8
|
+
readonly description?: string;
|
|
9
|
+
readonly url?: string;
|
|
10
|
+
};
|
|
11
|
+
export type CodexBarUsageSummary = {
|
|
12
|
+
readonly provider: CodexBarProvider;
|
|
13
|
+
readonly source?: string;
|
|
14
|
+
readonly primary?: CodexBarUsageWindowSummary;
|
|
15
|
+
readonly secondary?: CodexBarUsageWindowSummary;
|
|
16
|
+
readonly creditsRemaining?: number;
|
|
17
|
+
readonly updatedAt?: string;
|
|
18
|
+
readonly status?: CodexBarStatusSummary;
|
|
19
|
+
};
|
|
20
|
+
export type CodexBarCostSummary = {
|
|
21
|
+
readonly provider: CodexBarProvider;
|
|
22
|
+
readonly source?: string;
|
|
23
|
+
readonly sessionCostUSD?: number;
|
|
24
|
+
readonly last30DaysCostUSD?: number;
|
|
25
|
+
readonly totalCostUSD?: number;
|
|
26
|
+
readonly totalTokens?: number;
|
|
27
|
+
readonly updatedAt?: string;
|
|
28
|
+
};
|
|
29
|
+
export declare class CodexBarJsonError extends Error {
|
|
30
|
+
readonly name = "CodexBarJsonError";
|
|
31
|
+
readonly reason: string;
|
|
32
|
+
readonly inputLength: number;
|
|
33
|
+
constructor(reason: string, inputLength: number, options?: ErrorOptions);
|
|
34
|
+
}
|
|
35
|
+
export declare class CodexBarUnsafeOutputError extends Error {
|
|
36
|
+
readonly name = "CodexBarUnsafeOutputError";
|
|
37
|
+
readonly jsonPath: string;
|
|
38
|
+
constructor(jsonPath: string);
|
|
39
|
+
}
|
|
40
|
+
export declare function parseCodexBarUsageJson(stdout: string): CodexBarUsageSummary;
|
|
41
|
+
export declare function parseCodexBarCostJson(stdout: string): readonly CodexBarCostSummary[];
|
|
42
|
+
export declare function redactCodexBarJson(value: unknown): unknown;
|
|
43
|
+
export declare function hasSecretLikeValue(value: unknown): boolean;
|
|
44
|
+
//# sourceMappingURL=codexbar-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codexbar-adapter.d.ts","sourceRoot":"","sources":["../../src/core/codexbar-adapter.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEtC,MAAM,MAAM,0BAA0B,GAAG;IACxC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAClC,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,0BAA0B,CAAC;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,0BAA0B,CAAC;IAChD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,qBAAqB,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IACjC,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,qBAAa,iBAAkB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,uBAAuB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,YAAY,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,EAItE;CACD;AAED,qBAAa,yBAA0B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,+BAA+B;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,YAAY,QAAQ,EAAE,MAAM,EAG3B;CACD;AAkBD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,oBAAoB,CA6B3E;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,mBAAmB,EAAE,CAYpF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D","sourcesContent":["export type CodexBarProvider = string;\n\nexport type CodexBarUsageWindowSummary = {\n\treadonly usedPercent?: number;\n\treadonly resetsAt?: string;\n};\n\nexport type CodexBarStatusSummary = {\n\treadonly indicator?: string;\n\treadonly description?: string;\n\treadonly url?: string;\n};\n\nexport type CodexBarUsageSummary = {\n\treadonly provider: CodexBarProvider;\n\treadonly source?: string;\n\treadonly primary?: CodexBarUsageWindowSummary;\n\treadonly secondary?: CodexBarUsageWindowSummary;\n\treadonly creditsRemaining?: number;\n\treadonly updatedAt?: string;\n\treadonly status?: CodexBarStatusSummary;\n};\n\nexport type CodexBarCostSummary = {\n\treadonly provider: CodexBarProvider;\n\treadonly source?: string;\n\treadonly sessionCostUSD?: number;\n\treadonly last30DaysCostUSD?: number;\n\treadonly totalCostUSD?: number;\n\treadonly totalTokens?: number;\n\treadonly updatedAt?: string;\n};\n\nexport class CodexBarJsonError extends Error {\n\treadonly name = \"CodexBarJsonError\";\n\treadonly reason: string;\n\treadonly inputLength: number;\n\n\tconstructor(reason: string, inputLength: number, options?: ErrorOptions) {\n\t\tsuper(reason, options);\n\t\tthis.reason = reason;\n\t\tthis.inputLength = inputLength;\n\t}\n}\n\nexport class CodexBarUnsafeOutputError extends Error {\n\treadonly name = \"CodexBarUnsafeOutputError\";\n\treadonly jsonPath: string;\n\n\tconstructor(jsonPath: string) {\n\t\tsuper(`CodexBar output contains a credential-like value at ${jsonPath}`);\n\t\tthis.jsonPath = jsonPath;\n\t}\n}\n\nconst REDACTED_FIELD_NAMES = [\n\t\"accountEmail\",\n\t\"accountOrganization\",\n\t\"signedInEmail\",\n\t\"creditEvents\",\n\t\"dailyBreakdown\",\n] as const;\n\nconst SECRET_LIKE_VALUE_PATTERNS = [\n\tnew RegExp(`${\"Bear\"}${\"er\"}\\\\s+\\\\S+`, \"i\"),\n\tnew RegExp(`${\"Cook\"}${\"ie\"}:\\\\s*`, \"i\"),\n\tnew RegExp(`\\\\b${\"s\"}${\"k\"}-[A-Za-z0-9_-]*`, \"i\"),\n\tnew RegExp(`\\\\b${\"g\"}${\"hp\"}_[A-Za-z0-9_]+`, \"i\"),\n\tnew RegExp(`\\\\b${\"e\"}${\"y\"}${\"J\"}[A-Za-z0-9_-]{8,}(?:\\\\.[A-Za-z0-9_-]+){0,2}\\\\b`),\n] as const;\n\nexport function parseCodexBarUsageJson(stdout: string): CodexBarUsageSummary {\n\tconst parsed = parseJson(stdout);\n\tif (!isRecord(parsed)) {\n\t\tthrow new CodexBarJsonError(\"CodexBar usage JSON must be an object\", stdout.length);\n\t}\n\n\tconst usage = readRecord(parsed, \"usage\");\n\tconst credits = readRecord(parsed, \"credits\");\n\tconst statusRecord = readRecord(parsed, \"status\");\n\tconst primary = usage ? parseUsageWindow(usage.primary) : undefined;\n\tconst secondary = usage ? parseUsageWindow(usage.secondary) : undefined;\n\tconst status = parseStatus(statusRecord);\n\tconst source = readString(parsed, \"source\");\n\tconst creditsRemaining = credits ? readNumber(credits, \"remaining\") : undefined;\n\tconst updatedAt =\n\t\t(usage ? readString(usage, \"updatedAt\") : undefined) ??\n\t\treadString(parsed, \"updatedAt\") ??\n\t\t(credits ? readString(credits, \"updatedAt\") : undefined) ??\n\t\t(statusRecord ? readString(statusRecord, \"updatedAt\") : undefined);\n\n\treturn {\n\t\tprovider: readString(parsed, \"provider\") ?? \"unknown\",\n\t\t...(source === undefined ? {} : { source }),\n\t\t...(primary === undefined ? {} : { primary }),\n\t\t...(secondary === undefined ? {} : { secondary }),\n\t\t...(creditsRemaining === undefined ? {} : { creditsRemaining }),\n\t\t...(updatedAt === undefined ? {} : { updatedAt }),\n\t\t...(status === undefined ? {} : { status }),\n\t};\n}\n\nexport function parseCodexBarCostJson(stdout: string): readonly CodexBarCostSummary[] {\n\tconst parsed = parseJson(stdout);\n\tif (!isUnknownArray(parsed)) {\n\t\tthrow new CodexBarJsonError(\"CodexBar cost JSON must be an array\", stdout.length);\n\t}\n\n\treturn parsed.map((entry) => {\n\t\tif (!isRecord(entry)) {\n\t\t\tthrow new CodexBarJsonError(\"CodexBar cost entries must be objects\", stdout.length);\n\t\t}\n\t\treturn parseCostSummary(entry);\n\t});\n}\n\nexport function redactCodexBarJson(value: unknown): unknown {\n\treturn redactValue(value, undefined);\n}\n\nexport function hasSecretLikeValue(value: unknown): boolean {\n\treturn findUnsafeJsonPath(value, \"$\") !== undefined;\n}\n\nfunction parseJson(stdout: string): unknown {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(stdout);\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\tthrow new CodexBarJsonError(\"CodexBar output is not valid JSON\", stdout.length, { cause: error });\n\t\t}\n\t\tthrow error;\n\t}\n\n\tconst unsafeJsonPath = findUnsafeJsonPath(parsed, \"$.\");\n\tif (unsafeJsonPath !== undefined) {\n\t\tthrow new CodexBarUnsafeOutputError(unsafeJsonPath);\n\t}\n\treturn parsed;\n}\n\nfunction parseUsageWindow(value: unknown): CodexBarUsageWindowSummary | undefined {\n\tif (!isRecord(value)) {\n\t\treturn undefined;\n\t}\n\n\tconst usedPercent = readNumber(value, \"usedPercent\");\n\tconst resetsAt = readString(value, \"resetsAt\");\n\tif (usedPercent === undefined && resetsAt === undefined) {\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\t...(usedPercent === undefined ? {} : { usedPercent }),\n\t\t...(resetsAt === undefined ? {} : { resetsAt }),\n\t};\n}\n\nfunction parseStatus(value: Record<string, unknown> | undefined): CodexBarStatusSummary | undefined {\n\tif (value === undefined) {\n\t\treturn undefined;\n\t}\n\n\tconst indicator = readString(value, \"indicator\");\n\tconst description = readString(value, \"description\");\n\tconst url = readString(value, \"url\");\n\tif (indicator === undefined && description === undefined && url === undefined) {\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\t...(indicator === undefined ? {} : { indicator }),\n\t\t...(description === undefined ? {} : { description }),\n\t\t...(url === undefined ? {} : { url }),\n\t};\n}\n\nfunction parseCostSummary(entry: Record<string, unknown>): CodexBarCostSummary {\n\tconst totals = readRecord(entry, \"totals\");\n\tconst source = readString(entry, \"source\");\n\tconst sessionCostUSD = readNumber(entry, \"sessionCostUSD\");\n\tconst last30DaysCostUSD = readNumber(entry, \"last30DaysCostUSD\");\n\tconst totalCostUSD = readNumber(entry, \"totalCostUSD\") ?? (totals ? readNumber(totals, \"totalCost\") : undefined);\n\tconst totalTokens = readNumber(entry, \"totalTokens\") ?? (totals ? readNumber(totals, \"totalTokens\") : undefined);\n\tconst updatedAt = readString(entry, \"updatedAt\");\n\n\treturn {\n\t\tprovider: readString(entry, \"provider\") ?? \"unknown\",\n\t\t...(source === undefined ? {} : { source }),\n\t\t...(sessionCostUSD === undefined ? {} : { sessionCostUSD }),\n\t\t...(last30DaysCostUSD === undefined ? {} : { last30DaysCostUSD }),\n\t\t...(totalCostUSD === undefined ? {} : { totalCostUSD }),\n\t\t...(totalTokens === undefined ? {} : { totalTokens }),\n\t\t...(updatedAt === undefined ? {} : { updatedAt }),\n\t};\n}\n\nfunction redactValue(value: unknown, parentKey: string | undefined): unknown {\n\tif (isUnknownArray(value)) {\n\t\treturn value.map((item) => redactValue(item, parentKey));\n\t}\n\tif (!isRecord(value)) {\n\t\treturn value;\n\t}\n\n\tconst redacted: Record<string, unknown> = {};\n\tfor (const [key, fieldValue] of Object.entries(value)) {\n\t\tif (shouldDropField(key, parentKey)) {\n\t\t\tcontinue;\n\t\t}\n\t\tredacted[key] = redactValue(fieldValue, key);\n\t}\n\treturn redacted;\n}\n\nfunction shouldDropField(key: string, parentKey: string | undefined): boolean {\n\tif (REDACTED_FIELD_NAMES.some((fieldName) => fieldName === key)) {\n\t\treturn true;\n\t}\n\treturn parentKey === \"projects\" && (key === \"path\" || key === \"name\");\n}\n\nfunction findUnsafeJsonPath(value: unknown, jsonPath: string): string | undefined {\n\tif (typeof value === \"string\") {\n\t\treturn isSecretLikeString(value) ? jsonPath : undefined;\n\t}\n\tif (isUnknownArray(value)) {\n\t\tfor (const [index, item] of value.entries()) {\n\t\t\tconst found = findUnsafeJsonPath(item, `${jsonPath}[${index}]`);\n\t\t\tif (found !== undefined) {\n\t\t\t\treturn found;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\tif (!isRecord(value)) {\n\t\treturn undefined;\n\t}\n\n\tfor (const [key, fieldValue] of Object.entries(value)) {\n\t\tconst separator = jsonPath.endsWith(\".\") ? \"\" : \".\";\n\t\tconst found = findUnsafeJsonPath(fieldValue, `${jsonPath}${separator}${key}`);\n\t\tif (found !== undefined) {\n\t\t\treturn found;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction isSecretLikeString(value: string): boolean {\n\treturn SECRET_LIKE_VALUE_PATTERNS.some((pattern) => pattern.test(value));\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isUnknownArray(value: unknown): value is readonly unknown[] {\n\treturn Array.isArray(value);\n}\n\nfunction readRecord(record: Record<string, unknown>, key: string): Record<string, unknown> | undefined {\n\tconst value = record[key];\n\treturn isRecord(value) ? value : undefined;\n}\n\nfunction readString(record: Record<string, unknown>, key: string): string | undefined {\n\tconst value = record[key];\n\treturn typeof value === \"string\" ? value : undefined;\n}\n\nfunction readNumber(record: Record<string, unknown>, key: string): number | undefined {\n\tconst value = record[key];\n\treturn typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n"]}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
export class CodexBarJsonError extends Error {
|
|
2
|
+
name = "CodexBarJsonError";
|
|
3
|
+
reason;
|
|
4
|
+
inputLength;
|
|
5
|
+
constructor(reason, inputLength, options) {
|
|
6
|
+
super(reason, options);
|
|
7
|
+
this.reason = reason;
|
|
8
|
+
this.inputLength = inputLength;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class CodexBarUnsafeOutputError extends Error {
|
|
12
|
+
name = "CodexBarUnsafeOutputError";
|
|
13
|
+
jsonPath;
|
|
14
|
+
constructor(jsonPath) {
|
|
15
|
+
super(`CodexBar output contains a credential-like value at ${jsonPath}`);
|
|
16
|
+
this.jsonPath = jsonPath;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const REDACTED_FIELD_NAMES = [
|
|
20
|
+
"accountEmail",
|
|
21
|
+
"accountOrganization",
|
|
22
|
+
"signedInEmail",
|
|
23
|
+
"creditEvents",
|
|
24
|
+
"dailyBreakdown",
|
|
25
|
+
];
|
|
26
|
+
const SECRET_LIKE_VALUE_PATTERNS = [
|
|
27
|
+
new RegExp(`${"Bear"}${"er"}\\s+\\S+`, "i"),
|
|
28
|
+
new RegExp(`${"Cook"}${"ie"}:\\s*`, "i"),
|
|
29
|
+
new RegExp(`\\b${"s"}${"k"}-[A-Za-z0-9_-]*`, "i"),
|
|
30
|
+
new RegExp(`\\b${"g"}${"hp"}_[A-Za-z0-9_]+`, "i"),
|
|
31
|
+
new RegExp(`\\b${"e"}${"y"}${"J"}[A-Za-z0-9_-]{8,}(?:\\.[A-Za-z0-9_-]+){0,2}\\b`),
|
|
32
|
+
];
|
|
33
|
+
export function parseCodexBarUsageJson(stdout) {
|
|
34
|
+
const parsed = parseJson(stdout);
|
|
35
|
+
if (!isRecord(parsed)) {
|
|
36
|
+
throw new CodexBarJsonError("CodexBar usage JSON must be an object", stdout.length);
|
|
37
|
+
}
|
|
38
|
+
const usage = readRecord(parsed, "usage");
|
|
39
|
+
const credits = readRecord(parsed, "credits");
|
|
40
|
+
const statusRecord = readRecord(parsed, "status");
|
|
41
|
+
const primary = usage ? parseUsageWindow(usage.primary) : undefined;
|
|
42
|
+
const secondary = usage ? parseUsageWindow(usage.secondary) : undefined;
|
|
43
|
+
const status = parseStatus(statusRecord);
|
|
44
|
+
const source = readString(parsed, "source");
|
|
45
|
+
const creditsRemaining = credits ? readNumber(credits, "remaining") : undefined;
|
|
46
|
+
const updatedAt = (usage ? readString(usage, "updatedAt") : undefined) ??
|
|
47
|
+
readString(parsed, "updatedAt") ??
|
|
48
|
+
(credits ? readString(credits, "updatedAt") : undefined) ??
|
|
49
|
+
(statusRecord ? readString(statusRecord, "updatedAt") : undefined);
|
|
50
|
+
return {
|
|
51
|
+
provider: readString(parsed, "provider") ?? "unknown",
|
|
52
|
+
...(source === undefined ? {} : { source }),
|
|
53
|
+
...(primary === undefined ? {} : { primary }),
|
|
54
|
+
...(secondary === undefined ? {} : { secondary }),
|
|
55
|
+
...(creditsRemaining === undefined ? {} : { creditsRemaining }),
|
|
56
|
+
...(updatedAt === undefined ? {} : { updatedAt }),
|
|
57
|
+
...(status === undefined ? {} : { status }),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function parseCodexBarCostJson(stdout) {
|
|
61
|
+
const parsed = parseJson(stdout);
|
|
62
|
+
if (!isUnknownArray(parsed)) {
|
|
63
|
+
throw new CodexBarJsonError("CodexBar cost JSON must be an array", stdout.length);
|
|
64
|
+
}
|
|
65
|
+
return parsed.map((entry) => {
|
|
66
|
+
if (!isRecord(entry)) {
|
|
67
|
+
throw new CodexBarJsonError("CodexBar cost entries must be objects", stdout.length);
|
|
68
|
+
}
|
|
69
|
+
return parseCostSummary(entry);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export function redactCodexBarJson(value) {
|
|
73
|
+
return redactValue(value, undefined);
|
|
74
|
+
}
|
|
75
|
+
export function hasSecretLikeValue(value) {
|
|
76
|
+
return findUnsafeJsonPath(value, "$") !== undefined;
|
|
77
|
+
}
|
|
78
|
+
function parseJson(stdout) {
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(stdout);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof SyntaxError) {
|
|
85
|
+
throw new CodexBarJsonError("CodexBar output is not valid JSON", stdout.length, { cause: error });
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
const unsafeJsonPath = findUnsafeJsonPath(parsed, "$.");
|
|
90
|
+
if (unsafeJsonPath !== undefined) {
|
|
91
|
+
throw new CodexBarUnsafeOutputError(unsafeJsonPath);
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
function parseUsageWindow(value) {
|
|
96
|
+
if (!isRecord(value)) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const usedPercent = readNumber(value, "usedPercent");
|
|
100
|
+
const resetsAt = readString(value, "resetsAt");
|
|
101
|
+
if (usedPercent === undefined && resetsAt === undefined) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
...(usedPercent === undefined ? {} : { usedPercent }),
|
|
106
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function parseStatus(value) {
|
|
110
|
+
if (value === undefined) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const indicator = readString(value, "indicator");
|
|
114
|
+
const description = readString(value, "description");
|
|
115
|
+
const url = readString(value, "url");
|
|
116
|
+
if (indicator === undefined && description === undefined && url === undefined) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
...(indicator === undefined ? {} : { indicator }),
|
|
121
|
+
...(description === undefined ? {} : { description }),
|
|
122
|
+
...(url === undefined ? {} : { url }),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function parseCostSummary(entry) {
|
|
126
|
+
const totals = readRecord(entry, "totals");
|
|
127
|
+
const source = readString(entry, "source");
|
|
128
|
+
const sessionCostUSD = readNumber(entry, "sessionCostUSD");
|
|
129
|
+
const last30DaysCostUSD = readNumber(entry, "last30DaysCostUSD");
|
|
130
|
+
const totalCostUSD = readNumber(entry, "totalCostUSD") ?? (totals ? readNumber(totals, "totalCost") : undefined);
|
|
131
|
+
const totalTokens = readNumber(entry, "totalTokens") ?? (totals ? readNumber(totals, "totalTokens") : undefined);
|
|
132
|
+
const updatedAt = readString(entry, "updatedAt");
|
|
133
|
+
return {
|
|
134
|
+
provider: readString(entry, "provider") ?? "unknown",
|
|
135
|
+
...(source === undefined ? {} : { source }),
|
|
136
|
+
...(sessionCostUSD === undefined ? {} : { sessionCostUSD }),
|
|
137
|
+
...(last30DaysCostUSD === undefined ? {} : { last30DaysCostUSD }),
|
|
138
|
+
...(totalCostUSD === undefined ? {} : { totalCostUSD }),
|
|
139
|
+
...(totalTokens === undefined ? {} : { totalTokens }),
|
|
140
|
+
...(updatedAt === undefined ? {} : { updatedAt }),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function redactValue(value, parentKey) {
|
|
144
|
+
if (isUnknownArray(value)) {
|
|
145
|
+
return value.map((item) => redactValue(item, parentKey));
|
|
146
|
+
}
|
|
147
|
+
if (!isRecord(value)) {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
const redacted = {};
|
|
151
|
+
for (const [key, fieldValue] of Object.entries(value)) {
|
|
152
|
+
if (shouldDropField(key, parentKey)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
redacted[key] = redactValue(fieldValue, key);
|
|
156
|
+
}
|
|
157
|
+
return redacted;
|
|
158
|
+
}
|
|
159
|
+
function shouldDropField(key, parentKey) {
|
|
160
|
+
if (REDACTED_FIELD_NAMES.some((fieldName) => fieldName === key)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
return parentKey === "projects" && (key === "path" || key === "name");
|
|
164
|
+
}
|
|
165
|
+
function findUnsafeJsonPath(value, jsonPath) {
|
|
166
|
+
if (typeof value === "string") {
|
|
167
|
+
return isSecretLikeString(value) ? jsonPath : undefined;
|
|
168
|
+
}
|
|
169
|
+
if (isUnknownArray(value)) {
|
|
170
|
+
for (const [index, item] of value.entries()) {
|
|
171
|
+
const found = findUnsafeJsonPath(item, `${jsonPath}[${index}]`);
|
|
172
|
+
if (found !== undefined) {
|
|
173
|
+
return found;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
if (!isRecord(value)) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
for (const [key, fieldValue] of Object.entries(value)) {
|
|
182
|
+
const separator = jsonPath.endsWith(".") ? "" : ".";
|
|
183
|
+
const found = findUnsafeJsonPath(fieldValue, `${jsonPath}${separator}${key}`);
|
|
184
|
+
if (found !== undefined) {
|
|
185
|
+
return found;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
function isSecretLikeString(value) {
|
|
191
|
+
return SECRET_LIKE_VALUE_PATTERNS.some((pattern) => pattern.test(value));
|
|
192
|
+
}
|
|
193
|
+
function isRecord(value) {
|
|
194
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
195
|
+
}
|
|
196
|
+
function isUnknownArray(value) {
|
|
197
|
+
return Array.isArray(value);
|
|
198
|
+
}
|
|
199
|
+
function readRecord(record, key) {
|
|
200
|
+
const value = record[key];
|
|
201
|
+
return isRecord(value) ? value : undefined;
|
|
202
|
+
}
|
|
203
|
+
function readString(record, key) {
|
|
204
|
+
const value = record[key];
|
|
205
|
+
return typeof value === "string" ? value : undefined;
|
|
206
|
+
}
|
|
207
|
+
function readNumber(record, key) {
|
|
208
|
+
const value = record[key];
|
|
209
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=codexbar-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codexbar-adapter.js","sourceRoot":"","sources":["../../src/core/codexbar-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["export type CodexBarProvider = string;\n\nexport type CodexBarUsageWindowSummary = {\n\treadonly usedPercent?: number;\n\treadonly resetsAt?: string;\n};\n\nexport type CodexBarStatusSummary = {\n\treadonly indicator?: string;\n\treadonly description?: string;\n\treadonly url?: string;\n};\n\nexport type CodexBarUsageSummary = {\n\treadonly provider: CodexBarProvider;\n\treadonly source?: string;\n\treadonly primary?: CodexBarUsageWindowSummary;\n\treadonly secondary?: CodexBarUsageWindowSummary;\n\treadonly creditsRemaining?: number;\n\treadonly updatedAt?: string;\n\treadonly status?: CodexBarStatusSummary;\n};\n\nexport type CodexBarCostSummary = {\n\treadonly provider: CodexBarProvider;\n\treadonly source?: string;\n\treadonly sessionCostUSD?: number;\n\treadonly last30DaysCostUSD?: number;\n\treadonly totalCostUSD?: number;\n\treadonly totalTokens?: number;\n\treadonly updatedAt?: string;\n};\n\nexport class CodexBarJsonError extends Error {\n\treadonly name = \"CodexBarJsonError\";\n\treadonly reason: string;\n\treadonly inputLength: number;\n\n\tconstructor(reason: string, inputLength: number, options?: ErrorOptions) {\n\t\tsuper(reason, options);\n\t\tthis.reason = reason;\n\t\tthis.inputLength = inputLength;\n\t}\n}\n\nexport class CodexBarUnsafeOutputError extends Error {\n\treadonly name = \"CodexBarUnsafeOutputError\";\n\treadonly jsonPath: string;\n\n\tconstructor(jsonPath: string) {\n\t\tsuper(`CodexBar output contains a credential-like value at ${jsonPath}`);\n\t\tthis.jsonPath = jsonPath;\n\t}\n}\n\nconst REDACTED_FIELD_NAMES = [\n\t\"accountEmail\",\n\t\"accountOrganization\",\n\t\"signedInEmail\",\n\t\"creditEvents\",\n\t\"dailyBreakdown\",\n] as const;\n\nconst SECRET_LIKE_VALUE_PATTERNS = [\n\tnew RegExp(`${\"Bear\"}${\"er\"}\\\\s+\\\\S+`, \"i\"),\n\tnew RegExp(`${\"Cook\"}${\"ie\"}:\\\\s*`, \"i\"),\n\tnew RegExp(`\\\\b${\"s\"}${\"k\"}-[A-Za-z0-9_-]*`, \"i\"),\n\tnew RegExp(`\\\\b${\"g\"}${\"hp\"}_[A-Za-z0-9_]+`, \"i\"),\n\tnew RegExp(`\\\\b${\"e\"}${\"y\"}${\"J\"}[A-Za-z0-9_-]{8,}(?:\\\\.[A-Za-z0-9_-]+){0,2}\\\\b`),\n] as const;\n\nexport function parseCodexBarUsageJson(stdout: string): CodexBarUsageSummary {\n\tconst parsed = parseJson(stdout);\n\tif (!isRecord(parsed)) {\n\t\tthrow new CodexBarJsonError(\"CodexBar usage JSON must be an object\", stdout.length);\n\t}\n\n\tconst usage = readRecord(parsed, \"usage\");\n\tconst credits = readRecord(parsed, \"credits\");\n\tconst statusRecord = readRecord(parsed, \"status\");\n\tconst primary = usage ? parseUsageWindow(usage.primary) : undefined;\n\tconst secondary = usage ? parseUsageWindow(usage.secondary) : undefined;\n\tconst status = parseStatus(statusRecord);\n\tconst source = readString(parsed, \"source\");\n\tconst creditsRemaining = credits ? readNumber(credits, \"remaining\") : undefined;\n\tconst updatedAt =\n\t\t(usage ? readString(usage, \"updatedAt\") : undefined) ??\n\t\treadString(parsed, \"updatedAt\") ??\n\t\t(credits ? readString(credits, \"updatedAt\") : undefined) ??\n\t\t(statusRecord ? readString(statusRecord, \"updatedAt\") : undefined);\n\n\treturn {\n\t\tprovider: readString(parsed, \"provider\") ?? \"unknown\",\n\t\t...(source === undefined ? {} : { source }),\n\t\t...(primary === undefined ? {} : { primary }),\n\t\t...(secondary === undefined ? {} : { secondary }),\n\t\t...(creditsRemaining === undefined ? {} : { creditsRemaining }),\n\t\t...(updatedAt === undefined ? {} : { updatedAt }),\n\t\t...(status === undefined ? {} : { status }),\n\t};\n}\n\nexport function parseCodexBarCostJson(stdout: string): readonly CodexBarCostSummary[] {\n\tconst parsed = parseJson(stdout);\n\tif (!isUnknownArray(parsed)) {\n\t\tthrow new CodexBarJsonError(\"CodexBar cost JSON must be an array\", stdout.length);\n\t}\n\n\treturn parsed.map((entry) => {\n\t\tif (!isRecord(entry)) {\n\t\t\tthrow new CodexBarJsonError(\"CodexBar cost entries must be objects\", stdout.length);\n\t\t}\n\t\treturn parseCostSummary(entry);\n\t});\n}\n\nexport function redactCodexBarJson(value: unknown): unknown {\n\treturn redactValue(value, undefined);\n}\n\nexport function hasSecretLikeValue(value: unknown): boolean {\n\treturn findUnsafeJsonPath(value, \"$\") !== undefined;\n}\n\nfunction parseJson(stdout: string): unknown {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(stdout);\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\tthrow new CodexBarJsonError(\"CodexBar output is not valid JSON\", stdout.length, { cause: error });\n\t\t}\n\t\tthrow error;\n\t}\n\n\tconst unsafeJsonPath = findUnsafeJsonPath(parsed, \"$.\");\n\tif (unsafeJsonPath !== undefined) {\n\t\tthrow new CodexBarUnsafeOutputError(unsafeJsonPath);\n\t}\n\treturn parsed;\n}\n\nfunction parseUsageWindow(value: unknown): CodexBarUsageWindowSummary | undefined {\n\tif (!isRecord(value)) {\n\t\treturn undefined;\n\t}\n\n\tconst usedPercent = readNumber(value, \"usedPercent\");\n\tconst resetsAt = readString(value, \"resetsAt\");\n\tif (usedPercent === undefined && resetsAt === undefined) {\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\t...(usedPercent === undefined ? {} : { usedPercent }),\n\t\t...(resetsAt === undefined ? {} : { resetsAt }),\n\t};\n}\n\nfunction parseStatus(value: Record<string, unknown> | undefined): CodexBarStatusSummary | undefined {\n\tif (value === undefined) {\n\t\treturn undefined;\n\t}\n\n\tconst indicator = readString(value, \"indicator\");\n\tconst description = readString(value, \"description\");\n\tconst url = readString(value, \"url\");\n\tif (indicator === undefined && description === undefined && url === undefined) {\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\t...(indicator === undefined ? {} : { indicator }),\n\t\t...(description === undefined ? {} : { description }),\n\t\t...(url === undefined ? {} : { url }),\n\t};\n}\n\nfunction parseCostSummary(entry: Record<string, unknown>): CodexBarCostSummary {\n\tconst totals = readRecord(entry, \"totals\");\n\tconst source = readString(entry, \"source\");\n\tconst sessionCostUSD = readNumber(entry, \"sessionCostUSD\");\n\tconst last30DaysCostUSD = readNumber(entry, \"last30DaysCostUSD\");\n\tconst totalCostUSD = readNumber(entry, \"totalCostUSD\") ?? (totals ? readNumber(totals, \"totalCost\") : undefined);\n\tconst totalTokens = readNumber(entry, \"totalTokens\") ?? (totals ? readNumber(totals, \"totalTokens\") : undefined);\n\tconst updatedAt = readString(entry, \"updatedAt\");\n\n\treturn {\n\t\tprovider: readString(entry, \"provider\") ?? \"unknown\",\n\t\t...(source === undefined ? {} : { source }),\n\t\t...(sessionCostUSD === undefined ? {} : { sessionCostUSD }),\n\t\t...(last30DaysCostUSD === undefined ? {} : { last30DaysCostUSD }),\n\t\t...(totalCostUSD === undefined ? {} : { totalCostUSD }),\n\t\t...(totalTokens === undefined ? {} : { totalTokens }),\n\t\t...(updatedAt === undefined ? {} : { updatedAt }),\n\t};\n}\n\nfunction redactValue(value: unknown, parentKey: string | undefined): unknown {\n\tif (isUnknownArray(value)) {\n\t\treturn value.map((item) => redactValue(item, parentKey));\n\t}\n\tif (!isRecord(value)) {\n\t\treturn value;\n\t}\n\n\tconst redacted: Record<string, unknown> = {};\n\tfor (const [key, fieldValue] of Object.entries(value)) {\n\t\tif (shouldDropField(key, parentKey)) {\n\t\t\tcontinue;\n\t\t}\n\t\tredacted[key] = redactValue(fieldValue, key);\n\t}\n\treturn redacted;\n}\n\nfunction shouldDropField(key: string, parentKey: string | undefined): boolean {\n\tif (REDACTED_FIELD_NAMES.some((fieldName) => fieldName === key)) {\n\t\treturn true;\n\t}\n\treturn parentKey === \"projects\" && (key === \"path\" || key === \"name\");\n}\n\nfunction findUnsafeJsonPath(value: unknown, jsonPath: string): string | undefined {\n\tif (typeof value === \"string\") {\n\t\treturn isSecretLikeString(value) ? jsonPath : undefined;\n\t}\n\tif (isUnknownArray(value)) {\n\t\tfor (const [index, item] of value.entries()) {\n\t\t\tconst found = findUnsafeJsonPath(item, `${jsonPath}[${index}]`);\n\t\t\tif (found !== undefined) {\n\t\t\t\treturn found;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\tif (!isRecord(value)) {\n\t\treturn undefined;\n\t}\n\n\tfor (const [key, fieldValue] of Object.entries(value)) {\n\t\tconst separator = jsonPath.endsWith(\".\") ? \"\" : \".\";\n\t\tconst found = findUnsafeJsonPath(fieldValue, `${jsonPath}${separator}${key}`);\n\t\tif (found !== undefined) {\n\t\t\treturn found;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction isSecretLikeString(value: string): boolean {\n\treturn SECRET_LIKE_VALUE_PATTERNS.some((pattern) => pattern.test(value));\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isUnknownArray(value: unknown): value is readonly unknown[] {\n\treturn Array.isArray(value);\n}\n\nfunction readRecord(record: Record<string, unknown>, key: string): Record<string, unknown> | undefined {\n\tconst value = record[key];\n\treturn isRecord(value) ? value : undefined;\n}\n\nfunction readString(record: Record<string, unknown>, key: string): string | undefined {\n\tconst value = record[key];\n\treturn typeof value === \"string\" ? value : undefined;\n}\n\nfunction readNumber(record: Record<string, unknown>, key: string): number | undefined {\n\tconst value = record[key];\n\treturn typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-safety.d.ts","sourceRoot":"","sources":["../../src/core/command-safety.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;AAExD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CACf;AA+nBD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAEpE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE9D","sourcesContent":["// allow: SIZE_OK - legacy safety rules table; this change only preserves existing typed behavior.\nexport type CommandRisk = \"block\" | \"confirm\" | \"allow\";\n\nexport interface CommandVerdict {\n\trisk: CommandRisk;\n\trule: string;\n\treason: string;\n}\n\ninterface CommandPrefixResult {\n\ttokens: string[];\n\tprivilege: string | undefined;\n}\n\nconst RISK_RANK: Readonly<Record<CommandRisk, number>> = {\n\tallow: 0,\n\tconfirm: 1,\n\tblock: 2,\n};\n\nconst PRIVILEGE_COMMANDS = new Set([\"sudo\", \"su\", \"doas\", \"pkexec\"]);\nconst SUDO_OPTIONS_WITH_VALUE = new Set([\n\t\"-C\",\n\t\"--close-from\",\n\t\"-g\",\n\t\"--group\",\n\t\"-h\",\n\t\"--host\",\n\t\"-p\",\n\t\"--prompt\",\n\t\"-T\",\n\t\"--command-timeout\",\n\t\"-u\",\n\t\"--user\",\n]);\nconst GIT_GLOBAL_OPTIONS_WITH_VALUE = new Set([\"-C\", \"-c\", \"--git-dir\", \"--work-tree\", \"--namespace\"]);\n\n/** Maximum recursion depth when unwrapping shell wrappers (bash -c, eval, xargs, find -exec). */\nconst MAX_WRAP_DEPTH = 6;\n\n/** Executables that run an inline command string passed via a `-c`-style flag. */\nconst SHELL_WRAPPERS = new Set([\"bash\", \"sh\", \"zsh\", \"dash\", \"ksh\", \"ash\"]);\n\n/**\n * Script interpreters that execute an inline program string (via a -c/-e/-r\n * style flag). Running inline code can execute arbitrary shell commands, so a\n * matched destructive/exec keyword inside the program escalates the call to\n * confirm-tier. A plain script-file path stays allow-tier.\n */\nconst INTERPRETER_INLINE_FLAGS: Readonly<Record<string, readonly string[]>> = {\n\tpython: [\"-c\"],\n\tpython2: [\"-c\"],\n\tpython3: [\"-c\"],\n\tnode: [\"-e\", \"--eval\", \"-p\", \"--print\"],\n\tperl: [\"-e\", \"-E\"],\n\truby: [\"-e\", \"-E\"],\n\tphp: [\"-r\", \"--run\"],\n\tlua: [\"-e\"],\n\texpect: [\"-c\"],\n};\n\n/**\n * Patterns that indicate an inline program invokes a shell command or\n * performs a destructive filesystem operation. Matching any of these inside\n * a `-c`/`-e`/`-r` program string escalates the call to confirm-tier.\n */\nconst INLINE_DESTRUCTIVE_PATTERNS: readonly RegExp[] = [\n\t/\\bos\\.system\\s*\\(/,\n\t/\\bos\\.remove\\s*\\(/,\n\t/\\bos\\.unlink\\s*\\(/,\n\t/\\bshutil\\.rmtree\\s*\\(/,\n\t/\\bsubprocess\\.(?:call|run|check_call|check_output|Popen)\\s*\\(/,\n\t/\\bexec(?:Sync|File)?\\s*\\(/,\n\t/\\bspawn(?:Sync)?\\s*\\(/,\n\t/\\bchild_process\\b/,\n\t/\\brequire\\s*\\(\\s*['\"]child_process['\"]\\s*\\)/,\n\t/\\b(?:system|popen)\\s*\\(/,\n\t/`[^`]*\\brm\\s+-/m,\n\t/\\$\\(\\s*[^)]*\\brm\\s+-/,\n];\n\n/** xargs options that consume a following value token before the wrapped command begins. */\nconst XARGS_OPTIONS_WITH_VALUE = new Set([\n\t\"-I\",\n\t\"-i\",\n\t\"-n\",\n\t\"-L\",\n\t\"-P\",\n\t\"-d\",\n\t\"-E\",\n\t\"-s\",\n\t\"-a\",\n\t\"--max-args\",\n\t\"--max-procs\",\n\t\"--delimiter\",\n\t\"--arg-file\",\n\t\"--max-lines\",\n\t\"--replace\",\n]);\n\n/**\n * Credential / secret file patterns. A bash argv token matching one of these is\n * treated as a `confirm`-tier access: headless callers (LLM tool calls, RPC bash)\n * deny by default, while an interactive user can still approve reading their own\n * files. These complement the §0.1 freedom safety floor, which hard-denies the\n * same paths on the entry points it guards.\n */\nconst SECRET_FILE_STRICT_PATTERNS: readonly RegExp[] = [\n\t/(^|\\/)\\.env(\\.|$)/i,\n\t/(^|\\/)\\.npmrc$/i,\n\t/(^|\\/)\\.netrc$/i,\n\t/(^|\\/)\\.pgpass$/i,\n\t/(^|\\/)\\.aws\\/credentials$/i,\n\t/(^|\\/)credentials$/i,\n\t/(^|\\/)auth\\.json$/i,\n\t/(^|\\/)id_(rsa|dsa|ecdsa|ed25519)$/i,\n\t/\\.(pem|key|p12|pfx|keystore|jks)$/i,\n\t/(^|\\/)secrets?(\\.[^/]+)?$/i,\n];\n\n/** Benign sibling files that look secret-ish but never hold credentials. */\nconst SECRET_FILE_ALLOW_PATTERNS: readonly RegExp[] = [/(^|\\/)\\.env\\.(example|sample|template|dist|defaults?)$/i];\nconst HOME_VARIABLE_RM_TARGETS = new Set([\"$home\", \"$\" + \"{home}\", \"$home/\", \"$\" + \"{home}/\"]);\n\nfunction verdict(risk: CommandRisk, rule: string, reason: string): CommandVerdict {\n\treturn { risk, rule, reason };\n}\n\nfunction allowVerdict(): CommandVerdict {\n\treturn verdict(\"allow\", \"command.allow\", \"No destructive or protected command pattern matched.\");\n}\n\nfunction isEnvAssignment(token: string | undefined): boolean {\n\treturn token !== undefined && /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token);\n}\n\nfunction tokenizeShellSegment(segment: string): string[] {\n\tconst tokens: string[] = [];\n\tlet current = \"\";\n\tlet quote: string | undefined;\n\tlet escaped = false;\n\n\tfor (const character of segment.trim()) {\n\t\tif (escaped) {\n\t\t\tcurrent += character;\n\t\t\tescaped = false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (character === \"\\\\\" && quote !== \"'\") {\n\t\t\tescaped = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (quote) {\n\t\t\tif (character === quote) {\n\t\t\t\tquote = undefined;\n\t\t\t} else {\n\t\t\t\tcurrent += character;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (character === \"'\" || character === '\"') {\n\t\t\tquote = character;\n\t\t\tcontinue;\n\t\t}\n\t\tif (/\\s/.test(character)) {\n\t\t\tif (current) {\n\t\t\t\ttokens.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tcurrent += character;\n\t}\n\n\tif (escaped) current += \"\\\\\";\n\tif (current) tokens.push(current);\n\treturn tokens;\n}\n\nfunction splitShellCommands(command: string): string[] {\n\tconst segments: string[] = [];\n\tlet current = \"\";\n\tlet quote: string | undefined;\n\tlet escaped = false;\n\n\tfor (let index = 0; index < command.length; index += 1) {\n\t\tconst character = command[index];\n\t\tconst next = command[index + 1];\n\t\tif (escaped) {\n\t\t\tcurrent += character;\n\t\t\tescaped = false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (character === \"\\\\\" && quote !== \"'\") {\n\t\t\tescaped = true;\n\t\t\tcurrent += character;\n\t\t\tcontinue;\n\t\t}\n\t\tif (quote) {\n\t\t\tcurrent += character;\n\t\t\tif (character === quote) quote = undefined;\n\t\t\tcontinue;\n\t\t}\n\t\tif (character === \"'\" || character === '\"') {\n\t\t\tquote = character;\n\t\t\tcurrent += character;\n\t\t\tcontinue;\n\t\t}\n\t\tif (character === \";\" || character === \"|\" || character === \"\\n\" || character === \"\\r\") {\n\t\t\tif (current.trim()) segments.push(current.trim());\n\t\t\tcurrent = \"\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (character === \"&\") {\n\t\t\tif (current.trim()) segments.push(current.trim());\n\t\t\tcurrent = \"\";\n\t\t\tif (next === \"&\") index += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tcurrent += character;\n\t}\n\n\tif (current.trim()) segments.push(current.trim());\n\treturn segments;\n}\n\nfunction skipEnvironmentPrefix(tokens: readonly string[], startIndex: number): number {\n\tlet index = startIndex;\n\twhile (isEnvAssignment(tokens[index])) index += 1;\n\tif (tokens[index]?.toLowerCase() !== \"env\") return index;\n\n\tindex += 1;\n\twhile (index < tokens.length) {\n\t\tconst token = tokens[index];\n\t\tif (isEnvAssignment(token)) {\n\t\t\tindex += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tif (token?.startsWith(\"-\")) {\n\t\t\tindex += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tbreak;\n\t}\n\treturn index;\n}\n\nfunction optionConsumesValue(option: string, optionsWithValue: ReadonlySet<string>): boolean {\n\tif (option.includes(\"=\")) return false;\n\treturn optionsWithValue.has(option);\n}\n\nfunction skipPrivilegeOptions(tokens: readonly string[], startIndex: number, privilege: string): number {\n\tif (privilege !== \"sudo\" && privilege !== \"doas\") return startIndex;\n\n\tlet index = startIndex;\n\twhile (index < tokens.length) {\n\t\tconst token = tokens[index];\n\t\tif (!token?.startsWith(\"-\")) break;\n\t\tindex += 1;\n\t\tif (token === \"--\") break;\n\t\tif (optionConsumesValue(token, SUDO_OPTIONS_WITH_VALUE)) index += 1;\n\t}\n\treturn index;\n}\n\nfunction stripCommandPrefixes(tokens: readonly string[]): CommandPrefixResult {\n\tlet index = skipEnvironmentPrefix(tokens, 0);\n\tconst privilege = tokens[index]?.toLowerCase();\n\tif (!PRIVILEGE_COMMANDS.has(privilege ?? \"\")) {\n\t\treturn { tokens: tokens.slice(index), privilege: undefined };\n\t}\n\n\tindex += 1;\n\tindex = skipPrivilegeOptions(tokens, index, privilege ?? \"\");\n\tindex = skipEnvironmentPrefix(tokens, index);\n\treturn { tokens: tokens.slice(index), privilege };\n}\n\nfunction hasForkBomb(command: string): boolean {\n\treturn command.replace(/\\s/g, \"\").includes(\":(){:|:&};:\");\n}\n\nfunction normalizePathTarget(target: string): string {\n\treturn target.replace(/\\/+$/g, (match) => (target === match ? \"/\" : \"\")).replace(/\\/+/g, \"/\");\n}\n\nfunction classifyRmTarget(target: string): CommandVerdict | null {\n\tconst normalized = normalizePathTarget(target);\n\tif (normalized === \"/\" || normalized === \"/*\" || normalized === \"/.*\") {\n\t\treturn verdict(\"block\", \"fs.rm_rf_root\", \"Recursive forced rm targets filesystem root.\");\n\t}\n\tif (normalized === \"~\" || normalized === \"~/\" || normalized === \"~/*\" || normalized === \"~/.*\") {\n\t\treturn verdict(\"block\", \"fs.rm_rf_home\", \"Recursive forced rm targets the home directory.\");\n\t}\n\t// $HOME / ${HOME} expands to the user home directory at runtime.\n\tconst lower = normalized.toLowerCase();\n\tif (HOME_VARIABLE_RM_TARGETS.has(lower)) {\n\t\treturn verdict(\n\t\t\t\"confirm\",\n\t\t\t\"fs.rm_rf_home_var\",\n\t\t\t\"Recursive forced rm targets $HOME, which expands to the user home directory.\",\n\t\t);\n\t}\n\t// ~username targets another user home directory.\n\tif (/^~[A-Za-z0-9._-]/.test(normalized)) {\n\t\treturn verdict(\"confirm\", \"fs.rm_rf_user_home\", \"Recursive forced rm targets another user home directory.\");\n\t}\n\t// \".\" or \"./\" — current working directory (project root deletion risk).\n\tif (normalized === \".\") {\n\t\treturn verdict(\"confirm\", \"fs.rm_rf_cwd\", \"Recursive forced rm targets the current working directory.\");\n\t}\n\t// \"..\" — parent directory.\n\tif (normalized === \"..\") {\n\t\treturn verdict(\"confirm\", \"fs.rm_rf_parent\", \"Recursive forced rm targets the parent directory.\");\n\t}\n\treturn null;\n}\n\nfunction classifyRm(tokens: readonly string[]): CommandVerdict | null {\n\tlet hasRecursive = false;\n\tlet hasForce = false;\n\tlet endOfOptions = false;\n\tconst targets: string[] = [];\n\n\tfor (const token of tokens.slice(1)) {\n\t\tif (!endOfOptions && token === \"--\") {\n\t\t\tendOfOptions = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!endOfOptions && token.startsWith(\"-\") && token !== \"-\") {\n\t\t\tif (token === \"--recursive\") hasRecursive = true;\n\t\t\tif (token === \"--force\") hasForce = true;\n\t\t\tif (!token.startsWith(\"--\")) {\n\t\t\t\tconst flagCharacters = token.slice(1).split(\"\");\n\t\t\t\tif (flagCharacters.some((flag) => flag === \"r\" || flag === \"R\")) hasRecursive = true;\n\t\t\t\tif (flagCharacters.includes(\"f\")) hasForce = true;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\ttargets.push(token);\n\t}\n\n\tif (!hasRecursive || !hasForce) return null;\n\tfor (const target of targets) {\n\t\tconst targetVerdict = classifyRmTarget(target);\n\t\tif (targetVerdict) return targetVerdict;\n\t}\n\treturn null;\n}\n\nfunction isBlockDeviceOutput(token: string): boolean {\n\treturn /^of=\\/dev\\/(?:sd[a-z][a-z0-9]*|nvme[0-9a-z]+|disk(?:[0-9].*|\\/.*|$))/i.test(token);\n}\n\nfunction classifyDestructiveFilesystemCommand(command: string): CommandVerdict | null {\n\tif (hasForkBomb(command)) {\n\t\treturn verdict(\"block\", \"process.fork_bomb\", \"Shell fork bomb pattern would exhaust process resources.\");\n\t}\n\n\tconst { tokens } = stripCommandPrefixes(tokenizeShellSegment(command));\n\tconst executable = tokens[0]?.toLowerCase();\n\tif (!executable) return null;\n\tif (executable === \"rm\") return classifyRm(tokens);\n\tif (executable.startsWith(\"mkfs\")) {\n\t\treturn verdict(\"block\", \"fs.mkfs\", \"Filesystem formatting commands are blocked.\");\n\t}\n\tif (executable === \"dd\" && tokens.slice(1).some(isBlockDeviceOutput)) {\n\t\treturn verdict(\"block\", \"fs.dd_block_device\", \"dd writes directly to a block-device output path.\");\n\t}\n\treturn null;\n}\n\nfunction findGitCommand(tokens: readonly string[]): { command: string; args: string[]; originalArgs: string[] } | null {\n\tif (tokens[0]?.toLowerCase() !== \"git\") return null;\n\tlet index = 1;\n\twhile (index < tokens.length && tokens[index]?.startsWith(\"-\")) {\n\t\tconst option = tokens[index];\n\t\tconst normalizedOption = option.toLowerCase();\n\t\tindex += 1;\n\t\tif (option === \"--\") break;\n\t\tif (\n\t\t\toptionConsumesValue(option, GIT_GLOBAL_OPTIONS_WITH_VALUE) ||\n\t\t\toptionConsumesValue(normalizedOption, GIT_GLOBAL_OPTIONS_WITH_VALUE)\n\t\t) {\n\t\t\tindex += 1;\n\t\t}\n\t}\n\tconst command = tokens[index]?.toLowerCase();\n\tif (!command) return null;\n\tconst originalArgs = tokens.slice(index + 1);\n\treturn {\n\t\tcommand,\n\t\targs: originalArgs.map((argument) => argument.toLowerCase()),\n\t\toriginalArgs,\n\t};\n}\n\nfunction hasGitCleanForceDirectory(args: readonly string[]): boolean {\n\tlet hasForce = false;\n\tlet hasDirectory = false;\n\tfor (const arg of args) {\n\t\tif (arg === \"--force\") hasForce = true;\n\t\tif (arg === \"-f\") hasForce = true;\n\t\tif (arg === \"-d\") hasDirectory = true;\n\t\tif (arg.startsWith(\"-\") && !arg.startsWith(\"--\")) {\n\t\t\tconst flags = arg.slice(1).split(\"\");\n\t\t\tif (flags.includes(\"f\")) hasForce = true;\n\t\t\tif (flags.includes(\"d\")) hasDirectory = true;\n\t\t}\n\t}\n\treturn hasForce && hasDirectory;\n}\n\nfunction classifyProtectedGitCommand(command: string): CommandVerdict | null {\n\tconst { tokens } = stripCommandPrefixes(tokenizeShellSegment(command));\n\tconst gitCommand = findGitCommand(tokens);\n\tif (!gitCommand) return null;\n\n\tif (gitCommand.command === \"reset\" && gitCommand.args.includes(\"--hard\")) {\n\t\treturn verdict(\"confirm\", \"git.reset_hard\", \"git reset --hard can discard worktree changes.\");\n\t}\n\tif (gitCommand.command === \"restore\") {\n\t\tconst targetAll = gitCommand.originalArgs.includes(\".\") || gitCommand.originalArgs.includes(\"*\");\n\t\tconst rewritesWorktree = gitCommand.args.includes(\"--worktree\") || gitCommand.args.includes(\"--source\");\n\t\tif (targetAll || rewritesWorktree) {\n\t\t\treturn verdict(\"confirm\", \"git.restore\", \"git restore can discard or overwrite local worktree changes.\");\n\t\t}\n\t}\n\tif (gitCommand.command === \"checkout\" && gitCommand.originalArgs.includes(\".\")) {\n\t\treturn verdict(\"confirm\", \"git.checkout_dot\", \"git checkout . can overwrite local worktree changes.\");\n\t}\n\tif (gitCommand.command === \"clean\" && hasGitCleanForceDirectory(gitCommand.args)) {\n\t\treturn verdict(\"confirm\", \"git.clean_force\", \"git clean -fd can delete untracked files and directories.\");\n\t}\n\tif (gitCommand.command === \"stash\" && gitCommand.originalArgs.length === 0) {\n\t\treturn verdict(\"confirm\", \"git.stash_bare\", \"Bare git stash can hide local worktree changes.\");\n\t}\n\tif (gitCommand.command === \"add\") {\n\t\tif (gitCommand.originalArgs.includes(\".\")) {\n\t\t\treturn verdict(\"confirm\", \"git.add_dot\", \"git add . can stage unrelated local changes.\");\n\t\t}\n\t\tif (gitCommand.originalArgs.includes(\"-A\") || gitCommand.args.includes(\"--all\")) {\n\t\t\treturn verdict(\"confirm\", \"git.add_all\", \"git add -A can stage unrelated local changes.\");\n\t\t}\n\t}\n\tif (gitCommand.command === \"commit\" && gitCommand.args.includes(\"--no-verify\")) {\n\t\treturn verdict(\"confirm\", \"git.no_verify\", \"git commit --no-verify bypasses repository verification hooks.\");\n\t}\n\tif (gitCommand.command === \"push\" && gitCommand.args.includes(\"--no-verify\")) {\n\t\treturn verdict(\"confirm\", \"git.push_no_verify\", \"git push --no-verify bypasses remote verification hooks.\");\n\t}\n\tif (\n\t\tgitCommand.command === \"push\" &&\n\t\t(gitCommand.originalArgs.includes(\"-f\") ||\n\t\t\tgitCommand.args.includes(\"--force\") ||\n\t\t\tgitCommand.args.includes(\"--force-with-lease\"))\n\t) {\n\t\treturn verdict(\"confirm\", \"git.force_push\", \"Force-pushing can rewrite remote history.\");\n\t}\n\treturn null;\n}\n\nfunction classifyPrivilegeCommand(command: string): CommandVerdict | null {\n\tconst tokens = tokenizeShellSegment(command);\n\tconst index = skipEnvironmentPrefix(tokens, 0);\n\tconst privilege = tokens[index]?.toLowerCase();\n\tif (!PRIVILEGE_COMMANDS.has(privilege ?? \"\")) return null;\n\treturn verdict(\"confirm\", `priv.${privilege}`, `${privilege} requires explicit per-command confirmation.`);\n}\n\n/**\n * Locate the inline command-string argument for a `shell -c \"<cmd>\"` invocation.\n * Returns the token index of the command string, or -1 when the shell runs a\n * script file (no inline command to classify).\n */\nfunction findShellCommandStringIndex(tokens: readonly string[]): number {\n\tfor (let index = 1; index < tokens.length; index += 1) {\n\t\tconst token = tokens[index];\n\t\tif (!token.startsWith(\"-\")) return -1;\n\t\tif (/^-[a-z]*c$/i.test(token)) {\n\t\t\treturn index + 1 < tokens.length ? index + 1 : -1;\n\t\t}\n\t}\n\treturn -1;\n}\n\n/**\n * Locate the inline program-string argument for an interpreter invocation\n * (`python -c`, `node -e`, `perl -e`, ...). Returns the token index of the\n * program string, or -1 when the interpreter runs a script file (no inline\n * program to classify).\n */\nfunction findInterpreterInlineIndex(tokens: readonly string[], flags: readonly string[]): number {\n\tfor (let index = 1; index < tokens.length; index += 1) {\n\t\tconst token = tokens[index];\n\t\tif (token === \"--\") return -1;\n\t\tif (!token.startsWith(\"-\")) return -1;\n\t\tif (flags.includes(token) || flags.includes(token.toLowerCase())) {\n\t\t\treturn index + 1 < tokens.length ? index + 1 : -1;\n\t\t}\n\t}\n\treturn -1;\n}\n\n/** Drop leading xargs options (and their values) to expose the wrapped command. */\nfunction stripXargsOptions(args: readonly string[]): string[] {\n\tlet index = 0;\n\twhile (index < args.length) {\n\t\tconst token = args[index];\n\t\tif (!token.startsWith(\"-\") || token === \"-\") break;\n\t\tindex += 1;\n\t\tif (token === \"--\") break;\n\t\tif (!token.includes(\"=\") && XARGS_OPTIONS_WITH_VALUE.has(token)) index += 1;\n\t}\n\treturn args.slice(index);\n}\n\n/** Extract the command tokens of a `find ... -exec <cmd> {} ;|+` clause, if present. */\nfunction extractFindExec(tokens: readonly string[]): string[] | null {\n\tfor (let index = 1; index < tokens.length; index += 1) {\n\t\tconst token = tokens[index].toLowerCase();\n\t\tif (token === \"-exec\" || token === \"-execdir\" || token === \"-ok\" || token === \"-okdir\") {\n\t\t\tconst collected: string[] = [];\n\t\t\tfor (let inner = index + 1; inner < tokens.length; inner += 1) {\n\t\t\t\tconst piece = tokens[inner];\n\t\t\t\tif (piece === \";\" || piece === \"+\") break;\n\t\t\t\tif (piece === \"{}\") continue;\n\t\t\t\tcollected.push(piece);\n\t\t\t}\n\t\t\treturn collected.length > 0 ? collected : null;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Classify the effective command hidden inside a wrapper executable so a\n * destructive or protected command cannot be smuggled past the classifier via\n * `bash -c`, `eval`, `xargs`, or `find -exec`. Returns null when the segment is\n * not a recognized wrapper.\n */\nfunction classifyWrappedCommand(command: string, depth: number): CommandVerdict | null {\n\tif (depth >= MAX_WRAP_DEPTH) return null;\n\tconst { tokens } = stripCommandPrefixes(tokenizeShellSegment(command));\n\tconst executable = tokens[0]?.toLowerCase();\n\tif (!executable) return null;\n\n\tif (SHELL_WRAPPERS.has(executable)) {\n\t\tconst innerIndex = findShellCommandStringIndex(tokens);\n\t\tconst inner = innerIndex === -1 ? undefined : tokens[innerIndex];\n\t\treturn inner ? classifyShellCommandInternal(inner, depth + 1) : null;\n\t}\n\tif (executable === \"eval\") {\n\t\tconst inner = tokens.slice(1).join(\" \").trim();\n\t\treturn inner ? classifyShellCommandInternal(inner, depth + 1) : null;\n\t}\n\tif (executable === \"xargs\") {\n\t\tconst inner = stripXargsOptions(tokens.slice(1)).join(\" \").trim();\n\t\treturn inner ? classifyShellCommandInternal(inner, depth + 1) : null;\n\t}\n\tif (executable === \"find\") {\n\t\tconst lowered = tokens.map((token) => token.toLowerCase());\n\t\tconst hasDelete = lowered.includes(\"-delete\");\n\t\tconst execTokens = extractFindExec(tokens);\n\t\tconst inner = execTokens?.join(\" \").trim();\n\t\tconst innerVerdict = inner ? classifyShellCommandInternal(inner, depth + 1) : null;\n\t\tif (hasDelete) {\n\t\t\tconst deleteVerdict = verdict(\n\t\t\t\t\"confirm\",\n\t\t\t\t\"fs.find_delete\",\n\t\t\t\t\"find -delete removes matched files and directories.\",\n\t\t\t);\n\t\t\tif (!innerVerdict || RISK_RANK[deleteVerdict.risk] > RISK_RANK[innerVerdict.risk]) {\n\t\t\t\treturn deleteVerdict;\n\t\t\t}\n\t\t}\n\t\treturn innerVerdict ?? null;\n\t}\n\tconst inlineFlags = INTERPRETER_INLINE_FLAGS[executable];\n\tif (inlineFlags) {\n\t\tconst innerIndex = findInterpreterInlineIndex(tokens, inlineFlags);\n\t\tif (innerIndex === -1) return null; // script file path — allow\n\t\tconst program = tokens[innerIndex];\n\t\tif (INLINE_DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(program))) {\n\t\t\treturn verdict(\n\t\t\t\t\"confirm\",\n\t\t\t\t`interp.${executable}_inline_destructive`,\n\t\t\t\t`${executable} inline program can execute destructive shell commands.`,\n\t\t\t);\n\t\t}\n\t\treturn null;\n\t}\n\treturn null;\n}\n\n/** Flag commands that read, copy, or transmit a credential / secret file path. */\nfunction classifySecretAccess(command: string): CommandVerdict | null {\n\tconst { tokens } = stripCommandPrefixes(tokenizeShellSegment(command));\n\tif (tokens.length === 0) return null;\n\tfor (let index = 1; index < tokens.length; index += 1) {\n\t\tconst token = tokens[index];\n\t\tif (!token || token.startsWith(\"-\")) continue;\n\t\tif (/\\s/.test(token)) continue;\n\t\tif (SECRET_FILE_ALLOW_PATTERNS.some((pattern) => pattern.test(token))) continue;\n\t\tif (SECRET_FILE_STRICT_PATTERNS.some((pattern) => pattern.test(token))) {\n\t\t\treturn verdict(\"confirm\", \"secret.read_path\", \"Command references a credential or secret file path.\");\n\t\t}\n\t}\n\treturn null;\n}\n\nfunction classifySingleCommand(command: string, depth: number): CommandVerdict {\n\tconst trimmed = command.trim();\n\tif (!trimmed) return allowVerdict();\n\n\tconst destructiveFilesystem = classifyDestructiveFilesystemCommand(trimmed);\n\tif (destructiveFilesystem) return destructiveFilesystem;\n\n\tconst wrapped = classifyWrappedCommand(trimmed, depth);\n\tif (wrapped && wrapped.risk === \"block\") return wrapped;\n\n\tconst protectedGit = classifyProtectedGitCommand(trimmed);\n\tif (protectedGit) return protectedGit;\n\n\tconst privilege = classifyPrivilegeCommand(trimmed);\n\tif (privilege) return privilege;\n\n\tconst secret = classifySecretAccess(trimmed);\n\tif (secret) return secret;\n\n\tif (wrapped && RISK_RANK[wrapped.risk] > RISK_RANK.allow) return wrapped;\n\n\treturn allowVerdict();\n}\n\nfunction classifyShellCommandInternal(command: string, depth: number): CommandVerdict {\n\tlet selected = classifySingleCommand(command, depth);\n\tfor (const segment of splitShellCommands(command)) {\n\t\tconst candidate = classifySingleCommand(segment, depth);\n\t\tif (RISK_RANK[candidate.risk] > RISK_RANK[selected.risk]) selected = candidate;\n\t\tif (selected.risk === \"block\") break;\n\t}\n\treturn selected;\n}\n\nexport function classifyShellCommand(command: string): CommandVerdict {\n\treturn classifyShellCommandInternal(command, 0);\n}\n\nexport function isDestructiveFilesystem(command: string): boolean {\n\treturn classifyDestructiveFilesystemCommand(command) !== null;\n}\n\nexport function isProtectedGitOperation(command: string): boolean {\n\treturn classifyProtectedGitCommand(command) !== null;\n}\n\nexport function isPrivilegeEscalation(command: string): boolean {\n\treturn classifyPrivilegeCommand(command) !== null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"command-safety.d.ts","sourceRoot":"","sources":["../../src/core/command-safety.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;AAExD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CACf;AAsoBD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAEpE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE9D","sourcesContent":["// allow: SIZE_OK - legacy safety rules table; this change only preserves existing typed behavior.\nexport type CommandRisk = \"block\" | \"confirm\" | \"allow\";\n\nexport interface CommandVerdict {\n\trisk: CommandRisk;\n\trule: string;\n\treason: string;\n}\n\ninterface CommandPrefixResult {\n\ttokens: string[];\n\tprivilege: string | undefined;\n}\n\nconst RISK_RANK: Readonly<Record<CommandRisk, number>> = {\n\tallow: 0,\n\tconfirm: 1,\n\tblock: 2,\n};\n\nconst PRIVILEGE_COMMANDS = new Set([\"sudo\", \"su\", \"doas\", \"pkexec\"]);\nconst SUDO_OPTIONS_WITH_VALUE = new Set([\n\t\"-C\",\n\t\"--close-from\",\n\t\"-g\",\n\t\"--group\",\n\t\"-h\",\n\t\"--host\",\n\t\"-p\",\n\t\"--prompt\",\n\t\"-T\",\n\t\"--command-timeout\",\n\t\"-u\",\n\t\"--user\",\n]);\nconst GIT_GLOBAL_OPTIONS_WITH_VALUE = new Set([\"-C\", \"-c\", \"--git-dir\", \"--work-tree\", \"--namespace\"]);\n\n/** Maximum recursion depth when unwrapping shell wrappers (bash -c, eval, xargs, find -exec). */\nconst MAX_WRAP_DEPTH = 6;\n\n/** Executables that run an inline command string passed via a `-c`-style flag. */\nconst SHELL_WRAPPERS = new Set([\"bash\", \"sh\", \"zsh\", \"dash\", \"ksh\", \"ash\"]);\n\n/**\n * Script interpreters that execute an inline program string (via a -c/-e/-r\n * style flag). Running inline code can execute arbitrary shell commands, so a\n * matched destructive/exec keyword inside the program escalates the call to\n * confirm-tier. A plain script-file path stays allow-tier.\n */\nconst INTERPRETER_INLINE_FLAGS: Readonly<Record<string, readonly string[]>> = {\n\tpython: [\"-c\"],\n\tpython2: [\"-c\"],\n\tpython3: [\"-c\"],\n\tnode: [\"-e\", \"--eval\", \"-p\", \"--print\"],\n\tperl: [\"-e\", \"-E\"],\n\truby: [\"-e\", \"-E\"],\n\tphp: [\"-r\", \"--run\"],\n\tlua: [\"-e\"],\n\texpect: [\"-c\"],\n};\n\n/**\n * Patterns that indicate an inline program invokes a shell command or\n * performs a destructive filesystem operation. Matching any of these inside\n * a `-c`/`-e`/`-r` program string escalates the call to confirm-tier.\n */\nconst INLINE_DESTRUCTIVE_PATTERNS: readonly RegExp[] = [\n\t/\\bos\\.system\\s*\\(/,\n\t/\\bos\\.remove\\s*\\(/,\n\t/\\bos\\.unlink\\s*\\(/,\n\t/\\bshutil\\.rmtree\\s*\\(/,\n\t/\\bsubprocess\\.(?:call|run|check_call|check_output|Popen)\\s*\\(/,\n\t/\\bexec(?:Sync|File)?\\s*\\(/,\n\t/\\bspawn(?:Sync)?\\s*\\(/,\n\t/\\bchild_process\\b/,\n\t/\\brequire\\s*\\(\\s*['\"]child_process['\"]\\s*\\)/,\n\t/\\b(?:system|popen)\\s*\\(/,\n\t/`[^`]*\\brm\\s+-/m,\n\t/\\$\\(\\s*[^)]*\\brm\\s+-/,\n];\n\n/** xargs options that consume a following value token before the wrapped command begins. */\nconst XARGS_OPTIONS_WITH_VALUE = new Set([\n\t\"-I\",\n\t\"-i\",\n\t\"-n\",\n\t\"-L\",\n\t\"-P\",\n\t\"-d\",\n\t\"-E\",\n\t\"-s\",\n\t\"-a\",\n\t\"--max-args\",\n\t\"--max-procs\",\n\t\"--delimiter\",\n\t\"--arg-file\",\n\t\"--max-lines\",\n\t\"--replace\",\n]);\n\n/**\n * Credential / secret file patterns. A bash argv token matching one of these is\n * treated as a `confirm`-tier access: headless callers (LLM tool calls, RPC bash)\n * deny by default, while an interactive user can still approve reading their own\n * files. These complement the §0.1 freedom safety floor, which hard-denies the\n * same paths on the entry points it guards.\n */\nconst SECRET_FILE_STRICT_PATTERNS: readonly RegExp[] = [\n\t/(^|\\/)\\.env(\\.|$)/i,\n\t/(^|\\/)\\.npmrc$/i,\n\t/(^|\\/)\\.netrc$/i,\n\t/(^|\\/)\\.pgpass$/i,\n\t/(^|\\/)\\.aws\\/credentials$/i,\n\t/(^|\\/)credentials$/i,\n\t/(^|\\/)auth\\.json$/i,\n\t/(^|\\/)id_(rsa|dsa|ecdsa|ed25519)$/i,\n\t/\\.(pem|key|p12|pfx|keystore|jks)$/i,\n\t/(^|\\/)secrets?(\\.[^/]+)?$/i,\n];\n\n/** Benign sibling files that look secret-ish but never hold credentials. */\nconst SECRET_FILE_ALLOW_PATTERNS: readonly RegExp[] = [/(^|\\/)\\.env\\.(example|sample|template|dist|defaults?)$/i];\nconst SEARCH_COMMANDS = new Set([\"grep\", \"egrep\", \"fgrep\", \"rg\"]);\nconst HOME_VARIABLE_RM_TARGETS = new Set([\"$home\", \"$\" + \"{home}\", \"$home/\", \"$\" + \"{home}/\"]);\n\nfunction verdict(risk: CommandRisk, rule: string, reason: string): CommandVerdict {\n\treturn { risk, rule, reason };\n}\n\nfunction allowVerdict(): CommandVerdict {\n\treturn verdict(\"allow\", \"command.allow\", \"No destructive or protected command pattern matched.\");\n}\n\nfunction isEnvAssignment(token: string | undefined): boolean {\n\treturn token !== undefined && /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token);\n}\n\nfunction tokenizeShellSegment(segment: string): string[] {\n\tconst tokens: string[] = [];\n\tlet current = \"\";\n\tlet quote: string | undefined;\n\tlet escaped = false;\n\n\tfor (const character of segment.trim()) {\n\t\tif (escaped) {\n\t\t\tcurrent += character;\n\t\t\tescaped = false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (character === \"\\\\\" && quote !== \"'\") {\n\t\t\tescaped = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (quote) {\n\t\t\tif (character === quote) {\n\t\t\t\tquote = undefined;\n\t\t\t} else {\n\t\t\t\tcurrent += character;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (character === \"'\" || character === '\"') {\n\t\t\tquote = character;\n\t\t\tcontinue;\n\t\t}\n\t\tif (/\\s/.test(character)) {\n\t\t\tif (current) {\n\t\t\t\ttokens.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tcurrent += character;\n\t}\n\n\tif (escaped) current += \"\\\\\";\n\tif (current) tokens.push(current);\n\treturn tokens;\n}\n\nfunction splitShellCommands(command: string): string[] {\n\tconst segments: string[] = [];\n\tlet current = \"\";\n\tlet quote: string | undefined;\n\tlet escaped = false;\n\n\tfor (let index = 0; index < command.length; index += 1) {\n\t\tconst character = command[index];\n\t\tconst next = command[index + 1];\n\t\tif (escaped) {\n\t\t\tcurrent += character;\n\t\t\tescaped = false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (character === \"\\\\\" && quote !== \"'\") {\n\t\t\tescaped = true;\n\t\t\tcurrent += character;\n\t\t\tcontinue;\n\t\t}\n\t\tif (quote) {\n\t\t\tcurrent += character;\n\t\t\tif (character === quote) quote = undefined;\n\t\t\tcontinue;\n\t\t}\n\t\tif (character === \"'\" || character === '\"') {\n\t\t\tquote = character;\n\t\t\tcurrent += character;\n\t\t\tcontinue;\n\t\t}\n\t\tif (character === \";\" || character === \"|\" || character === \"\\n\" || character === \"\\r\") {\n\t\t\tif (current.trim()) segments.push(current.trim());\n\t\t\tcurrent = \"\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (character === \"&\") {\n\t\t\tif (current.trim()) segments.push(current.trim());\n\t\t\tcurrent = \"\";\n\t\t\tif (next === \"&\") index += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tcurrent += character;\n\t}\n\n\tif (current.trim()) segments.push(current.trim());\n\treturn segments;\n}\n\nfunction skipEnvironmentPrefix(tokens: readonly string[], startIndex: number): number {\n\tlet index = startIndex;\n\twhile (isEnvAssignment(tokens[index])) index += 1;\n\tif (tokens[index]?.toLowerCase() !== \"env\") return index;\n\n\tindex += 1;\n\twhile (index < tokens.length) {\n\t\tconst token = tokens[index];\n\t\tif (isEnvAssignment(token)) {\n\t\t\tindex += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tif (token?.startsWith(\"-\")) {\n\t\t\tindex += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tbreak;\n\t}\n\treturn index;\n}\n\nfunction optionConsumesValue(option: string, optionsWithValue: ReadonlySet<string>): boolean {\n\tif (option.includes(\"=\")) return false;\n\treturn optionsWithValue.has(option);\n}\n\nfunction skipPrivilegeOptions(tokens: readonly string[], startIndex: number, privilege: string): number {\n\tif (privilege !== \"sudo\" && privilege !== \"doas\") return startIndex;\n\n\tlet index = startIndex;\n\twhile (index < tokens.length) {\n\t\tconst token = tokens[index];\n\t\tif (!token?.startsWith(\"-\")) break;\n\t\tindex += 1;\n\t\tif (token === \"--\") break;\n\t\tif (optionConsumesValue(token, SUDO_OPTIONS_WITH_VALUE)) index += 1;\n\t}\n\treturn index;\n}\n\nfunction stripCommandPrefixes(tokens: readonly string[]): CommandPrefixResult {\n\tlet index = skipEnvironmentPrefix(tokens, 0);\n\tconst privilege = tokens[index]?.toLowerCase();\n\tif (!PRIVILEGE_COMMANDS.has(privilege ?? \"\")) {\n\t\treturn { tokens: tokens.slice(index), privilege: undefined };\n\t}\n\n\tindex += 1;\n\tindex = skipPrivilegeOptions(tokens, index, privilege ?? \"\");\n\tindex = skipEnvironmentPrefix(tokens, index);\n\treturn { tokens: tokens.slice(index), privilege };\n}\n\nfunction hasForkBomb(command: string): boolean {\n\treturn command.replace(/\\s/g, \"\").includes(\":(){:|:&};:\");\n}\n\nfunction normalizePathTarget(target: string): string {\n\treturn target.replace(/\\/+$/g, (match) => (target === match ? \"/\" : \"\")).replace(/\\/+/g, \"/\");\n}\n\nfunction classifyRmTarget(target: string): CommandVerdict | null {\n\tconst normalized = normalizePathTarget(target);\n\tif (normalized === \"/\" || normalized === \"/*\" || normalized === \"/.*\") {\n\t\treturn verdict(\"block\", \"fs.rm_rf_root\", \"Recursive forced rm targets filesystem root.\");\n\t}\n\tif (normalized === \"~\" || normalized === \"~/\" || normalized === \"~/*\" || normalized === \"~/.*\") {\n\t\treturn verdict(\"block\", \"fs.rm_rf_home\", \"Recursive forced rm targets the home directory.\");\n\t}\n\t// $HOME / ${HOME} expands to the user home directory at runtime.\n\tconst lower = normalized.toLowerCase();\n\tif (HOME_VARIABLE_RM_TARGETS.has(lower)) {\n\t\treturn verdict(\n\t\t\t\"confirm\",\n\t\t\t\"fs.rm_rf_home_var\",\n\t\t\t\"Recursive forced rm targets $HOME, which expands to the user home directory.\",\n\t\t);\n\t}\n\t// ~username targets another user home directory.\n\tif (/^~[A-Za-z0-9._-]/.test(normalized)) {\n\t\treturn verdict(\"confirm\", \"fs.rm_rf_user_home\", \"Recursive forced rm targets another user home directory.\");\n\t}\n\t// \".\" or \"./\" — current working directory (project root deletion risk).\n\tif (normalized === \".\") {\n\t\treturn verdict(\"confirm\", \"fs.rm_rf_cwd\", \"Recursive forced rm targets the current working directory.\");\n\t}\n\t// \"..\" — parent directory.\n\tif (normalized === \"..\") {\n\t\treturn verdict(\"confirm\", \"fs.rm_rf_parent\", \"Recursive forced rm targets the parent directory.\");\n\t}\n\treturn null;\n}\n\nfunction classifyRm(tokens: readonly string[]): CommandVerdict | null {\n\tlet hasRecursive = false;\n\tlet hasForce = false;\n\tlet endOfOptions = false;\n\tconst targets: string[] = [];\n\n\tfor (const token of tokens.slice(1)) {\n\t\tif (!endOfOptions && token === \"--\") {\n\t\t\tendOfOptions = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!endOfOptions && token.startsWith(\"-\") && token !== \"-\") {\n\t\t\tif (token === \"--recursive\") hasRecursive = true;\n\t\t\tif (token === \"--force\") hasForce = true;\n\t\t\tif (!token.startsWith(\"--\")) {\n\t\t\t\tconst flagCharacters = token.slice(1).split(\"\");\n\t\t\t\tif (flagCharacters.some((flag) => flag === \"r\" || flag === \"R\")) hasRecursive = true;\n\t\t\t\tif (flagCharacters.includes(\"f\")) hasForce = true;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\ttargets.push(token);\n\t}\n\n\tif (!hasRecursive || !hasForce) return null;\n\tfor (const target of targets) {\n\t\tconst targetVerdict = classifyRmTarget(target);\n\t\tif (targetVerdict) return targetVerdict;\n\t}\n\treturn null;\n}\n\nfunction isBlockDeviceOutput(token: string): boolean {\n\treturn /^of=\\/dev\\/(?:sd[a-z][a-z0-9]*|nvme[0-9a-z]+|disk(?:[0-9].*|\\/.*|$))/i.test(token);\n}\n\nfunction classifyDestructiveFilesystemCommand(command: string): CommandVerdict | null {\n\tif (hasForkBomb(command)) {\n\t\treturn verdict(\"block\", \"process.fork_bomb\", \"Shell fork bomb pattern would exhaust process resources.\");\n\t}\n\n\tconst { tokens } = stripCommandPrefixes(tokenizeShellSegment(command));\n\tconst executable = tokens[0]?.toLowerCase();\n\tif (!executable) return null;\n\tif (executable === \"rm\") return classifyRm(tokens);\n\tif (executable.startsWith(\"mkfs\")) {\n\t\treturn verdict(\"block\", \"fs.mkfs\", \"Filesystem formatting commands are blocked.\");\n\t}\n\tif (executable === \"dd\" && tokens.slice(1).some(isBlockDeviceOutput)) {\n\t\treturn verdict(\"block\", \"fs.dd_block_device\", \"dd writes directly to a block-device output path.\");\n\t}\n\treturn null;\n}\n\nfunction findGitCommand(tokens: readonly string[]): { command: string; args: string[]; originalArgs: string[] } | null {\n\tif (tokens[0]?.toLowerCase() !== \"git\") return null;\n\tlet index = 1;\n\twhile (index < tokens.length && tokens[index]?.startsWith(\"-\")) {\n\t\tconst option = tokens[index];\n\t\tconst normalizedOption = option.toLowerCase();\n\t\tindex += 1;\n\t\tif (option === \"--\") break;\n\t\tif (\n\t\t\toptionConsumesValue(option, GIT_GLOBAL_OPTIONS_WITH_VALUE) ||\n\t\t\toptionConsumesValue(normalizedOption, GIT_GLOBAL_OPTIONS_WITH_VALUE)\n\t\t) {\n\t\t\tindex += 1;\n\t\t}\n\t}\n\tconst command = tokens[index]?.toLowerCase();\n\tif (!command) return null;\n\tconst originalArgs = tokens.slice(index + 1);\n\treturn {\n\t\tcommand,\n\t\targs: originalArgs.map((argument) => argument.toLowerCase()),\n\t\toriginalArgs,\n\t};\n}\n\nfunction hasGitCleanForceDirectory(args: readonly string[]): boolean {\n\tlet hasForce = false;\n\tlet hasDirectory = false;\n\tfor (const arg of args) {\n\t\tif (arg === \"--force\") hasForce = true;\n\t\tif (arg === \"-f\") hasForce = true;\n\t\tif (arg === \"-d\") hasDirectory = true;\n\t\tif (arg.startsWith(\"-\") && !arg.startsWith(\"--\")) {\n\t\t\tconst flags = arg.slice(1).split(\"\");\n\t\t\tif (flags.includes(\"f\")) hasForce = true;\n\t\t\tif (flags.includes(\"d\")) hasDirectory = true;\n\t\t}\n\t}\n\treturn hasForce && hasDirectory;\n}\n\nfunction classifyProtectedGitCommand(command: string): CommandVerdict | null {\n\tconst { tokens } = stripCommandPrefixes(tokenizeShellSegment(command));\n\tconst gitCommand = findGitCommand(tokens);\n\tif (!gitCommand) return null;\n\n\tif (gitCommand.command === \"reset\" && gitCommand.args.includes(\"--hard\")) {\n\t\treturn verdict(\"confirm\", \"git.reset_hard\", \"git reset --hard can discard worktree changes.\");\n\t}\n\tif (gitCommand.command === \"restore\") {\n\t\tconst targetAll = gitCommand.originalArgs.includes(\".\") || gitCommand.originalArgs.includes(\"*\");\n\t\tconst rewritesWorktree = gitCommand.args.includes(\"--worktree\") || gitCommand.args.includes(\"--source\");\n\t\tif (targetAll || rewritesWorktree) {\n\t\t\treturn verdict(\"confirm\", \"git.restore\", \"git restore can discard or overwrite local worktree changes.\");\n\t\t}\n\t}\n\tif (gitCommand.command === \"checkout\" && gitCommand.originalArgs.includes(\".\")) {\n\t\treturn verdict(\"confirm\", \"git.checkout_dot\", \"git checkout . can overwrite local worktree changes.\");\n\t}\n\tif (gitCommand.command === \"clean\" && hasGitCleanForceDirectory(gitCommand.args)) {\n\t\treturn verdict(\"confirm\", \"git.clean_force\", \"git clean -fd can delete untracked files and directories.\");\n\t}\n\tif (gitCommand.command === \"stash\" && gitCommand.originalArgs.length === 0) {\n\t\treturn verdict(\"confirm\", \"git.stash_bare\", \"Bare git stash can hide local worktree changes.\");\n\t}\n\tif (gitCommand.command === \"add\") {\n\t\tif (gitCommand.originalArgs.includes(\".\")) {\n\t\t\treturn verdict(\"confirm\", \"git.add_dot\", \"git add . can stage unrelated local changes.\");\n\t\t}\n\t\tif (gitCommand.originalArgs.includes(\"-A\") || gitCommand.args.includes(\"--all\")) {\n\t\t\treturn verdict(\"confirm\", \"git.add_all\", \"git add -A can stage unrelated local changes.\");\n\t\t}\n\t}\n\tif (gitCommand.command === \"commit\" && gitCommand.args.includes(\"--no-verify\")) {\n\t\treturn verdict(\"confirm\", \"git.no_verify\", \"git commit --no-verify bypasses repository verification hooks.\");\n\t}\n\tif (gitCommand.command === \"push\" && gitCommand.args.includes(\"--no-verify\")) {\n\t\treturn verdict(\"confirm\", \"git.push_no_verify\", \"git push --no-verify bypasses remote verification hooks.\");\n\t}\n\tif (\n\t\tgitCommand.command === \"push\" &&\n\t\t(gitCommand.originalArgs.includes(\"-f\") ||\n\t\t\tgitCommand.args.includes(\"--force\") ||\n\t\t\tgitCommand.args.includes(\"--force-with-lease\"))\n\t) {\n\t\treturn verdict(\"confirm\", \"git.force_push\", \"Force-pushing can rewrite remote history.\");\n\t}\n\treturn null;\n}\n\nfunction classifyPrivilegeCommand(command: string): CommandVerdict | null {\n\tconst tokens = tokenizeShellSegment(command);\n\tconst index = skipEnvironmentPrefix(tokens, 0);\n\tconst privilege = tokens[index]?.toLowerCase();\n\tif (!PRIVILEGE_COMMANDS.has(privilege ?? \"\")) return null;\n\treturn verdict(\"confirm\", `priv.${privilege}`, `${privilege} requires explicit per-command confirmation.`);\n}\n\n/**\n * Locate the inline command-string argument for a `shell -c \"<cmd>\"` invocation.\n * Returns the token index of the command string, or -1 when the shell runs a\n * script file (no inline command to classify).\n */\nfunction findShellCommandStringIndex(tokens: readonly string[]): number {\n\tfor (let index = 1; index < tokens.length; index += 1) {\n\t\tconst token = tokens[index];\n\t\tif (!token.startsWith(\"-\")) return -1;\n\t\tif (/^-[a-z]*c$/i.test(token)) {\n\t\t\treturn index + 1 < tokens.length ? index + 1 : -1;\n\t\t}\n\t}\n\treturn -1;\n}\n\n/**\n * Locate the inline program-string argument for an interpreter invocation\n * (`python -c`, `node -e`, `perl -e`, ...). Returns the token index of the\n * program string, or -1 when the interpreter runs a script file (no inline\n * program to classify).\n */\nfunction findInterpreterInlineIndex(tokens: readonly string[], flags: readonly string[]): number {\n\tfor (let index = 1; index < tokens.length; index += 1) {\n\t\tconst token = tokens[index];\n\t\tif (token === \"--\") return -1;\n\t\tif (!token.startsWith(\"-\")) return -1;\n\t\tif (flags.includes(token) || flags.includes(token.toLowerCase())) {\n\t\t\treturn index + 1 < tokens.length ? index + 1 : -1;\n\t\t}\n\t}\n\treturn -1;\n}\n\n/** Drop leading xargs options (and their values) to expose the wrapped command. */\nfunction stripXargsOptions(args: readonly string[]): string[] {\n\tlet index = 0;\n\twhile (index < args.length) {\n\t\tconst token = args[index];\n\t\tif (!token.startsWith(\"-\") || token === \"-\") break;\n\t\tindex += 1;\n\t\tif (token === \"--\") break;\n\t\tif (!token.includes(\"=\") && XARGS_OPTIONS_WITH_VALUE.has(token)) index += 1;\n\t}\n\treturn args.slice(index);\n}\n\n/** Extract the command tokens of a `find ... -exec <cmd> {} ;|+` clause, if present. */\nfunction extractFindExec(tokens: readonly string[]): string[] | null {\n\tfor (let index = 1; index < tokens.length; index += 1) {\n\t\tconst token = tokens[index].toLowerCase();\n\t\tif (token === \"-exec\" || token === \"-execdir\" || token === \"-ok\" || token === \"-okdir\") {\n\t\t\tconst collected: string[] = [];\n\t\t\tfor (let inner = index + 1; inner < tokens.length; inner += 1) {\n\t\t\t\tconst piece = tokens[inner];\n\t\t\t\tif (piece === \";\" || piece === \"+\") break;\n\t\t\t\tif (piece === \"{}\") continue;\n\t\t\t\tcollected.push(piece);\n\t\t\t}\n\t\t\treturn collected.length > 0 ? collected : null;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Classify the effective command hidden inside a wrapper executable so a\n * destructive or protected command cannot be smuggled past the classifier via\n * `bash -c`, `eval`, `xargs`, or `find -exec`. Returns null when the segment is\n * not a recognized wrapper.\n */\nfunction classifyWrappedCommand(command: string, depth: number): CommandVerdict | null {\n\tif (depth >= MAX_WRAP_DEPTH) return null;\n\tconst { tokens } = stripCommandPrefixes(tokenizeShellSegment(command));\n\tconst executable = tokens[0]?.toLowerCase();\n\tif (!executable) return null;\n\n\tif (SHELL_WRAPPERS.has(executable)) {\n\t\tconst innerIndex = findShellCommandStringIndex(tokens);\n\t\tconst inner = innerIndex === -1 ? undefined : tokens[innerIndex];\n\t\treturn inner ? classifyShellCommandInternal(inner, depth + 1) : null;\n\t}\n\tif (executable === \"eval\") {\n\t\tconst inner = tokens.slice(1).join(\" \").trim();\n\t\treturn inner ? classifyShellCommandInternal(inner, depth + 1) : null;\n\t}\n\tif (executable === \"xargs\") {\n\t\tconst inner = stripXargsOptions(tokens.slice(1)).join(\" \").trim();\n\t\treturn inner ? classifyShellCommandInternal(inner, depth + 1) : null;\n\t}\n\tif (executable === \"find\") {\n\t\tconst lowered = tokens.map((token) => token.toLowerCase());\n\t\tconst hasDelete = lowered.includes(\"-delete\");\n\t\tconst execTokens = extractFindExec(tokens);\n\t\tconst inner = execTokens?.join(\" \").trim();\n\t\tconst innerVerdict = inner ? classifyShellCommandInternal(inner, depth + 1) : null;\n\t\tif (hasDelete) {\n\t\t\tconst deleteVerdict = verdict(\n\t\t\t\t\"confirm\",\n\t\t\t\t\"fs.find_delete\",\n\t\t\t\t\"find -delete removes matched files and directories.\",\n\t\t\t);\n\t\t\tif (!innerVerdict || RISK_RANK[deleteVerdict.risk] > RISK_RANK[innerVerdict.risk]) {\n\t\t\t\treturn deleteVerdict;\n\t\t\t}\n\t\t}\n\t\treturn innerVerdict ?? null;\n\t}\n\tconst inlineFlags = INTERPRETER_INLINE_FLAGS[executable];\n\tif (inlineFlags) {\n\t\tconst innerIndex = findInterpreterInlineIndex(tokens, inlineFlags);\n\t\tif (innerIndex === -1) return null; // script file path — allow\n\t\tconst program = tokens[innerIndex];\n\t\tif (INLINE_DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(program))) {\n\t\t\treturn verdict(\n\t\t\t\t\"confirm\",\n\t\t\t\t`interp.${executable}_inline_destructive`,\n\t\t\t\t`${executable} inline program can execute destructive shell commands.`,\n\t\t\t);\n\t\t}\n\t\treturn null;\n\t}\n\treturn null;\n}\n\nfunction isSearchPatternArgument(tokens: readonly string[], index: number): boolean {\n\tconst executable = tokens[0]?.toLowerCase();\n\treturn SEARCH_COMMANDS.has(executable ?? \"\") && tokens[index - 1] === \"--\";\n}\n\n/** Flag commands that read, copy, or transmit a credential / secret file path. */\nfunction classifySecretAccess(command: string): CommandVerdict | null {\n\tconst { tokens } = stripCommandPrefixes(tokenizeShellSegment(command));\n\tif (tokens.length === 0) return null;\n\tfor (let index = 1; index < tokens.length; index += 1) {\n\t\tconst token = tokens[index];\n\t\tif (!token || token.startsWith(\"-\")) continue;\n\t\tif (isSearchPatternArgument(tokens, index)) continue;\n\t\tif (/\\s/.test(token)) continue;\n\t\tif (SECRET_FILE_ALLOW_PATTERNS.some((pattern) => pattern.test(token))) continue;\n\t\tif (SECRET_FILE_STRICT_PATTERNS.some((pattern) => pattern.test(token))) {\n\t\t\treturn verdict(\"confirm\", \"secret.read_path\", \"Command references a credential or secret file path.\");\n\t\t}\n\t}\n\treturn null;\n}\n\nfunction classifySingleCommand(command: string, depth: number): CommandVerdict {\n\tconst trimmed = command.trim();\n\tif (!trimmed) return allowVerdict();\n\n\tconst destructiveFilesystem = classifyDestructiveFilesystemCommand(trimmed);\n\tif (destructiveFilesystem) return destructiveFilesystem;\n\n\tconst wrapped = classifyWrappedCommand(trimmed, depth);\n\tif (wrapped && wrapped.risk === \"block\") return wrapped;\n\n\tconst protectedGit = classifyProtectedGitCommand(trimmed);\n\tif (protectedGit) return protectedGit;\n\n\tconst privilege = classifyPrivilegeCommand(trimmed);\n\tif (privilege) return privilege;\n\n\tconst secret = classifySecretAccess(trimmed);\n\tif (secret) return secret;\n\n\tif (wrapped && RISK_RANK[wrapped.risk] > RISK_RANK.allow) return wrapped;\n\n\treturn allowVerdict();\n}\n\nfunction classifyShellCommandInternal(command: string, depth: number): CommandVerdict {\n\tlet selected = classifySingleCommand(command, depth);\n\tfor (const segment of splitShellCommands(command)) {\n\t\tconst candidate = classifySingleCommand(segment, depth);\n\t\tif (RISK_RANK[candidate.risk] > RISK_RANK[selected.risk]) selected = candidate;\n\t\tif (selected.risk === \"block\") break;\n\t}\n\treturn selected;\n}\n\nexport function classifyShellCommand(command: string): CommandVerdict {\n\treturn classifyShellCommandInternal(command, 0);\n}\n\nexport function isDestructiveFilesystem(command: string): boolean {\n\treturn classifyDestructiveFilesystemCommand(command) !== null;\n}\n\nexport function isProtectedGitOperation(command: string): boolean {\n\treturn classifyProtectedGitCommand(command) !== null;\n}\n\nexport function isPrivilegeEscalation(command: string): boolean {\n\treturn classifyPrivilegeCommand(command) !== null;\n}\n"]}
|
|
@@ -98,6 +98,7 @@ const SECRET_FILE_STRICT_PATTERNS = [
|
|
|
98
98
|
];
|
|
99
99
|
/** Benign sibling files that look secret-ish but never hold credentials. */
|
|
100
100
|
const SECRET_FILE_ALLOW_PATTERNS = [/(^|\/)\.env\.(example|sample|template|dist|defaults?)$/i];
|
|
101
|
+
const SEARCH_COMMANDS = new Set(["grep", "egrep", "fgrep", "rg"]);
|
|
101
102
|
const HOME_VARIABLE_RM_TARGETS = new Set(["$home", "$" + "{home}", "$home/", "$" + "{home}/"]);
|
|
102
103
|
function verdict(risk, rule, reason) {
|
|
103
104
|
return { risk, rule, reason };
|
|
@@ -563,6 +564,10 @@ function classifyWrappedCommand(command, depth) {
|
|
|
563
564
|
}
|
|
564
565
|
return null;
|
|
565
566
|
}
|
|
567
|
+
function isSearchPatternArgument(tokens, index) {
|
|
568
|
+
const executable = tokens[0]?.toLowerCase();
|
|
569
|
+
return SEARCH_COMMANDS.has(executable ?? "") && tokens[index - 1] === "--";
|
|
570
|
+
}
|
|
566
571
|
/** Flag commands that read, copy, or transmit a credential / secret file path. */
|
|
567
572
|
function classifySecretAccess(command) {
|
|
568
573
|
const { tokens } = stripCommandPrefixes(tokenizeShellSegment(command));
|
|
@@ -572,6 +577,8 @@ function classifySecretAccess(command) {
|
|
|
572
577
|
const token = tokens[index];
|
|
573
578
|
if (!token || token.startsWith("-"))
|
|
574
579
|
continue;
|
|
580
|
+
if (isSearchPatternArgument(tokens, index))
|
|
581
|
+
continue;
|
|
575
582
|
if (/\s/.test(token))
|
|
576
583
|
continue;
|
|
577
584
|
if (SECRET_FILE_ALLOW_PATTERNS.some((pattern) => pattern.test(token)))
|