cc-safety-net 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/bin/cc-safety-net.js +2 -2
- 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/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/pi/builtin-commands/commands.d.ts +1 -0
- package/dist/pi/index.d.ts +2 -2
- package/dist/pi/index.js +49 -22
- 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
|
|
@@ -7830,7 +7830,7 @@ import { existsSync as existsSync14 } from "node:fs";
|
|
|
7830
7830
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
7831
7831
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
7832
7832
|
import { delimiter, extname, join as join11 } from "node:path";
|
|
7833
|
-
var CURRENT_VERSION = "1.0.
|
|
7833
|
+
var CURRENT_VERSION = "1.0.1";
|
|
7834
7834
|
var VERSION_FETCH_TIMEOUT_MS = 2000;
|
|
7835
7835
|
var PI_PROBE_TIMEOUT_MS = 5000;
|
|
7836
7836
|
var PI_SENTINEL_COMMAND = "cc-safety-net";
|
|
@@ -9327,7 +9327,7 @@ function formatTraceJson(result) {
|
|
|
9327
9327
|
return JSON.stringify(result, null, 2);
|
|
9328
9328
|
}
|
|
9329
9329
|
// src/bin/help.ts
|
|
9330
|
-
var version = "1.0.
|
|
9330
|
+
var version = "1.0.1";
|
|
9331
9331
|
var INDENT = " ";
|
|
9332
9332
|
var PROGRAM_NAME = "cc-safety-net";
|
|
9333
9333
|
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[];
|
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;
|
|
@@ -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();
|
|
@@ -6331,22 +6334,34 @@ async function runConfiguredHookAdapter(adapter) {
|
|
|
6331
6334
|
});
|
|
6332
6335
|
}
|
|
6333
6336
|
|
|
6334
|
-
// src/pi/tool-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
+
// src/pi/tool-call.ts
|
|
6338
|
+
var PI_SHELL_TOOL_ADAPTERS = {
|
|
6339
|
+
bash: {
|
|
6340
|
+
commandField: "command"
|
|
6341
|
+
},
|
|
6342
|
+
Shell: {
|
|
6343
|
+
commandField: "command",
|
|
6344
|
+
cwdField: "working_directory"
|
|
6345
|
+
}
|
|
6346
|
+
};
|
|
6347
|
+
function registerToolCallEvent(pi) {
|
|
6348
|
+
pi.on("tool_call", handlePiToolCall);
|
|
6337
6349
|
}
|
|
6338
|
-
function
|
|
6339
|
-
|
|
6350
|
+
function handlePiToolCall(event, ctx) {
|
|
6351
|
+
const shellToolCall = getPiShellToolCall(event, ctx);
|
|
6352
|
+
if (!shellToolCall)
|
|
6340
6353
|
return;
|
|
6341
|
-
if (
|
|
6342
|
-
return
|
|
6354
|
+
if ("malformed" in shellToolCall) {
|
|
6355
|
+
return blockPiToolCall(REASON_SAFETY_NET_FAILED_CLOSED);
|
|
6343
6356
|
}
|
|
6357
|
+
const command2 = shellToolCall.command;
|
|
6358
|
+
const cwd = shellToolCall.cwd;
|
|
6344
6359
|
const modes = getCCSafetyNetEnvModes();
|
|
6345
6360
|
let result;
|
|
6346
6361
|
try {
|
|
6347
|
-
result = (ctx.safetyNetAnalyzeCommand ?? analyzeCommand)(
|
|
6348
|
-
cwd
|
|
6349
|
-
config: loadConfig(
|
|
6362
|
+
result = (ctx.safetyNetAnalyzeCommand ?? analyzeCommand)(command2, {
|
|
6363
|
+
cwd,
|
|
6364
|
+
config: loadConfig(cwd, {
|
|
6350
6365
|
repairLocalRulebooks: true,
|
|
6351
6366
|
...ctx.safetyNetConfigOptions
|
|
6352
6367
|
}),
|
|
@@ -6357,14 +6372,14 @@ function handlePiToolUse(event, ctx) {
|
|
|
6357
6372
|
});
|
|
6358
6373
|
} catch (error) {
|
|
6359
6374
|
if (envTruthy(ENV_FLAGS.debug)) {
|
|
6360
|
-
console.error(`CC Safety Net debug: pi
|
|
6375
|
+
console.error(`CC Safety Net debug: pi tool_call analysis failed: ${redactSecrets(error instanceof Error ? error.message : String(error))}`);
|
|
6361
6376
|
}
|
|
6362
|
-
return
|
|
6377
|
+
return blockPiToolCall(REASON_SAFETY_NET_FAILED_CLOSED, command2, command2);
|
|
6363
6378
|
}
|
|
6364
6379
|
if (!result) {
|
|
6365
6380
|
const sessionId2 = ctx.sessionManager.getSessionFile();
|
|
6366
6381
|
if (sessionId2 && envTruthy(ENV_FLAGS.debug)) {
|
|
6367
|
-
writeAuditLog(sessionId2,
|
|
6382
|
+
writeAuditLog(sessionId2, command2, command2, "allowed", cwd, {
|
|
6368
6383
|
decision: "allow"
|
|
6369
6384
|
});
|
|
6370
6385
|
}
|
|
@@ -6372,17 +6387,29 @@ function handlePiToolUse(event, ctx) {
|
|
|
6372
6387
|
}
|
|
6373
6388
|
const sessionId = ctx.sessionManager.getSessionFile();
|
|
6374
6389
|
if (sessionId) {
|
|
6375
|
-
writeAuditLog(sessionId,
|
|
6390
|
+
writeAuditLog(sessionId, command2, result.segment, result.reason, cwd);
|
|
6376
6391
|
}
|
|
6377
|
-
return
|
|
6392
|
+
return blockPiToolCall(result.reason, command2, result.segment, result.manualPermissionAdvice);
|
|
6378
6393
|
}
|
|
6379
|
-
function
|
|
6394
|
+
function getPiShellToolCall(event, ctx) {
|
|
6380
6395
|
if (!event || typeof event !== "object")
|
|
6381
|
-
return
|
|
6382
|
-
const
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6396
|
+
return;
|
|
6397
|
+
const toolCall = event;
|
|
6398
|
+
if (typeof toolCall.toolName !== "string")
|
|
6399
|
+
return;
|
|
6400
|
+
const adapter = PI_SHELL_TOOL_ADAPTERS[toolCall.toolName];
|
|
6401
|
+
if (!adapter)
|
|
6402
|
+
return;
|
|
6403
|
+
if (!toolCall.input || typeof toolCall.input !== "object")
|
|
6404
|
+
return { malformed: true };
|
|
6405
|
+
const command2 = toolCall.input[adapter.commandField];
|
|
6406
|
+
if (typeof command2 !== "string")
|
|
6407
|
+
return { malformed: true };
|
|
6408
|
+
const cwdInput = adapter.cwdField ? toolCall.input[adapter.cwdField] : undefined;
|
|
6409
|
+
const cwd = typeof cwdInput === "string" ? resolve8(ctx.cwd, cwdInput) : ctx.cwd;
|
|
6410
|
+
return { command: command2, cwd };
|
|
6411
|
+
}
|
|
6412
|
+
function blockPiToolCall(reason, command2, segment, manualPermissionAdvice) {
|
|
6386
6413
|
return {
|
|
6387
6414
|
block: true,
|
|
6388
6415
|
reason: formatBlockedMessage({
|
|
@@ -6397,7 +6424,7 @@ function blockPiToolUse(reason, command2, segment, manualPermissionAdvice) {
|
|
|
6397
6424
|
|
|
6398
6425
|
// src/pi/index.ts
|
|
6399
6426
|
function ccSafetyNetPiExtension(pi) {
|
|
6400
|
-
|
|
6427
|
+
registerToolCallEvent(pi);
|
|
6401
6428
|
registerBuiltinCommands(pi);
|
|
6402
6429
|
}
|
|
6403
6430
|
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 {};
|