@tuent/sentinel 0.1.3 → 0.1.4

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,53 @@ 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
+ ]);
348
+ if (enforcement.restrictAfter !== void 0) {
349
+ requireSaneNumber("enforcement.restrictAfter", enforcement.restrictAfter, {
350
+ integer: true,
351
+ min: 1
352
+ });
353
+ }
354
+ if (enforcement.quarantineAfter !== void 0) {
355
+ requireSaneNumber("enforcement.quarantineAfter", enforcement.quarantineAfter, {
356
+ integer: true,
357
+ min: 1
358
+ });
359
+ }
360
+ {
361
+ const effRestrict = enforcement.restrictAfter ?? DEFAULT_RESTRICT_AFTER;
362
+ const effQuarantine = enforcement.quarantineAfter ?? DEFAULT_QUARANTINE_AFTER;
363
+ if (effRestrict >= effQuarantine) {
364
+ fail(
365
+ `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}.)`
366
+ );
367
+ }
189
368
  }
190
369
  if (enforcement.approvalRequired !== void 0 && typeof enforcement.approvalRequired !== "boolean") {
191
370
  fail("enforcement.approvalRequired must be a boolean");
192
371
  }
193
372
  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
- }
373
+ fail(
374
+ "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."
375
+ );
197
376
  }
198
377
  if (enforcement.promote !== void 0) {
199
378
  if (!Array.isArray(enforcement.promote)) {
@@ -225,6 +404,11 @@ function validatePolicy(data) {
225
404
  fail("enforcement.baselineMaturity must be an object");
226
405
  }
227
406
  const bm = enforcement.baselineMaturity;
407
+ rejectUnknownKeys("enforcement.baselineMaturity", bm, [
408
+ "minSessions",
409
+ "minDaysObserved",
410
+ "minCategoryDiversity"
411
+ ]);
228
412
  for (const key of ["minSessions", "minDaysObserved", "minCategoryDiversity"]) {
229
413
  if (bm[key] !== void 0) {
230
414
  if (typeof bm[key] !== "number" || !Number.isFinite(bm[key])) {
@@ -242,6 +426,13 @@ function validatePolicy(data) {
242
426
  fail("alerts must be an object");
243
427
  }
244
428
  const alerts = doc.alerts;
429
+ rejectUnknownKeys("alerts", alerts, [
430
+ "channels",
431
+ "webhookUrl",
432
+ "filePath",
433
+ "minSeverity",
434
+ "minKind"
435
+ ]);
245
436
  if (!Array.isArray(alerts.channels) || alerts.channels.length === 0) {
246
437
  fail("alerts.channels must be a non-empty array");
247
438
  }
@@ -252,6 +443,7 @@ function validatePolicy(data) {
252
443
  }
253
444
  } else if (typeof ch === "object" && ch !== null) {
254
445
  const chObj = ch;
446
+ rejectUnknownKeys("alerts.channels[]", chObj, ["type", "minKind"]);
255
447
  if (typeof chObj.type !== "string" || !VALID_CHANNELS.has(chObj.type)) {
256
448
  fail(
257
449
  `alerts.channels contains invalid channel type "${chObj.type}". Valid: console, webhook, file`
@@ -288,6 +480,7 @@ function validatePolicy(data) {
288
480
  fail("repo must be an object");
289
481
  }
290
482
  const repo = doc.repo;
483
+ rejectUnknownKeys("repo", repo, ["root", "scanOnStartup", "mapPath", "overlayPath"]);
291
484
  if (repo.root !== void 0) {
292
485
  if (typeof repo.root !== "string" || repo.root.length === 0) {
293
486
  fail("repo.root must be a non-empty string");
@@ -368,14 +561,6 @@ function policyToRole(policy) {
368
561
  activeDays: policy.policy.schedule.days
369
562
  };
370
563
  }
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
564
  return role;
380
565
  }
381
566
  function policyToConfig(policy) {
@@ -435,10 +620,19 @@ function policyToConfig(policy) {
435
620
  }
436
621
 
437
622
  export {
623
+ DEFAULT_FORBIDDEN_PATTERNS,
624
+ withPolicyReadExceptions,
625
+ DEFAULT_MEDIUM_DISPOSITION,
626
+ DEFAULT_RESTRICT_AFTER,
627
+ DEFAULT_QUARANTINE_AFTER,
628
+ DEFAULT_NETWORK_DENYLIST_CIDRS,
629
+ DEFAULT_DANGEROUS_SCHEMES,
438
630
  LOCKED_ACTIONABLE_TYPES,
631
+ checkSaneNumber,
632
+ suggestKey,
439
633
  loadPolicy,
440
634
  loadPolicyFromString,
441
635
  policyToRole,
442
636
  policyToConfig
443
637
  };
444
- //# sourceMappingURL=chunk-WLIDSTS4.js.map
638
+ //# sourceMappingURL=chunk-KWZ7JKKO.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-FIEIGBYL.js";
11
11
  import {
12
12
  loadPolicy,
13
13
  loadPolicyFromString
14
- } from "./chunk-WLIDSTS4.js";
14
+ } from "./chunk-KWZ7JKKO.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-LTBVWF5H.js.map