cc-safety-net 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/bin/cc-safety-net.js +88 -20
- package/dist/bin/hook/common.d.ts +1 -3
- package/dist/bin/rule/format.d.ts +0 -7
- package/dist/core/analyze/parallel.d.ts +1 -0
- package/dist/core/analyze/segment.d.ts +1 -0
- package/dist/core/analyze/xargs.d.ts +1 -0
- package/dist/core/git/env.d.ts +3 -0
- package/dist/core/rules/policy/index.d.ts +5 -6
- package/dist/core/rules/policy/paths.d.ts +1 -0
- package/dist/core/rules/policy/scope-policy.d.ts +1 -0
- package/dist/core/rules/policy/types.d.ts +0 -1
- package/dist/core/rules/rulebook.d.ts +2 -0
- package/dist/index.js +62 -6
- package/dist/pi/builtin-commands/commands.d.ts +1 -0
- package/dist/pi/index.d.ts +2 -2
- package/dist/pi/index.js +111 -28
- package/dist/pi/{tool-use.d.ts → tool-call.d.ts} +6 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -237,6 +237,25 @@ Install CC Safety Net with OpenCode's native plugin command:
|
|
|
237
237
|
opencode plugin -g cc-safety-net
|
|
238
238
|
```
|
|
239
239
|
|
|
240
|
+
> [!NOTE]
|
|
241
|
+
> OpenCode can sometimes keep using a stale cached plugin version. See
|
|
242
|
+
> anomalyco/opencode#25293 for the current tracking issue.
|
|
243
|
+
>
|
|
244
|
+
> To force OpenCode to reinstall `cc-safety-net`, remove its cached package and
|
|
245
|
+
> install the version you want:
|
|
246
|
+
>
|
|
247
|
+
> ```sh
|
|
248
|
+
> rm -rf ~/.cache/opencode/packages/cc-safety-net@latest
|
|
249
|
+
> opencode plugin -g -f cc-safety-net@latest
|
|
250
|
+
>
|
|
251
|
+
> If you prefer pinning a specific version:
|
|
252
|
+
>
|
|
253
|
+
> rm -rf ~/.cache/opencode/packages/cc-safety-net@latest
|
|
254
|
+
> opencode plugin -g -f cc-safety-net@<version>
|
|
255
|
+
>
|
|
256
|
+
> Restart OpenCode after updating so the plugin is loaded from the refreshed
|
|
257
|
+
> cache.
|
|
258
|
+
|
|
240
259
|
---
|
|
241
260
|
|
|
242
261
|
### Pi Installation
|
|
@@ -338,6 +338,10 @@ function dangerousInText(text) {
|
|
|
338
338
|
return null;
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
// src/core/analyze/segment.ts
|
|
342
|
+
import { realpathSync as realpathSync6 } from "node:fs";
|
|
343
|
+
import { normalize as normalize3 } from "node:path";
|
|
344
|
+
|
|
341
345
|
// src/core/analyze/awk.ts
|
|
342
346
|
var AWK_INTERPRETERS = new Set(["awk", "gawk", "nawk", "mawk"]);
|
|
343
347
|
var REASON_AWK_SYSTEM_DYNAMIC = "Detected awk system() call with dynamic command that cannot be safely analyzed.";
|
|
@@ -4143,8 +4147,7 @@ function analyzeParallelCommand(context) {
|
|
|
4143
4147
|
}
|
|
4144
4148
|
var CWD_CHANGE_REGEX = /^\s*(?:\$\(\s*)?[({]*\s*(?:command\s+|builtin\s+)?(?:cd|pushd|popd)(?:\s|$)/;
|
|
4145
4149
|
function segmentChangesCwd(segment) {
|
|
4146
|
-
const
|
|
4147
|
-
const unwrapped = stripWrappers([...stripped]);
|
|
4150
|
+
const unwrapped = getCwdChangeTokens(segment);
|
|
4148
4151
|
if (unwrapped.length === 0) {
|
|
4149
4152
|
return false;
|
|
4150
4153
|
}
|
|
@@ -4163,6 +4166,32 @@ function segmentChangesCwd(segment) {
|
|
|
4163
4166
|
const joined = segment.join(" ");
|
|
4164
4167
|
return CWD_CHANGE_REGEX.test(joined);
|
|
4165
4168
|
}
|
|
4169
|
+
function resolveCwdAfterSegment(segment, cwd) {
|
|
4170
|
+
if (!segmentChangesCwd(segment)) {
|
|
4171
|
+
return;
|
|
4172
|
+
}
|
|
4173
|
+
if (!cwd) {
|
|
4174
|
+
return null;
|
|
4175
|
+
}
|
|
4176
|
+
const unwrapped = getCwdChangeTokens(segment, cwd);
|
|
4177
|
+
const cdIndex = getCdCommandIndex(unwrapped);
|
|
4178
|
+
if (cdIndex === -1 || unwrapped[cdIndex] !== "cd") {
|
|
4179
|
+
return null;
|
|
4180
|
+
}
|
|
4181
|
+
const target = unwrapped[cdIndex + 1];
|
|
4182
|
+
if (!target || target === "-" || target.includes("$") || target.includes("`")) {
|
|
4183
|
+
return null;
|
|
4184
|
+
}
|
|
4185
|
+
try {
|
|
4186
|
+
const resolved = resolveChdirTarget(cwd, target);
|
|
4187
|
+
if (samePath(resolved, cwd)) {
|
|
4188
|
+
return cwd;
|
|
4189
|
+
}
|
|
4190
|
+
} catch {
|
|
4191
|
+
return null;
|
|
4192
|
+
}
|
|
4193
|
+
return null;
|
|
4194
|
+
}
|
|
4166
4195
|
function getHeadAfterTimePrefix(tokens, startIndex) {
|
|
4167
4196
|
let i = startIndex;
|
|
4168
4197
|
while (tokens[i]?.startsWith("-")) {
|
|
@@ -4170,6 +4199,31 @@ function getHeadAfterTimePrefix(tokens, startIndex) {
|
|
|
4170
4199
|
}
|
|
4171
4200
|
return tokens[i] ?? "";
|
|
4172
4201
|
}
|
|
4202
|
+
function getCdCommandIndex(tokens) {
|
|
4203
|
+
let headIndex = 0;
|
|
4204
|
+
if (tokens[0] === "builtin" && tokens.length > 1) {
|
|
4205
|
+
headIndex = 1;
|
|
4206
|
+
}
|
|
4207
|
+
if (tokens[headIndex] !== "time") {
|
|
4208
|
+
return headIndex;
|
|
4209
|
+
}
|
|
4210
|
+
let i = headIndex + 1;
|
|
4211
|
+
while (tokens[i]?.startsWith("-")) {
|
|
4212
|
+
i++;
|
|
4213
|
+
}
|
|
4214
|
+
return i;
|
|
4215
|
+
}
|
|
4216
|
+
function getCwdChangeTokens(segment, cwd) {
|
|
4217
|
+
const stripped = stripLeadingGrouping(segment);
|
|
4218
|
+
return stripWrappers([...stripped], cwd);
|
|
4219
|
+
}
|
|
4220
|
+
function samePath(a, b) {
|
|
4221
|
+
try {
|
|
4222
|
+
return normalize3(realpathSync6(a)) === normalize3(realpathSync6(b));
|
|
4223
|
+
} catch {
|
|
4224
|
+
return normalize3(a) === normalize3(b);
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4173
4227
|
function stripLeadingGrouping(tokens) {
|
|
4174
4228
|
let i = 0;
|
|
4175
4229
|
while (i < tokens.length) {
|
|
@@ -4520,8 +4574,9 @@ function analyzeCommandInternal(command2, depth, options2) {
|
|
|
4520
4574
|
if (textReason) {
|
|
4521
4575
|
return { reason: textReason, segment: segmentStr };
|
|
4522
4576
|
}
|
|
4523
|
-
|
|
4524
|
-
|
|
4577
|
+
const nextCwd2 = resolveCwdAfterSegment(segment, effectiveCwd);
|
|
4578
|
+
if (nextCwd2 !== undefined) {
|
|
4579
|
+
effectiveCwd = nextCwd2;
|
|
4525
4580
|
}
|
|
4526
4581
|
continue;
|
|
4527
4582
|
}
|
|
@@ -4543,8 +4598,9 @@ function analyzeCommandInternal(command2, depth, options2) {
|
|
|
4543
4598
|
if (reason) {
|
|
4544
4599
|
return { reason, segment: segmentStr };
|
|
4545
4600
|
}
|
|
4546
|
-
|
|
4547
|
-
|
|
4601
|
+
const nextCwd = resolveCwdAfterSegment(segment, effectiveCwd);
|
|
4602
|
+
if (nextCwd !== undefined) {
|
|
4603
|
+
effectiveCwd = nextCwd;
|
|
4548
4604
|
}
|
|
4549
4605
|
applyShellGitContextEnvSegment(segment, shellGitContextState);
|
|
4550
4606
|
}
|
|
@@ -7830,7 +7886,7 @@ import { existsSync as existsSync14 } from "node:fs";
|
|
|
7830
7886
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
7831
7887
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
7832
7888
|
import { delimiter, extname, join as join11 } from "node:path";
|
|
7833
|
-
var CURRENT_VERSION = "1.0.
|
|
7889
|
+
var CURRENT_VERSION = "1.0.2";
|
|
7834
7890
|
var VERSION_FETCH_TIMEOUT_MS = 2000;
|
|
7835
7891
|
var PI_PROBE_TIMEOUT_MS = 5000;
|
|
7836
7892
|
var PI_SENTINEL_COMMAND = "cc-safety-net";
|
|
@@ -8882,12 +8938,14 @@ function explainCommand2(command2, options2) {
|
|
|
8882
8938
|
token: redactEnvAssignmentsInString(segment[0]),
|
|
8883
8939
|
matched: false
|
|
8884
8940
|
});
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8941
|
+
const nextCwd2 = resolveCwdAfterSegment(segment, effectiveCwd);
|
|
8942
|
+
if (nextCwd2 !== undefined) {
|
|
8943
|
+
if (nextCwd2 !== null) {
|
|
8944
|
+
effectiveCwd = nextCwd2;
|
|
8945
|
+
trace.segments.push({ index: i, steps: segmentSteps });
|
|
8946
|
+
continue;
|
|
8947
|
+
}
|
|
8948
|
+
segmentSteps.push(cwdChangeStep(segment));
|
|
8891
8949
|
effectiveCwd = null;
|
|
8892
8950
|
}
|
|
8893
8951
|
trace.segments.push({ index: i, steps: segmentSteps });
|
|
@@ -8903,12 +8961,15 @@ function explainCommand2(command2, options2) {
|
|
|
8903
8961
|
blockReason = result.reason;
|
|
8904
8962
|
blockSegment = redactEnvAssignmentsInString(segment.join(" "));
|
|
8905
8963
|
}
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8964
|
+
const nextCwd = resolveCwdAfterSegment(segment, effectiveCwd);
|
|
8965
|
+
if (nextCwd !== undefined) {
|
|
8966
|
+
if (nextCwd !== null) {
|
|
8967
|
+
effectiveCwd = nextCwd;
|
|
8968
|
+
applyShellGitContextEnvSegment(segment, shellGitContextState);
|
|
8969
|
+
trace.segments.push({ index: i, steps: segmentSteps });
|
|
8970
|
+
continue;
|
|
8971
|
+
}
|
|
8972
|
+
segmentSteps.push(cwdChangeStep(segment));
|
|
8912
8973
|
effectiveCwd = null;
|
|
8913
8974
|
}
|
|
8914
8975
|
applyShellGitContextEnvSegment(segment, shellGitContextState);
|
|
@@ -8924,6 +8985,13 @@ function explainCommand2(command2, options2) {
|
|
|
8924
8985
|
configValid
|
|
8925
8986
|
};
|
|
8926
8987
|
}
|
|
8988
|
+
function cwdChangeStep(segment) {
|
|
8989
|
+
return {
|
|
8990
|
+
type: "cwd-change",
|
|
8991
|
+
segment: redactEnvAssignmentsInString(segment.join(" ")),
|
|
8992
|
+
effectiveCwdNowUnknown: true
|
|
8993
|
+
};
|
|
8994
|
+
}
|
|
8927
8995
|
function getCustomRuleMetadata(reason, options2, cwd) {
|
|
8928
8996
|
const id = reason?.match(/^\[([^\]]+)]/)?.[1];
|
|
8929
8997
|
if (!id)
|
|
@@ -9327,7 +9395,7 @@ function formatTraceJson(result) {
|
|
|
9327
9395
|
return JSON.stringify(result, null, 2);
|
|
9328
9396
|
}
|
|
9329
9397
|
// src/bin/help.ts
|
|
9330
|
-
var version = "1.0.
|
|
9398
|
+
var version = "1.0.2";
|
|
9331
9399
|
var INDENT = " ";
|
|
9332
9400
|
var PROGRAM_NAME = "cc-safety-net";
|
|
9333
9401
|
function formatOptionFlags(option) {
|
|
@@ -11,10 +11,8 @@ type ConfiguredHookAdapter<T> = Omit<HookAdapter<T>, 'outputDeny'> & {
|
|
|
11
11
|
createDenyOutput: (message: string) => object;
|
|
12
12
|
getManualPermissionAdvice?: (reason: string) => boolean | undefined;
|
|
13
13
|
};
|
|
14
|
-
export declare function outputHookDeny(createDenyOutput: (message: string) => object, reason: string, command?: string, segment?: string, manualPermissionAdvice?: boolean): void;
|
|
15
|
-
export declare function readHookInput<T>(outputDeny: (reason: string) => void): Promise<T | null>;
|
|
16
14
|
export declare function parseHookJson<T>(inputText: string, outputDeny: (reason: string) => void, strictReason: string): T | null;
|
|
15
|
+
/** @internal - exported for direct test coverage */
|
|
17
16
|
export declare function handleBlockedHookCommand(command: string, cwd: string, sessionId: string | undefined, outputDeny: (reason: string, command?: string, segment?: string) => void): void;
|
|
18
|
-
export declare function runHookAdapter<T>(adapter: HookAdapter<T>): Promise<void>;
|
|
19
17
|
export declare function runConfiguredHookAdapter<T>(adapter: ConfiguredHookAdapter<T>): Promise<void>;
|
|
20
18
|
export {};
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import { type LoadedRulesPolicy, type RulebookLockEntryWithStats } from '@/core/rules/policy';
|
|
2
|
-
export declare function printSyncResult(result: {
|
|
3
|
-
ok: boolean;
|
|
4
|
-
errors: string[];
|
|
5
|
-
warnings?: string[];
|
|
6
|
-
entries: RulebookLockEntryWithStats[];
|
|
7
|
-
}): void;
|
|
8
2
|
export declare function printRuleChangeResult(result: {
|
|
9
3
|
ok: boolean;
|
|
10
4
|
errors: string[];
|
|
@@ -18,4 +12,3 @@ export declare function printRulesTestResult(result: {
|
|
|
18
12
|
entries: RulebookLockEntryWithStats[];
|
|
19
13
|
}, sourceDisplayMap?: Map<string, string>): void;
|
|
20
14
|
export declare function printRulesListReport(policy: LoadedRulesPolicy, sourceDisplayMaps: Record<'user' | 'project', Map<string, string>>): void;
|
|
21
|
-
export declare function relativeDisplay(cwd: string, path: string): string;
|
|
@@ -9,4 +9,5 @@ export interface ParallelAnalyzeContext {
|
|
|
9
9
|
analyzeNested: (command: string, overrides?: AnalyzeNestedOverrides) => string | null;
|
|
10
10
|
}
|
|
11
11
|
export declare function analyzeParallel(tokens: readonly string[], context: ParallelAnalyzeContext): string | null;
|
|
12
|
+
/** @internal - exported for test coverage */
|
|
12
13
|
export declare function extractParallelChildCommand(tokens: readonly string[]): string[];
|
|
@@ -8,3 +8,4 @@ export type InternalOptions = AnalyzeOptions & {
|
|
|
8
8
|
};
|
|
9
9
|
export declare function analyzeSegment(tokens: string[], depth: number, options: InternalOptions): string | null;
|
|
10
10
|
export declare function segmentChangesCwd(segment: readonly string[]): boolean;
|
|
11
|
+
export declare function resolveCwdAfterSegment(segment: readonly string[], cwd: string | null | undefined): string | null | undefined;
|
package/dist/core/git/env.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export declare const GIT_CONTEXT_ENV_OVERRIDES: readonly ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"];
|
|
2
|
+
/** @internal - exported for test coverage */
|
|
2
3
|
export declare const GIT_CONFIG_AFFECTING_ENV_NAMES: ReadonlySet<string>;
|
|
4
|
+
/** @internal - exported for test coverage */
|
|
3
5
|
export declare const GIT_SSH_ENV_NAMES: ReadonlySet<string>;
|
|
6
|
+
/** @internal - exported for test coverage */
|
|
4
7
|
export declare function isGitContextEnvOverrideName(name: string): boolean;
|
|
5
8
|
export declare function isGitConfigEnvName(name: string): boolean;
|
|
6
9
|
export declare function isTrackedGitEnvName(name: string): boolean;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export { readRulesConfig,
|
|
2
|
-
export { getLegacyUserRulesConfigPath, getProjectRulesConfigPath, getProjectRulesDir,
|
|
3
|
-
export { getRulebookMigratedFrom, getRulesConfigRuntimeErrorsForConfig, getRulesConfigSourceDisplayMap,
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export type { LoadedRulebookInfo, LoadedRulesPolicy, RulebookLockEntry, RulebookLockEntryWithStats, RulebookSourceKind, RuleOverride, RulesConfig, RulesLockfile, RulesPolicyOptions, SyncRulesConfigOptions, SyncRulesConfigResult, } from './types';
|
|
1
|
+
export { readRulesConfig, writeDefaultRulesConfig, writeStarterRulebook, } from './config-file';
|
|
2
|
+
export { getLegacyUserRulesConfigPath, getProjectRulesConfigPath, getProjectRulesDir, getRulebookDisplaySource, getRulesLockPathForConfigPath, getUserRulesConfigPath, getUserRulesDir, getUserRulesLockPath, RULES_DIR, } from './paths';
|
|
3
|
+
export { getRulebookMigratedFrom, getRulesConfigRuntimeErrorsForConfig, getRulesConfigSourceDisplayMap, loadRulesPolicy, } from './scope-policy';
|
|
4
|
+
export { addRulebookSource, removeRulebookSource, syncRulesConfig, testRulebookSources, } from './sync';
|
|
5
|
+
export type { LoadedRulesPolicy, RulebookLockEntryWithStats, RuleOverride, SyncRulesConfigOptions, } from './types';
|
|
@@ -17,6 +17,7 @@ export interface ScopePaths {
|
|
|
17
17
|
}
|
|
18
18
|
export declare function getProjectRulesDir(cwd?: string): string;
|
|
19
19
|
export declare function getProjectRulesConfigPath(cwd?: string): string;
|
|
20
|
+
/** @internal - exported for test coverage */
|
|
20
21
|
export declare function getProjectRulesLockPath(cwd?: string): string;
|
|
21
22
|
export declare function getUserRulesDir(options?: RulesPolicyOptions): string;
|
|
22
23
|
export declare function getUserRulesConfigPath(options?: RulesPolicyOptions): string;
|
|
@@ -11,6 +11,7 @@ interface ScopePolicy {
|
|
|
11
11
|
export declare function loadRulesPolicy(options?: RulesPolicyOptions): LoadedRulesPolicy;
|
|
12
12
|
export declare function getRulesConfigSourceDisplayMap(configPath: string): Map<string, string>;
|
|
13
13
|
export declare function getRulesConfigRuntimeErrorsForConfig(configPath: string, lockPath: string, options: RulesPolicyOptions): string[];
|
|
14
|
+
/** @internal - exported for test coverage */
|
|
14
15
|
export declare function getUnknownOverrideErrorsForConfig(configPath: string, lockPath: string, options: RulesPolicyOptions): string[];
|
|
15
16
|
export declare function loadScopePolicy(config: RulesConfig, lockPath: string, configDir: string, options: RulesPolicyOptions, source: 'user' | 'project'): ScopePolicy;
|
|
16
17
|
export declare function rulesPolicyToConfig(policy: LoadedRulesPolicy): Config;
|
|
@@ -14,6 +14,7 @@ export interface Rulebook {
|
|
|
14
14
|
rules: CustomRule[];
|
|
15
15
|
tests: RulebookFixture[];
|
|
16
16
|
}
|
|
17
|
+
/** @internal - exported for test coverage */
|
|
17
18
|
export interface RulebookFixtureFailure {
|
|
18
19
|
command: string;
|
|
19
20
|
message: string;
|
|
@@ -23,6 +24,7 @@ export interface RulebookFixtureResult {
|
|
|
23
24
|
ok: boolean;
|
|
24
25
|
failures: RulebookFixtureFailure[];
|
|
25
26
|
}
|
|
27
|
+
/** @internal - exported for test coverage */
|
|
26
28
|
export declare function validateRulebook(rulebook: unknown): ValidationResult;
|
|
27
29
|
export declare function runRulebookFixtures(rulebook: Rulebook): RulebookFixtureResult;
|
|
28
30
|
export declare function assertValidRulebook(rulebook: unknown): Rulebook;
|
package/dist/index.js
CHANGED
|
@@ -282,6 +282,10 @@ function dangerousInText(text) {
|
|
|
282
282
|
return null;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
// src/core/analyze/segment.ts
|
|
286
|
+
import { realpathSync as realpathSync6 } from "node:fs";
|
|
287
|
+
import { normalize as normalize3 } from "node:path";
|
|
288
|
+
|
|
285
289
|
// src/core/analyze/awk.ts
|
|
286
290
|
var AWK_INTERPRETERS = new Set(["awk", "gawk", "nawk", "mawk"]);
|
|
287
291
|
var REASON_AWK_SYSTEM_DYNAMIC = "Detected awk system() call with dynamic command that cannot be safely analyzed.";
|
|
@@ -4087,8 +4091,7 @@ function analyzeParallelCommand(context) {
|
|
|
4087
4091
|
}
|
|
4088
4092
|
var CWD_CHANGE_REGEX = /^\s*(?:\$\(\s*)?[({]*\s*(?:command\s+|builtin\s+)?(?:cd|pushd|popd)(?:\s|$)/;
|
|
4089
4093
|
function segmentChangesCwd(segment) {
|
|
4090
|
-
const
|
|
4091
|
-
const unwrapped = stripWrappers([...stripped]);
|
|
4094
|
+
const unwrapped = getCwdChangeTokens(segment);
|
|
4092
4095
|
if (unwrapped.length === 0) {
|
|
4093
4096
|
return false;
|
|
4094
4097
|
}
|
|
@@ -4107,6 +4110,32 @@ function segmentChangesCwd(segment) {
|
|
|
4107
4110
|
const joined = segment.join(" ");
|
|
4108
4111
|
return CWD_CHANGE_REGEX.test(joined);
|
|
4109
4112
|
}
|
|
4113
|
+
function resolveCwdAfterSegment(segment, cwd) {
|
|
4114
|
+
if (!segmentChangesCwd(segment)) {
|
|
4115
|
+
return;
|
|
4116
|
+
}
|
|
4117
|
+
if (!cwd) {
|
|
4118
|
+
return null;
|
|
4119
|
+
}
|
|
4120
|
+
const unwrapped = getCwdChangeTokens(segment, cwd);
|
|
4121
|
+
const cdIndex = getCdCommandIndex(unwrapped);
|
|
4122
|
+
if (cdIndex === -1 || unwrapped[cdIndex] !== "cd") {
|
|
4123
|
+
return null;
|
|
4124
|
+
}
|
|
4125
|
+
const target = unwrapped[cdIndex + 1];
|
|
4126
|
+
if (!target || target === "-" || target.includes("$") || target.includes("`")) {
|
|
4127
|
+
return null;
|
|
4128
|
+
}
|
|
4129
|
+
try {
|
|
4130
|
+
const resolved = resolveChdirTarget(cwd, target);
|
|
4131
|
+
if (samePath(resolved, cwd)) {
|
|
4132
|
+
return cwd;
|
|
4133
|
+
}
|
|
4134
|
+
} catch {
|
|
4135
|
+
return null;
|
|
4136
|
+
}
|
|
4137
|
+
return null;
|
|
4138
|
+
}
|
|
4110
4139
|
function getHeadAfterTimePrefix(tokens, startIndex) {
|
|
4111
4140
|
let i = startIndex;
|
|
4112
4141
|
while (tokens[i]?.startsWith("-")) {
|
|
@@ -4114,6 +4143,31 @@ function getHeadAfterTimePrefix(tokens, startIndex) {
|
|
|
4114
4143
|
}
|
|
4115
4144
|
return tokens[i] ?? "";
|
|
4116
4145
|
}
|
|
4146
|
+
function getCdCommandIndex(tokens) {
|
|
4147
|
+
let headIndex = 0;
|
|
4148
|
+
if (tokens[0] === "builtin" && tokens.length > 1) {
|
|
4149
|
+
headIndex = 1;
|
|
4150
|
+
}
|
|
4151
|
+
if (tokens[headIndex] !== "time") {
|
|
4152
|
+
return headIndex;
|
|
4153
|
+
}
|
|
4154
|
+
let i = headIndex + 1;
|
|
4155
|
+
while (tokens[i]?.startsWith("-")) {
|
|
4156
|
+
i++;
|
|
4157
|
+
}
|
|
4158
|
+
return i;
|
|
4159
|
+
}
|
|
4160
|
+
function getCwdChangeTokens(segment, cwd) {
|
|
4161
|
+
const stripped = stripLeadingGrouping(segment);
|
|
4162
|
+
return stripWrappers([...stripped], cwd);
|
|
4163
|
+
}
|
|
4164
|
+
function samePath(a, b) {
|
|
4165
|
+
try {
|
|
4166
|
+
return normalize3(realpathSync6(a)) === normalize3(realpathSync6(b));
|
|
4167
|
+
} catch {
|
|
4168
|
+
return normalize3(a) === normalize3(b);
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4117
4171
|
function stripLeadingGrouping(tokens) {
|
|
4118
4172
|
let i = 0;
|
|
4119
4173
|
while (i < tokens.length) {
|
|
@@ -4464,8 +4518,9 @@ function analyzeCommandInternal(command2, depth, options2) {
|
|
|
4464
4518
|
if (textReason) {
|
|
4465
4519
|
return { reason: textReason, segment: segmentStr };
|
|
4466
4520
|
}
|
|
4467
|
-
|
|
4468
|
-
|
|
4521
|
+
const nextCwd2 = resolveCwdAfterSegment(segment, effectiveCwd);
|
|
4522
|
+
if (nextCwd2 !== undefined) {
|
|
4523
|
+
effectiveCwd = nextCwd2;
|
|
4469
4524
|
}
|
|
4470
4525
|
continue;
|
|
4471
4526
|
}
|
|
@@ -4487,8 +4542,9 @@ function analyzeCommandInternal(command2, depth, options2) {
|
|
|
4487
4542
|
if (reason) {
|
|
4488
4543
|
return { reason, segment: segmentStr };
|
|
4489
4544
|
}
|
|
4490
|
-
|
|
4491
|
-
|
|
4545
|
+
const nextCwd = resolveCwdAfterSegment(segment, effectiveCwd);
|
|
4546
|
+
if (nextCwd !== undefined) {
|
|
4547
|
+
effectiveCwd = nextCwd;
|
|
4492
4548
|
}
|
|
4493
4549
|
applyShellGitContextEnvSegment(segment, shellGitContextState);
|
|
4494
4550
|
}
|
|
@@ -11,5 +11,6 @@ type PiCommandContext = {
|
|
|
11
11
|
isIdle: () => boolean;
|
|
12
12
|
};
|
|
13
13
|
export declare function registerBuiltinCommands(pi: PiCommandApi): void;
|
|
14
|
+
/** @internal - exported for test coverage */
|
|
14
15
|
export declare function buildSafetyNetCommandPrompt(args: string): string;
|
|
15
16
|
export {};
|
package/dist/pi/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { registerBuiltinCommands } from '@/pi/builtin-commands';
|
|
2
|
-
import {
|
|
3
|
-
type PiExtensionApi = Parameters<typeof registerBuiltinCommands>[0] & Parameters<typeof
|
|
2
|
+
import { registerToolCallEvent } from '@/pi/tool-call';
|
|
3
|
+
type PiExtensionApi = Parameters<typeof registerBuiltinCommands>[0] & Parameters<typeof registerToolCallEvent>[0];
|
|
4
4
|
export default function ccSafetyNetPiExtension(pi: PiExtensionApi): void;
|
|
5
5
|
export {};
|
package/dist/pi/index.js
CHANGED
|
@@ -275,6 +275,9 @@ function buildSafetyNetCommandPrompt(args) {
|
|
|
275
275
|
|
|
276
276
|
${args.trim() || DEFAULT_USER_REQUEST}`;
|
|
277
277
|
}
|
|
278
|
+
// src/pi/tool-call.ts
|
|
279
|
+
import { resolve as resolve8 } from "node:path";
|
|
280
|
+
|
|
278
281
|
// src/core/analyze/dangerous-text.ts
|
|
279
282
|
function dangerousInText(text) {
|
|
280
283
|
const t = text.toLowerCase();
|
|
@@ -343,6 +346,10 @@ function dangerousInText(text) {
|
|
|
343
346
|
return null;
|
|
344
347
|
}
|
|
345
348
|
|
|
349
|
+
// src/core/analyze/segment.ts
|
|
350
|
+
import { realpathSync as realpathSync6 } from "node:fs";
|
|
351
|
+
import { normalize as normalize3 } from "node:path";
|
|
352
|
+
|
|
346
353
|
// src/core/analyze/awk.ts
|
|
347
354
|
var AWK_INTERPRETERS = new Set(["awk", "gawk", "nawk", "mawk"]);
|
|
348
355
|
var REASON_AWK_SYSTEM_DYNAMIC = "Detected awk system() call with dynamic command that cannot be safely analyzed.";
|
|
@@ -4148,8 +4155,7 @@ function analyzeParallelCommand(context) {
|
|
|
4148
4155
|
}
|
|
4149
4156
|
var CWD_CHANGE_REGEX = /^\s*(?:\$\(\s*)?[({]*\s*(?:command\s+|builtin\s+)?(?:cd|pushd|popd)(?:\s|$)/;
|
|
4150
4157
|
function segmentChangesCwd(segment) {
|
|
4151
|
-
const
|
|
4152
|
-
const unwrapped = stripWrappers([...stripped]);
|
|
4158
|
+
const unwrapped = getCwdChangeTokens(segment);
|
|
4153
4159
|
if (unwrapped.length === 0) {
|
|
4154
4160
|
return false;
|
|
4155
4161
|
}
|
|
@@ -4168,6 +4174,32 @@ function segmentChangesCwd(segment) {
|
|
|
4168
4174
|
const joined = segment.join(" ");
|
|
4169
4175
|
return CWD_CHANGE_REGEX.test(joined);
|
|
4170
4176
|
}
|
|
4177
|
+
function resolveCwdAfterSegment(segment, cwd) {
|
|
4178
|
+
if (!segmentChangesCwd(segment)) {
|
|
4179
|
+
return;
|
|
4180
|
+
}
|
|
4181
|
+
if (!cwd) {
|
|
4182
|
+
return null;
|
|
4183
|
+
}
|
|
4184
|
+
const unwrapped = getCwdChangeTokens(segment, cwd);
|
|
4185
|
+
const cdIndex = getCdCommandIndex(unwrapped);
|
|
4186
|
+
if (cdIndex === -1 || unwrapped[cdIndex] !== "cd") {
|
|
4187
|
+
return null;
|
|
4188
|
+
}
|
|
4189
|
+
const target = unwrapped[cdIndex + 1];
|
|
4190
|
+
if (!target || target === "-" || target.includes("$") || target.includes("`")) {
|
|
4191
|
+
return null;
|
|
4192
|
+
}
|
|
4193
|
+
try {
|
|
4194
|
+
const resolved = resolveChdirTarget(cwd, target);
|
|
4195
|
+
if (samePath(resolved, cwd)) {
|
|
4196
|
+
return cwd;
|
|
4197
|
+
}
|
|
4198
|
+
} catch {
|
|
4199
|
+
return null;
|
|
4200
|
+
}
|
|
4201
|
+
return null;
|
|
4202
|
+
}
|
|
4171
4203
|
function getHeadAfterTimePrefix(tokens, startIndex) {
|
|
4172
4204
|
let i = startIndex;
|
|
4173
4205
|
while (tokens[i]?.startsWith("-")) {
|
|
@@ -4175,6 +4207,31 @@ function getHeadAfterTimePrefix(tokens, startIndex) {
|
|
|
4175
4207
|
}
|
|
4176
4208
|
return tokens[i] ?? "";
|
|
4177
4209
|
}
|
|
4210
|
+
function getCdCommandIndex(tokens) {
|
|
4211
|
+
let headIndex = 0;
|
|
4212
|
+
if (tokens[0] === "builtin" && tokens.length > 1) {
|
|
4213
|
+
headIndex = 1;
|
|
4214
|
+
}
|
|
4215
|
+
if (tokens[headIndex] !== "time") {
|
|
4216
|
+
return headIndex;
|
|
4217
|
+
}
|
|
4218
|
+
let i = headIndex + 1;
|
|
4219
|
+
while (tokens[i]?.startsWith("-")) {
|
|
4220
|
+
i++;
|
|
4221
|
+
}
|
|
4222
|
+
return i;
|
|
4223
|
+
}
|
|
4224
|
+
function getCwdChangeTokens(segment, cwd) {
|
|
4225
|
+
const stripped = stripLeadingGrouping(segment);
|
|
4226
|
+
return stripWrappers([...stripped], cwd);
|
|
4227
|
+
}
|
|
4228
|
+
function samePath(a, b) {
|
|
4229
|
+
try {
|
|
4230
|
+
return normalize3(realpathSync6(a)) === normalize3(realpathSync6(b));
|
|
4231
|
+
} catch {
|
|
4232
|
+
return normalize3(a) === normalize3(b);
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4178
4235
|
function stripLeadingGrouping(tokens) {
|
|
4179
4236
|
let i = 0;
|
|
4180
4237
|
while (i < tokens.length) {
|
|
@@ -4525,8 +4582,9 @@ function analyzeCommandInternal(command2, depth, options2) {
|
|
|
4525
4582
|
if (textReason) {
|
|
4526
4583
|
return { reason: textReason, segment: segmentStr };
|
|
4527
4584
|
}
|
|
4528
|
-
|
|
4529
|
-
|
|
4585
|
+
const nextCwd2 = resolveCwdAfterSegment(segment, effectiveCwd);
|
|
4586
|
+
if (nextCwd2 !== undefined) {
|
|
4587
|
+
effectiveCwd = nextCwd2;
|
|
4530
4588
|
}
|
|
4531
4589
|
continue;
|
|
4532
4590
|
}
|
|
@@ -4548,8 +4606,9 @@ function analyzeCommandInternal(command2, depth, options2) {
|
|
|
4548
4606
|
if (reason) {
|
|
4549
4607
|
return { reason, segment: segmentStr };
|
|
4550
4608
|
}
|
|
4551
|
-
|
|
4552
|
-
|
|
4609
|
+
const nextCwd = resolveCwdAfterSegment(segment, effectiveCwd);
|
|
4610
|
+
if (nextCwd !== undefined) {
|
|
4611
|
+
effectiveCwd = nextCwd;
|
|
4553
4612
|
}
|
|
4554
4613
|
applyShellGitContextEnvSegment(segment, shellGitContextState);
|
|
4555
4614
|
}
|
|
@@ -6331,22 +6390,34 @@ async function runConfiguredHookAdapter(adapter) {
|
|
|
6331
6390
|
});
|
|
6332
6391
|
}
|
|
6333
6392
|
|
|
6334
|
-
// src/pi/tool-
|
|
6335
|
-
|
|
6336
|
-
|
|
6393
|
+
// src/pi/tool-call.ts
|
|
6394
|
+
var PI_SHELL_TOOL_ADAPTERS = {
|
|
6395
|
+
bash: {
|
|
6396
|
+
commandField: "command"
|
|
6397
|
+
},
|
|
6398
|
+
Shell: {
|
|
6399
|
+
commandField: "command",
|
|
6400
|
+
cwdField: "working_directory"
|
|
6401
|
+
}
|
|
6402
|
+
};
|
|
6403
|
+
function registerToolCallEvent(pi) {
|
|
6404
|
+
pi.on("tool_call", handlePiToolCall);
|
|
6337
6405
|
}
|
|
6338
|
-
function
|
|
6339
|
-
|
|
6406
|
+
function handlePiToolCall(event, ctx) {
|
|
6407
|
+
const shellToolCall = getPiShellToolCall(event, ctx);
|
|
6408
|
+
if (!shellToolCall)
|
|
6340
6409
|
return;
|
|
6341
|
-
if (
|
|
6342
|
-
return
|
|
6410
|
+
if ("malformed" in shellToolCall) {
|
|
6411
|
+
return blockPiToolCall(REASON_SAFETY_NET_FAILED_CLOSED);
|
|
6343
6412
|
}
|
|
6413
|
+
const command2 = shellToolCall.command;
|
|
6414
|
+
const cwd = shellToolCall.cwd;
|
|
6344
6415
|
const modes = getCCSafetyNetEnvModes();
|
|
6345
6416
|
let result;
|
|
6346
6417
|
try {
|
|
6347
|
-
result = (ctx.safetyNetAnalyzeCommand ?? analyzeCommand)(
|
|
6348
|
-
cwd
|
|
6349
|
-
config: loadConfig(
|
|
6418
|
+
result = (ctx.safetyNetAnalyzeCommand ?? analyzeCommand)(command2, {
|
|
6419
|
+
cwd,
|
|
6420
|
+
config: loadConfig(cwd, {
|
|
6350
6421
|
repairLocalRulebooks: true,
|
|
6351
6422
|
...ctx.safetyNetConfigOptions
|
|
6352
6423
|
}),
|
|
@@ -6357,14 +6428,14 @@ function handlePiToolUse(event, ctx) {
|
|
|
6357
6428
|
});
|
|
6358
6429
|
} catch (error) {
|
|
6359
6430
|
if (envTruthy(ENV_FLAGS.debug)) {
|
|
6360
|
-
console.error(`CC Safety Net debug: pi
|
|
6431
|
+
console.error(`CC Safety Net debug: pi tool_call analysis failed: ${redactSecrets(error instanceof Error ? error.message : String(error))}`);
|
|
6361
6432
|
}
|
|
6362
|
-
return
|
|
6433
|
+
return blockPiToolCall(REASON_SAFETY_NET_FAILED_CLOSED, command2, command2);
|
|
6363
6434
|
}
|
|
6364
6435
|
if (!result) {
|
|
6365
6436
|
const sessionId2 = ctx.sessionManager.getSessionFile();
|
|
6366
6437
|
if (sessionId2 && envTruthy(ENV_FLAGS.debug)) {
|
|
6367
|
-
writeAuditLog(sessionId2,
|
|
6438
|
+
writeAuditLog(sessionId2, command2, command2, "allowed", cwd, {
|
|
6368
6439
|
decision: "allow"
|
|
6369
6440
|
});
|
|
6370
6441
|
}
|
|
@@ -6372,17 +6443,29 @@ function handlePiToolUse(event, ctx) {
|
|
|
6372
6443
|
}
|
|
6373
6444
|
const sessionId = ctx.sessionManager.getSessionFile();
|
|
6374
6445
|
if (sessionId) {
|
|
6375
|
-
writeAuditLog(sessionId,
|
|
6446
|
+
writeAuditLog(sessionId, command2, result.segment, result.reason, cwd);
|
|
6376
6447
|
}
|
|
6377
|
-
return
|
|
6448
|
+
return blockPiToolCall(result.reason, command2, result.segment, result.manualPermissionAdvice);
|
|
6378
6449
|
}
|
|
6379
|
-
function
|
|
6450
|
+
function getPiShellToolCall(event, ctx) {
|
|
6380
6451
|
if (!event || typeof event !== "object")
|
|
6381
|
-
return
|
|
6382
|
-
const
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6452
|
+
return;
|
|
6453
|
+
const toolCall = event;
|
|
6454
|
+
if (typeof toolCall.toolName !== "string")
|
|
6455
|
+
return;
|
|
6456
|
+
const adapter = PI_SHELL_TOOL_ADAPTERS[toolCall.toolName];
|
|
6457
|
+
if (!adapter)
|
|
6458
|
+
return;
|
|
6459
|
+
if (!toolCall.input || typeof toolCall.input !== "object")
|
|
6460
|
+
return { malformed: true };
|
|
6461
|
+
const command2 = toolCall.input[adapter.commandField];
|
|
6462
|
+
if (typeof command2 !== "string")
|
|
6463
|
+
return { malformed: true };
|
|
6464
|
+
const cwdInput = adapter.cwdField ? toolCall.input[adapter.cwdField] : undefined;
|
|
6465
|
+
const cwd = typeof cwdInput === "string" ? resolve8(ctx.cwd, cwdInput) : ctx.cwd;
|
|
6466
|
+
return { command: command2, cwd };
|
|
6467
|
+
}
|
|
6468
|
+
function blockPiToolCall(reason, command2, segment, manualPermissionAdvice) {
|
|
6386
6469
|
return {
|
|
6387
6470
|
block: true,
|
|
6388
6471
|
reason: formatBlockedMessage({
|
|
@@ -6397,7 +6480,7 @@ function blockPiToolUse(reason, command2, segment, manualPermissionAdvice) {
|
|
|
6397
6480
|
|
|
6398
6481
|
// src/pi/index.ts
|
|
6399
6482
|
function ccSafetyNetPiExtension(pi) {
|
|
6400
|
-
|
|
6483
|
+
registerToolCallEvent(pi);
|
|
6401
6484
|
registerBuiltinCommands(pi);
|
|
6402
6485
|
}
|
|
6403
6486
|
export {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { analyzeCommand } from '@/core/analyze';
|
|
2
2
|
import type { LoadConfigOptions } from '@/core/config';
|
|
3
3
|
type PiApi = {
|
|
4
|
-
on: (event: 'tool_call', handler: (event: unknown, ctx:
|
|
4
|
+
on: (event: 'tool_call', handler: (event: unknown, ctx: PiToolCallContext) => PiToolCallResult) => void;
|
|
5
5
|
};
|
|
6
|
-
type
|
|
6
|
+
type PiToolCallContext = {
|
|
7
7
|
cwd: string;
|
|
8
8
|
sessionManager: {
|
|
9
9
|
getSessionFile: () => string | undefined;
|
|
@@ -11,10 +11,11 @@ type PiToolUseContext = {
|
|
|
11
11
|
safetyNetAnalyzeCommand?: typeof analyzeCommand;
|
|
12
12
|
safetyNetConfigOptions?: LoadConfigOptions;
|
|
13
13
|
};
|
|
14
|
-
type
|
|
14
|
+
type PiToolCallResult = {
|
|
15
15
|
block: true;
|
|
16
16
|
reason: string;
|
|
17
17
|
} | undefined;
|
|
18
|
-
export declare function
|
|
19
|
-
|
|
18
|
+
export declare function registerToolCallEvent(pi: PiApi): void;
|
|
19
|
+
/** @internal - exported for test coverage */
|
|
20
|
+
export declare function handlePiToolCall(event: unknown, ctx: PiToolCallContext): PiToolCallResult;
|
|
20
21
|
export {};
|