@seanmozeik/tripwire 0.6.3 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tripwire-cli.js +1 -1
- package/dist/tripwire-cli.js.jsc +0 -0
- package/dist/tripwire.js +60 -60
- package/dist/tripwire.js.jsc +0 -0
- package/package.json +1 -1
- package/src/dispatch.ts +34 -3
- package/src/index.ts +2 -2
- package/src/lib/config.ts +42 -16
- package/src/rules/bash-tool-policy.ts +1 -1
package/dist/tripwire.js.jsc
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/dispatch.ts
CHANGED
|
@@ -18,8 +18,14 @@ import { BunRuntime } from '@effect/platform-bun';
|
|
|
18
18
|
import { Cause, Effect, Exit, Schema } from 'effect';
|
|
19
19
|
|
|
20
20
|
import { parseCommand } from './lib/bash';
|
|
21
|
-
import {
|
|
22
|
-
|
|
21
|
+
import {
|
|
22
|
+
CONFIG_PATH,
|
|
23
|
+
getDefaultConfig,
|
|
24
|
+
loadConfigResult,
|
|
25
|
+
mergeWithDefaults,
|
|
26
|
+
type Config,
|
|
27
|
+
} from './lib/config';
|
|
28
|
+
import { type Decision, allow, deny, merge } from './lib/decision';
|
|
23
29
|
import {
|
|
24
30
|
type BashInput,
|
|
25
31
|
type EditInput,
|
|
@@ -255,8 +261,17 @@ const handleAllow = (event: HookEvent, decision: Decision, config: Config): void
|
|
|
255
261
|
writeAllow();
|
|
256
262
|
};
|
|
257
263
|
|
|
264
|
+
// A broken config (bad JSON / unknown field / decode failure) silently dropping
|
|
265
|
+
// All custom policy is the dangerous case this guards. Fail closed: deny the
|
|
266
|
+
// Pending PreToolUse call with the decode error inline so the agent halts and
|
|
267
|
+
// The user sees it, rather than running on bare defaults unannounced.
|
|
268
|
+
const configErrorMessage = (error: string): string =>
|
|
269
|
+
`tripwire config at ${CONFIG_PATH} failed to load, so ALL custom safety policy is ` +
|
|
270
|
+
`inactive. Failing closed until it is fixed. Fix the JSON, then this clears on the next ` +
|
|
271
|
+
`call (the shim daemon caches config at warm — restart it there). Error: ${error}`;
|
|
272
|
+
|
|
258
273
|
const program = Effect.gen(function* () {
|
|
259
|
-
const
|
|
274
|
+
const configLoad = yield* loadConfigResult();
|
|
260
275
|
const raw = yield* Effect.promise(readStdin);
|
|
261
276
|
|
|
262
277
|
const parseExit = yield* Effect.exit(
|
|
@@ -278,6 +293,22 @@ const program = Effect.gen(function* () {
|
|
|
278
293
|
}
|
|
279
294
|
const event = decodeExit.value;
|
|
280
295
|
|
|
296
|
+
if (!configLoad.ok) {
|
|
297
|
+
if (event.hook_event_name === 'PreToolUse') {
|
|
298
|
+
writePreToolGate(
|
|
299
|
+
event.hook_event_name,
|
|
300
|
+
deny('config-error', configErrorMessage(configLoad.error)),
|
|
301
|
+
);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Config governs PreToolUse gating; PostToolUse secret-scrub is config-
|
|
305
|
+
// Independent and there is always an imminent next PreToolUse to surface the
|
|
306
|
+
// Deny, so don't block already-run output here.
|
|
307
|
+
writeAllow();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const config = configLoad.config;
|
|
311
|
+
|
|
281
312
|
if (event.hook_event_name === 'PreToolUse') {
|
|
282
313
|
const decision = decide(event, config);
|
|
283
314
|
if (decision.kind === 'deny' || decision.kind === 'ask') {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { Decision } from './lib/decision.ts';
|
|
2
2
|
export type { HookEvent } from './lib/event.ts';
|
|
3
|
-
export type { Config } from './lib/config.ts';
|
|
3
|
+
export type { Config, ConfigLoad } from './lib/config.ts';
|
|
4
4
|
export { allow, deny, ask, warn } from './lib/decision.ts';
|
|
5
5
|
export { decide } from './dispatch.ts';
|
|
6
|
-
export { getDefaultConfig, loadConfig, mergeWithDefaults } from './lib/config.ts';
|
|
6
|
+
export { getDefaultConfig, loadConfig, loadConfigResult, mergeWithDefaults } from './lib/config.ts';
|
package/src/lib/config.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { accessSync, constants, readFileSync } from 'node:fs';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
|
|
8
|
-
import { Effect, Schema } from 'effect';
|
|
8
|
+
import { Cause, Effect, Schema } from 'effect';
|
|
9
9
|
|
|
10
10
|
const BlockRuleSchema = Schema.Struct({
|
|
11
11
|
pattern: Schema.String,
|
|
@@ -36,24 +36,28 @@ const ConfigSchema = Schema.Struct({
|
|
|
36
36
|
|
|
37
37
|
const CONFIG_PATH = `${homedir()}/.config/tripwire/config.json`;
|
|
38
38
|
|
|
39
|
-
const configExists = (): Effect.Effect<boolean> =>
|
|
39
|
+
const configExists = (path: string): Effect.Effect<boolean> =>
|
|
40
40
|
Effect.sync(() => {
|
|
41
41
|
try {
|
|
42
|
-
accessSync(
|
|
42
|
+
accessSync(path, constants.R_OK);
|
|
43
43
|
return true;
|
|
44
44
|
} catch {
|
|
45
45
|
return false;
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
const readConfigFile = (): Effect.Effect<string, Error> =>
|
|
50
|
-
Effect.try({ try: () => readFileSync(
|
|
49
|
+
const readConfigFile = (path: string): Effect.Effect<string, Error> =>
|
|
50
|
+
Effect.try({ try: () => readFileSync(path, 'utf8'), catch: (error) => error as Error });
|
|
51
51
|
|
|
52
52
|
const parseConfigJson = (raw: string): Effect.Effect<unknown, Error> =>
|
|
53
53
|
Effect.try({ try: () => JSON.parse(raw) as unknown, catch: (error) => error as Error });
|
|
54
54
|
|
|
55
|
+
// `onExcessProperty: 'error'` rejects unknown keys (the default 'ignore' would
|
|
56
|
+
// Silently strip them — a typo'd `blockedComands` would vanish unnoticed, the
|
|
57
|
+
// Same silent-policy-drop class this whole change exists to kill). A stray key
|
|
58
|
+
// Now fails loud, e.g. the `rtk` block that triggered MTA-137.
|
|
55
59
|
const decodeConfig = (unknown: unknown): Effect.Effect<Config, Error> =>
|
|
56
|
-
Schema.decodeUnknownEffect(ConfigSchema)(unknown);
|
|
60
|
+
Schema.decodeUnknownEffect(ConfigSchema)(unknown, { onExcessProperty: 'error' });
|
|
57
61
|
|
|
58
62
|
const getDefaultConfig = (): Config => ({
|
|
59
63
|
git: {
|
|
@@ -72,30 +76,52 @@ const mergeWithDefaults = (partial: Config): Config => ({
|
|
|
72
76
|
allowedCommands: partial.allowedCommands ?? getDefaultConfig().allowedCommands,
|
|
73
77
|
});
|
|
74
78
|
|
|
75
|
-
|
|
79
|
+
// A present-but-broken config (bad JSON, schema decode failure, timeout) must
|
|
80
|
+
// Never be papered over with defaults — that silently drops all custom safety
|
|
81
|
+
// Policy. `loadConfigResult` reports the failure as data so callers can fail
|
|
82
|
+
// Closed loudly (see `loadConfig` and the dispatcher). A *missing* file is the
|
|
83
|
+
// One legitimate defaults case.
|
|
84
|
+
type ConfigLoad =
|
|
85
|
+
| { readonly ok: true; readonly config: Config }
|
|
86
|
+
| { readonly ok: false; readonly error: string };
|
|
87
|
+
|
|
88
|
+
export const loadConfigResult = (path: string = CONFIG_PATH): Effect.Effect<ConfigLoad> =>
|
|
76
89
|
Effect.gen(function* () {
|
|
77
|
-
const exists = yield* configExists();
|
|
90
|
+
const exists = yield* configExists(path);
|
|
78
91
|
if (!exists) {
|
|
79
|
-
|
|
92
|
+
const result: ConfigLoad = { ok: true, config: getDefaultConfig() };
|
|
93
|
+
return result;
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
const raw = yield* readConfigFile();
|
|
96
|
+
const raw = yield* readConfigFile(path);
|
|
83
97
|
const parsed = yield* parseConfigJson(raw);
|
|
84
98
|
const config = yield* decodeConfig(parsed);
|
|
85
|
-
|
|
99
|
+
const result: ConfigLoad = { ok: true, config: mergeWithDefaults(config) };
|
|
100
|
+
return result;
|
|
86
101
|
}).pipe(
|
|
87
102
|
Effect.timeout(1000),
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
console.error('[tripwire] Config loading failed, using defaults');
|
|
92
|
-
return Effect.succeed(getDefaultConfig());
|
|
103
|
+
Effect.catchCause((cause) => {
|
|
104
|
+
const result: ConfigLoad = { ok: false, error: Cause.pretty(cause) };
|
|
105
|
+
return Effect.succeed(result);
|
|
93
106
|
}),
|
|
94
107
|
);
|
|
95
108
|
|
|
109
|
+
// Loud loader for library consumers (e.g. the shim daemon) that expect a
|
|
110
|
+
// `Config`. A broken config dies rather than silently defaulting, so the
|
|
111
|
+
// Consumer fails closed visibly until the file is fixed.
|
|
112
|
+
export const loadConfig = (path: string = CONFIG_PATH): Effect.Effect<Config> =>
|
|
113
|
+
loadConfigResult(path).pipe(
|
|
114
|
+
Effect.flatMap((result) =>
|
|
115
|
+
result.ok
|
|
116
|
+
? Effect.succeed(result.config)
|
|
117
|
+
: Effect.die(new Error(`[tripwire] config load failed (${path}): ${result.error}`)),
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
|
|
96
121
|
export type BlockRule = typeof BlockRuleSchema.Type;
|
|
97
122
|
export type GitConfig = typeof GitConfigSchema.Type;
|
|
98
123
|
export type SafePathsConfig = typeof SafePathsConfigSchema.Type;
|
|
99
124
|
export type Config = typeof ConfigSchema.Type;
|
|
100
125
|
|
|
126
|
+
export type { ConfigLoad };
|
|
101
127
|
export { CONFIG_PATH, ConfigSchema, getDefaultConfig, mergeWithDefaults };
|
|
@@ -88,7 +88,7 @@ const POLICIES: readonly Policy[] = [
|
|
|
88
88
|
rule: 'consider-rg',
|
|
89
89
|
action: 'warn',
|
|
90
90
|
message:
|
|
91
|
-
'Consider `rg` (ripgrep) instead of `grep`. Faster
|
|
91
|
+
'Consider `rg` (ripgrep) instead of `grep`. Faster; recurses by default; respects .gitignore by default (use `-u`/`-uu` to include ignored/hidden files); searches the CWD when no path is given. NOT a clean flag-for-flag swap; some flags differ or are unneeded. Carry over fine: `-i`, `-n`, `-v`, `-l`, `-c`. WATCH OUT: `-r` is NOT recursive in rg — it means `--replace` (rg already recurses), so `rg -rn "PATTERN" dir/` silently parses as `--replace=n` and rewrites every match to the literal "n" while exiting 0. Drop the `-r`: `grep -rn PATTERN .` → `rg -n PATTERN`. And `grep --include`/`--exclude` become `rg -g GLOB`/`-g !GLOB`.',
|
|
92
92
|
fires: (seg) => seg.head === 'grep' || seg.head === 'egrep' || seg.head === 'fgrep',
|
|
93
93
|
},
|
|
94
94
|
{
|