@tuent/sentinel 0.1.3 → 0.1.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.
@@ -1,5 +1,81 @@
1
1
  // src/policyLoader.ts
2
2
  import yaml from "js-yaml";
3
+
4
+ // src/defaults.ts
5
+ var DEFAULT_FORBIDDEN_PATTERNS = [
6
+ "**/.env",
7
+ "**/.env.*",
8
+ "**/.ssh/**",
9
+ "**/.aws/**",
10
+ "**/secrets/**",
11
+ "**/credentials/**",
12
+ "**/id_rsa*",
13
+ "**/id_dsa*",
14
+ "**/id_ecdsa*",
15
+ "**/id_ed25519*",
16
+ "**/*.pem",
17
+ "**/*.key",
18
+ "/etc/**",
19
+ // Sprint 26 FIX 1 (A) — common credential stores. DRIFT: keep in sync with
20
+ // STARTER_POLICY.forbid.targets in setup/initClaudeCode.ts (template↔defaults
21
+ // unification tracked in a separate ticket — do not refactor here).
22
+ "**/.netrc",
23
+ "**/.npmrc",
24
+ "**/.git-credentials",
25
+ "**/.pgpass",
26
+ "**/.zsh_history",
27
+ "**/.config/gh/**",
28
+ "**/.docker/config.json",
29
+ "**/.gnupg/**",
30
+ "**/.config/gcloud/**",
31
+ "**/.kube/**",
32
+ "**/Library/Keychains/**",
33
+ // Sprint 26 FIX 1 (B) — Sentinel's own state dir (current path only; the
34
+ // ~/.dahlia → ~/.sentinel rename is a separate re-arch).
35
+ "**/.dahlia/**",
36
+ // Sprint 26 FIX 3 — the live policy file and cc's hook-wiring settings files.
37
+ // Denies the agent's own tool-writes (policy rewrite / unhook vectors); reads
38
+ // stay allowed via DEFAULT_POLICY_READ_EXCEPTIONS below. The settings glob
39
+ // covers project AND user-level (~/.claude/settings*.json) in one pattern.
40
+ // DRIFT: keep in sync with STARTER_POLICY.forbid.targets in setup/initClaudeCode.ts.
41
+ "**/.sentinel.yaml",
42
+ "**/.claude/settings*.json"
43
+ ];
44
+ var DEFAULT_POLICY_READ_EXCEPTIONS = [
45
+ { target: "**/.sentinel.yaml", allowedActions: ["file_read"] },
46
+ { target: "**/.claude/settings*.json", allowedActions: ["file_read"] }
47
+ ];
48
+ function withPolicyReadExceptions(existing) {
49
+ const merged = existing ? [...existing] : [];
50
+ for (const exc of DEFAULT_POLICY_READ_EXCEPTIONS) {
51
+ if (!merged.some((e) => e.target === exc.target)) merged.push(exc);
52
+ }
53
+ return merged;
54
+ }
55
+ var DEFAULT_MEDIUM_DISPOSITION = {
56
+ network_request: "deny"
57
+ };
58
+ var DEFAULT_RESTRICT_AFTER = 2;
59
+ var DEFAULT_QUARANTINE_AFTER = 3;
60
+ var DEFAULT_NETWORK_DENYLIST_CIDRS = [
61
+ "10.0.0.0/8",
62
+ // RFC1918 private (Class A)
63
+ "172.16.0.0/12",
64
+ // RFC1918 private (Class B)
65
+ "192.168.0.0/16",
66
+ // RFC1918 private (Class C)
67
+ "127.0.0.0/8",
68
+ // loopback v4
69
+ "169.254.0.0/16",
70
+ // link-local v4 (cloud metadata endpoints)
71
+ "fe80::/10",
72
+ // link-local v6
73
+ "::1/128"
74
+ // loopback v6
75
+ ];
76
+ var DEFAULT_DANGEROUS_SCHEMES = ["file:", "data:", "javascript:", "vbscript:"];
77
+
78
+ // src/policyLoader.ts
3
79
  var LOCKED_ACTIONABLE_TYPES = /* @__PURE__ */ new Set([
4
80
  "role_violation",
5
81
  "unauthorized_target",
@@ -29,15 +105,88 @@ var VALID_ACTIONS = /* @__PURE__ */ new Set([
29
105
  function fail(message) {
30
106
  throw new Error(`Policy validation error: ${message}`);
31
107
  }
108
+ function checkSaneNumber(value, opts = {}) {
109
+ if (typeof value !== "number" || !Number.isFinite(value)) {
110
+ return `must be a finite number; got ${String(value)}`;
111
+ }
112
+ if (opts.integer && !Number.isInteger(value)) {
113
+ return `must be an integer; got ${value}`;
114
+ }
115
+ if (opts.min !== void 0 && value < opts.min) {
116
+ return `must be >= ${opts.min}; got ${value}`;
117
+ }
118
+ if (opts.max !== void 0 && value > opts.max) {
119
+ return `must be <= ${opts.max}; got ${value}`;
120
+ }
121
+ return null;
122
+ }
123
+ function requireSaneNumber(name, value, opts = {}) {
124
+ const violation = checkSaneNumber(value, opts);
125
+ if (violation) fail(`${name} ${violation}`);
126
+ }
127
+ function levenshtein(a, b) {
128
+ const m = a.length;
129
+ const n = b.length;
130
+ const row = Array.from({ length: n + 1 }, (_, j) => j);
131
+ for (let i = 1; i <= m; i++) {
132
+ let prev = row[0];
133
+ row[0] = i;
134
+ for (let j = 1; j <= n; j++) {
135
+ const tmp = row[j];
136
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
137
+ prev = tmp;
138
+ }
139
+ }
140
+ return row[n];
141
+ }
142
+ function suggestKey(unknown, valid) {
143
+ const lower = unknown.toLowerCase();
144
+ for (const v of valid) {
145
+ if (v.toLowerCase() === lower) return v;
146
+ }
147
+ for (const v of valid) {
148
+ const vl = v.toLowerCase();
149
+ if (lower.includes(vl) || vl.includes(lower)) return v;
150
+ }
151
+ let best = null;
152
+ let bestDist = 3;
153
+ for (const v of valid) {
154
+ const d = levenshtein(lower, v.toLowerCase());
155
+ if (d < bestDist) {
156
+ bestDist = d;
157
+ best = v;
158
+ }
159
+ }
160
+ return best;
161
+ }
162
+ function rejectUnknownKeys(section, obj, allowed) {
163
+ for (const key of Object.keys(obj)) {
164
+ if (!allowed.includes(key)) {
165
+ const suggestion = suggestKey(key, allowed);
166
+ fail(
167
+ `unknown key "${key}" in ${section}.` + (suggestion ? ` Did you mean "${suggestion}"?` : "") + ` Valid keys: ${allowed.join(", ")}`
168
+ );
169
+ }
170
+ }
171
+ }
32
172
  function validatePolicy(data) {
33
173
  if (typeof data !== "object" || data === null) {
34
174
  fail("policy must be a YAML object");
35
175
  }
36
176
  const doc = data;
177
+ rejectUnknownKeys("policy document", doc, [
178
+ "version",
179
+ "agent",
180
+ "policy",
181
+ "enforcement",
182
+ "alerts",
183
+ "repo"
184
+ ]);
37
185
  if (doc.version === void 0) fail("version is required");
38
186
  if (String(doc.version) !== "1.0") fail('version must be "1.0"');
39
187
  if (typeof doc.agent !== "object" || doc.agent === null) fail("agent section is required");
40
188
  const agent = doc.agent;
189
+ rejectUnknownKeys("agent", agent, ["id", "name", "description"]);
41
190
  if (typeof agent.id !== "string" || agent.id.length === 0) fail("agent.id is required");
42
191
  if (typeof agent.name !== "string" || agent.name.length === 0) fail("agent.name is required");
43
192
  if (agent.description !== void 0 && typeof agent.description !== "string") {
@@ -45,10 +194,12 @@ function validatePolicy(data) {
45
194
  }
46
195
  if (typeof doc.policy !== "object" || doc.policy === null) fail("policy section is required");
47
196
  const policy = doc.policy;
197
+ rejectUnknownKeys("policy", policy, ["allow", "forbid", "exceptions", "schedule", "limits"]);
48
198
  if (typeof policy.allow !== "object" || policy.allow === null) {
49
199
  fail("policy.allow section is required");
50
200
  }
51
201
  const allow = policy.allow;
202
+ rejectUnknownKeys("policy.allow", allow, ["actions", "targets", "networkHosts"]);
52
203
  if (!Array.isArray(allow.actions) || allow.actions.length === 0) {
53
204
  fail("policy.allow.actions must be a non-empty array of strings");
54
205
  }
@@ -84,6 +235,7 @@ function validatePolicy(data) {
84
235
  fail("policy.forbid section is required");
85
236
  }
86
237
  const forbid = policy.forbid;
238
+ rejectUnknownKeys("policy.forbid", forbid, ["targets"]);
87
239
  if (!Array.isArray(forbid.targets) || forbid.targets.length === 0) {
88
240
  fail("policy.forbid.targets must be a non-empty array of strings");
89
241
  }
@@ -100,6 +252,14 @@ function validatePolicy(data) {
100
252
  if (typeof ex !== "object" || ex === null) {
101
253
  fail(`${prefix}: must be an object`);
102
254
  }
255
+ rejectUnknownKeys(prefix, ex, [
256
+ "target",
257
+ "allowedActions",
258
+ "requiresTask",
259
+ "requiresApproval",
260
+ "expiresAfter",
261
+ "downgradeKindTo"
262
+ ]);
103
263
  if (typeof ex.target !== "string" || ex.target.length === 0) {
104
264
  fail(`${prefix}: target is required and must be a non-empty string`);
105
265
  }
@@ -144,6 +304,7 @@ function validatePolicy(data) {
144
304
  fail("policy.schedule must be an object");
145
305
  }
146
306
  const schedule = policy.schedule;
307
+ rejectUnknownKeys("policy.schedule", schedule, ["hours", "days"]);
147
308
  if (!Array.isArray(schedule.hours) || schedule.hours.length !== 2) {
148
309
  fail("schedule.hours must be [start, end] with values 0-23");
149
310
  }
@@ -165,35 +326,54 @@ function validatePolicy(data) {
165
326
  }
166
327
  }
167
328
  if (policy.limits !== void 0) {
168
- if (typeof policy.limits !== "object" || policy.limits === null) {
169
- fail("policy.limits must be an object");
170
- }
171
- const limits = policy.limits;
172
- if (limits.maxEventsPerHour !== void 0 && typeof limits.maxEventsPerHour !== "number") {
173
- fail("policy.limits.maxEventsPerHour must be a number");
174
- }
175
- if (limits.maxSessionDuration !== void 0 && typeof limits.maxSessionDuration !== "number") {
176
- fail("policy.limits.maxSessionDuration must be a number");
177
- }
329
+ fail(
330
+ "policy.limits is not supported \u2014 rate/duration limiting (maxEventsPerHour, maxSessionDuration) is not currently enforced by any runtime check. Remove this section. If you need rate limiting, track it as a feature request."
331
+ );
178
332
  }
179
333
  if (doc.enforcement !== void 0) {
180
334
  if (typeof doc.enforcement !== "object" || doc.enforcement === null) {
181
335
  fail("enforcement must be an object");
182
336
  }
183
337
  const enforcement = doc.enforcement;
184
- if (enforcement.restrictAfter !== void 0 && typeof enforcement.restrictAfter !== "number") {
185
- fail("enforcement.restrictAfter must be a number");
186
- }
187
- if (enforcement.quarantineAfter !== void 0 && typeof enforcement.quarantineAfter !== "number") {
188
- fail("enforcement.quarantineAfter must be a number");
338
+ rejectUnknownKeys("enforcement", enforcement, [
339
+ "restrictAfter",
340
+ "quarantineAfter",
341
+ "approvalRequired",
342
+ "minKind",
343
+ "promote",
344
+ "baselineMaturity",
345
+ "unknownTools",
346
+ "allowUnknownTools",
347
+ "onInternalError"
348
+ ]);
349
+ if (enforcement.restrictAfter !== void 0) {
350
+ requireSaneNumber("enforcement.restrictAfter", enforcement.restrictAfter, {
351
+ integer: true,
352
+ min: 1
353
+ });
354
+ }
355
+ if (enforcement.quarantineAfter !== void 0) {
356
+ requireSaneNumber("enforcement.quarantineAfter", enforcement.quarantineAfter, {
357
+ integer: true,
358
+ min: 1
359
+ });
360
+ }
361
+ {
362
+ const effRestrict = enforcement.restrictAfter ?? DEFAULT_RESTRICT_AFTER;
363
+ const effQuarantine = enforcement.quarantineAfter ?? DEFAULT_QUARANTINE_AFTER;
364
+ if (effRestrict >= effQuarantine) {
365
+ fail(
366
+ `enforcement.restrictAfter (${effRestrict}) must be strictly less than enforcement.quarantineAfter (${effQuarantine}) \u2014 otherwise the restricted tier is unreachable and agents jump straight to quarantine. (Unset values default to restrictAfter ${DEFAULT_RESTRICT_AFTER}, quarantineAfter ${DEFAULT_QUARANTINE_AFTER}.)`
367
+ );
368
+ }
189
369
  }
190
370
  if (enforcement.approvalRequired !== void 0 && typeof enforcement.approvalRequired !== "boolean") {
191
371
  fail("enforcement.approvalRequired must be a boolean");
192
372
  }
193
373
  if (enforcement.minKind !== void 0) {
194
- if (typeof enforcement.minKind !== "string" || !VALID_KINDS.has(enforcement.minKind)) {
195
- fail("enforcement.minKind must be one of: informational, actionable");
196
- }
374
+ fail(
375
+ "enforcement.minKind is not supported \u2014 enforcement gates on finding kind 'actionable' and this field was never wired. Use enforcement.promote to promote specific finding types to actionable, or remove this field."
376
+ );
197
377
  }
198
378
  if (enforcement.promote !== void 0) {
199
379
  if (!Array.isArray(enforcement.promote)) {
@@ -220,11 +400,21 @@ function validatePolicy(data) {
220
400
  fail("enforcement.allowUnknownTools must be an array of tool name strings");
221
401
  }
222
402
  }
403
+ if (enforcement.onInternalError !== void 0) {
404
+ if (enforcement.onInternalError !== "fail-open" && enforcement.onInternalError !== "fail-closed") {
405
+ fail('enforcement.onInternalError must be "fail-open" or "fail-closed"');
406
+ }
407
+ }
223
408
  if (enforcement.baselineMaturity !== void 0) {
224
409
  if (typeof enforcement.baselineMaturity !== "object" || enforcement.baselineMaturity === null) {
225
410
  fail("enforcement.baselineMaturity must be an object");
226
411
  }
227
412
  const bm = enforcement.baselineMaturity;
413
+ rejectUnknownKeys("enforcement.baselineMaturity", bm, [
414
+ "minSessions",
415
+ "minDaysObserved",
416
+ "minCategoryDiversity"
417
+ ]);
228
418
  for (const key of ["minSessions", "minDaysObserved", "minCategoryDiversity"]) {
229
419
  if (bm[key] !== void 0) {
230
420
  if (typeof bm[key] !== "number" || !Number.isFinite(bm[key])) {
@@ -242,6 +432,13 @@ function validatePolicy(data) {
242
432
  fail("alerts must be an object");
243
433
  }
244
434
  const alerts = doc.alerts;
435
+ rejectUnknownKeys("alerts", alerts, [
436
+ "channels",
437
+ "webhookUrl",
438
+ "filePath",
439
+ "minSeverity",
440
+ "minKind"
441
+ ]);
245
442
  if (!Array.isArray(alerts.channels) || alerts.channels.length === 0) {
246
443
  fail("alerts.channels must be a non-empty array");
247
444
  }
@@ -252,6 +449,7 @@ function validatePolicy(data) {
252
449
  }
253
450
  } else if (typeof ch === "object" && ch !== null) {
254
451
  const chObj = ch;
452
+ rejectUnknownKeys("alerts.channels[]", chObj, ["type", "minKind"]);
255
453
  if (typeof chObj.type !== "string" || !VALID_CHANNELS.has(chObj.type)) {
256
454
  fail(
257
455
  `alerts.channels contains invalid channel type "${chObj.type}". Valid: console, webhook, file`
@@ -288,6 +486,7 @@ function validatePolicy(data) {
288
486
  fail("repo must be an object");
289
487
  }
290
488
  const repo = doc.repo;
489
+ rejectUnknownKeys("repo", repo, ["root", "scanOnStartup", "mapPath", "overlayPath"]);
291
490
  if (repo.root !== void 0) {
292
491
  if (typeof repo.root !== "string" || repo.root.length === 0) {
293
492
  fail("repo.root must be a non-empty string");
@@ -368,20 +567,12 @@ function policyToRole(policy) {
368
567
  activeDays: policy.policy.schedule.days
369
568
  };
370
569
  }
371
- if (policy.policy.limits) {
372
- if (policy.policy.limits.maxEventsPerHour !== void 0) {
373
- role.maxEventsPerHour = policy.policy.limits.maxEventsPerHour;
374
- }
375
- if (policy.policy.limits.maxSessionDuration !== void 0) {
376
- role.maxSessionDuration = policy.policy.limits.maxSessionDuration;
377
- }
378
- }
379
570
  return role;
380
571
  }
381
572
  function policyToConfig(policy) {
382
573
  const config = {};
383
574
  if (policy.enforcement) {
384
- if (policy.enforcement.restrictAfter !== void 0 || policy.enforcement.quarantineAfter !== void 0 || policy.enforcement.promote !== void 0 || policy.enforcement.baselineMaturity !== void 0 || policy.enforcement.unknownTools !== void 0 || policy.enforcement.allowUnknownTools !== void 0) {
575
+ if (policy.enforcement.restrictAfter !== void 0 || policy.enforcement.quarantineAfter !== void 0 || policy.enforcement.promote !== void 0 || policy.enforcement.baselineMaturity !== void 0 || policy.enforcement.unknownTools !== void 0 || policy.enforcement.allowUnknownTools !== void 0 || policy.enforcement.onInternalError !== void 0) {
385
576
  config.enforcement = {};
386
577
  if (policy.enforcement.restrictAfter !== void 0) {
387
578
  config.enforcement.restrictAfter = policy.enforcement.restrictAfter;
@@ -401,6 +592,9 @@ function policyToConfig(policy) {
401
592
  if (policy.enforcement.allowUnknownTools !== void 0) {
402
593
  config.enforcement.allowUnknownTools = policy.enforcement.allowUnknownTools;
403
594
  }
595
+ if (policy.enforcement.onInternalError !== void 0) {
596
+ config.enforcement.onInternalError = policy.enforcement.onInternalError;
597
+ }
404
598
  }
405
599
  }
406
600
  if (policy.alerts) {
@@ -435,10 +629,19 @@ function policyToConfig(policy) {
435
629
  }
436
630
 
437
631
  export {
632
+ DEFAULT_FORBIDDEN_PATTERNS,
633
+ withPolicyReadExceptions,
634
+ DEFAULT_MEDIUM_DISPOSITION,
635
+ DEFAULT_RESTRICT_AFTER,
636
+ DEFAULT_QUARANTINE_AFTER,
637
+ DEFAULT_NETWORK_DENYLIST_CIDRS,
638
+ DEFAULT_DANGEROUS_SCHEMES,
438
639
  LOCKED_ACTIONABLE_TYPES,
640
+ checkSaneNumber,
641
+ suggestKey,
439
642
  loadPolicy,
440
643
  loadPolicyFromString,
441
644
  policyToRole,
442
645
  policyToConfig
443
646
  };
444
- //# sourceMappingURL=chunk-WLIDSTS4.js.map
647
+ //# sourceMappingURL=chunk-SKE74CYZ.js.map
@@ -4,14 +4,14 @@ import {
4
4
  } from "./chunk-LATQNIRW.js";
5
5
  import {
6
6
  discoverPolicy
7
- } from "./chunk-FMZWHT4M.js";
7
+ } from "./chunk-B6S2PBS4.js";
8
8
  import {
9
9
  FORBIDDEN_BASENAMES
10
- } from "./chunk-JTR2E7RD.js";
10
+ } from "./chunk-M5EEVMLU.js";
11
11
  import {
12
12
  loadPolicy,
13
13
  loadPolicyFromString
14
- } from "./chunk-WLIDSTS4.js";
14
+ } from "./chunk-SKE74CYZ.js";
15
15
 
16
16
  // src/setup/initClaudeCode.ts
17
17
  import { access, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
@@ -94,19 +94,45 @@ const FLOOR_HIGH = ["Bash", "Write", "Edit", "WebFetch", "NotebookEdit", "Task",
94
94
  // The marker default is [] \u2014 an un-substituted script stays strictest.
95
95
  const ALLOW_UNKNOWN_TOOLS = /* __ALLOW_UNKNOWN_TOOLS__ */ [];
96
96
 
97
- // Tier config uses flat format: { high, low, mcpDefault, unknownDefault } (plan v3.1 spec'd nested but simplified during 5a)
97
+ // Tier config uses flat format: { high, low, mcpDefault } (plan v3.1 spec'd nested but simplified during 5a)
98
+ // Validated field-by-field against safe defaults (enforcement-config sanity class):
99
+ // the hook must never block the cc session on a config error, so invalid values
100
+ // fall back to the fail-closed default AND are logged \u2014 never silently honored.
101
+ // In particular mcpDefault was compared with === "high", so any typo silently
102
+ // demoted ALL MCP tools to low tier gateway-down (allow-unlogged, the F-8 hole).
98
103
  function loadTiers() {
104
+ const DEFAULT_TIERS = {
105
+ high: ["Bash", "Write", "Edit", "WebFetch", "NotebookEdit", "Task", "Skill"],
106
+ low: ["Read", "Glob", "Grep", "WebSearch"],
107
+ mcpDefault: "high",
108
+ };
109
+ let parsed;
99
110
  try {
100
- return JSON.parse(readFileSync(TIERS_PATH, "utf-8"));
111
+ parsed = JSON.parse(readFileSync(TIERS_PATH, "utf-8"));
101
112
  } catch {
102
- // Default tiers if config is missing
103
- return {
104
- high: ["Bash", "Write", "Edit", "WebFetch", "NotebookEdit", "Task", "Skill"],
105
- low: ["Read", "Glob", "Grep", "WebSearch"],
106
- mcpDefault: "high",
107
- unknownDefault: "high",
108
- };
113
+ return DEFAULT_TIERS; // missing/unreadable config \u2014 safe defaults
109
114
  }
115
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
116
+ logFallback({ event: "tiers-config-invalid", reason: "top level must be an object", treatedAs: "defaults" });
117
+ return DEFAULT_TIERS;
118
+ }
119
+ const tiers = { ...DEFAULT_TIERS };
120
+ if (Array.isArray(parsed.high)) tiers.high = parsed.high.filter((t) => typeof t === "string");
121
+ else if (parsed.high !== undefined) logFallback({ event: "tiers-config-invalid", field: "high", reason: "must be an array of tool names", treatedAs: "defaults" });
122
+ if (Array.isArray(parsed.low)) tiers.low = parsed.low.filter((t) => typeof t === "string");
123
+ else if (parsed.low !== undefined) logFallback({ event: "tiers-config-invalid", field: "low", reason: "must be an array of tool names", treatedAs: "defaults" });
124
+ if (parsed.mcpDefault === "high" || parsed.mcpDefault === "low") tiers.mcpDefault = parsed.mcpDefault;
125
+ else if (parsed.mcpDefault !== undefined) logFallback({ event: "tiers-config-invalid", field: "mcpDefault", value: String(parsed.mcpDefault), reason: 'must be "high" or "low"', treatedAs: "high" });
126
+ // unknownDefault is NOT supported: unknown tools are ALWAYS high-tier
127
+ // gateway-down (see isHighSensitivity). It used to be written by init and
128
+ // read by nothing \u2014 a silent no-op knob. Warn so the operator knows.
129
+ if (parsed.unknownDefault !== undefined) logFallback({ event: "tiers-config-ignored", field: "unknownDefault", reason: "unsupported \u2014 unknown tools are always high-tier gateway-down" });
130
+ for (const key of Object.keys(parsed)) {
131
+ if (!["high", "low", "mcpDefault", "unknownDefault"].includes(key)) {
132
+ logFallback({ event: "tiers-config-unknown-key", key: key });
133
+ }
134
+ }
135
+ return tiers;
110
136
  }
111
137
 
112
138
  function isHighSensitivity(toolName, tiers) {
@@ -460,8 +486,7 @@ enforcement:
460
486
  var FAIL_CLOSED_TIERS = {
461
487
  high: ["Bash", "Write", "Edit", "WebFetch", "NotebookEdit", "Task", "Skill"],
462
488
  low: ["Read", "Glob", "Grep", "WebSearch"],
463
- mcpDefault: "high",
464
- unknownDefault: "high"
489
+ mcpDefault: "high"
465
490
  };
466
491
  async function runInitClaudeCode(options) {
467
492
  const force = options.force ?? false;
@@ -712,16 +737,30 @@ async function runSessionStart(options) {
712
737
  }
713
738
  try {
714
739
  if (lock.pid) await terminateDaemon(lock.pid);
715
- const pid2 = spawnDaemon(gatewayEntry, policyPath, port, home);
740
+ const pid = spawnDaemon(gatewayEntry, policyPath, port, home);
716
741
  await waitForGatewayReady(port);
717
- return { action: "relaunched", pid: pid2, policyPath, relaunchReason: reason };
742
+ return { action: "relaunched", pid, policyPath, relaunchReason: reason };
718
743
  } finally {
719
744
  releaseRelaunchLock(home);
720
745
  }
721
746
  }
722
- const pid = spawnDaemon(gatewayEntry, policyPath, port, home);
723
- await waitForGatewayReady(port);
724
- return { action: "spawned", pid, policyPath, relaunchReason: null };
747
+ if (!acquireRelaunchLock(home)) {
748
+ await waitForGatewayReady(port);
749
+ const peerPid = acquireGatewayLock(home).pid ?? void 0;
750
+ return { action: "reused", pid: peerPid, policyPath, relaunchReason: null };
751
+ }
752
+ try {
753
+ const relock = acquireGatewayLock(home);
754
+ if (relock.reused) {
755
+ await waitForGatewayReady(port);
756
+ return { action: "reused", pid: relock.pid, policyPath, relaunchReason: null };
757
+ }
758
+ const pid = spawnDaemon(gatewayEntry, policyPath, port, home);
759
+ await waitForGatewayReady(port);
760
+ return { action: "spawned", pid, policyPath, relaunchReason: null };
761
+ } finally {
762
+ releaseRelaunchLock(home);
763
+ }
725
764
  }
726
765
 
727
766
  export {
@@ -729,4 +768,4 @@ export {
729
768
  computeBuildId,
730
769
  runSessionStart
731
770
  };
732
- //# sourceMappingURL=chunk-2TJ5Z53T.js.map
771
+ //# sourceMappingURL=chunk-UVNRPML4.js.map