@tuent/sentinel 0.1.0 → 0.1.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.
@@ -1,11 +1,20 @@
1
+ import {
2
+ deriveAgentId
3
+ } from "./chunk-B5QKJHSV.js";
1
4
  import {
2
5
  discoverPolicy
3
6
  } from "./chunk-FMZWHT4M.js";
4
7
  import {
5
8
  DEFAULT_FORBIDDEN_PATTERNS,
9
+ FORBIDDEN_BASENAMES,
10
+ isPositionallySafeMention,
6
11
  matchGlobInsensitive,
7
- normalizeForbiddenPattern
8
- } from "./chunk-6MHWJATS.js";
12
+ normalizeForbiddenPattern,
13
+ scanBashCommand,
14
+ scanContentForForbiddenBasenames,
15
+ scanGlobPattern,
16
+ tokenizePaths
17
+ } from "./chunk-QHE56MEO.js";
9
18
  import {
10
19
  loadPolicy,
11
20
  policyToConfig,
@@ -15,34 +24,6 @@ import {
15
24
  // src/gateway/workspaceRouter.ts
16
25
  import { resolve, dirname } from "path";
17
26
 
18
- // src/workspaceIdentity.ts
19
- var AGENT_PREFIX = "claude-code";
20
- function fnv1a32Hex(s) {
21
- let h = 2166136261;
22
- for (let i = 0; i < s.length; i++) {
23
- h ^= s.charCodeAt(i);
24
- h = Math.imul(h, 16777619);
25
- }
26
- return (h >>> 0).toString(16).padStart(8, "0");
27
- }
28
- function lastSegment(path) {
29
- const parts = path.split("/").filter(Boolean);
30
- return parts.length > 0 ? parts[parts.length - 1] : "";
31
- }
32
- function slugify(s) {
33
- return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
34
- }
35
- function normalizeRoot(root) {
36
- if (root === "" || root === "/") return root;
37
- return root.replace(/\/+$/, "") || "/";
38
- }
39
- function deriveAgentId(workspaceRoot) {
40
- const root = normalizeRoot(workspaceRoot);
41
- const slug = slugify(lastSegment(root)) || "root";
42
- const hash = fnv1a32Hex(root);
43
- return `${AGENT_PREFIX}@${slug}-${hash}`;
44
- }
45
-
46
27
  // src/mergeRoles.ts
47
28
  function isWithinActiveHours(hour, range) {
48
29
  const [startHour, endHour] = range;
@@ -363,393 +344,6 @@ var TranslatorRegistry = class {
363
344
  }
364
345
  };
365
346
 
366
- // src/gateway/bashScanner.ts
367
- import { parse as shellParse } from "shell-quote";
368
- import { realpathSync } from "fs";
369
- import { dirname as dirname2, join, basename, normalize } from "path";
370
- var BRACE_PATTERN_RE = /\{[^}]*,[^}]*\}/;
371
- var MAX_BRACE_EXPANSION = 64;
372
- function fnmatchBasename(pattern, candidate) {
373
- if (pattern.length !== candidate.length) return false;
374
- for (let i = 0; i < pattern.length; i++) {
375
- const p = pattern[i].toLowerCase();
376
- const c = candidate[i].toLowerCase();
377
- if (p === "?") continue;
378
- if (p !== c) return false;
379
- }
380
- return true;
381
- }
382
- function bracketTokenMatchesForbidden(token, forbiddenBasenames) {
383
- const literals = [];
384
- let current = "";
385
- let inBracket = false;
386
- for (let i = 0; i < token.length; i++) {
387
- if (token[i] === "[" && !inBracket) {
388
- if (current) literals.push(current);
389
- current = "";
390
- inBracket = true;
391
- } else if (token[i] === "]" && inBracket) {
392
- inBracket = false;
393
- } else if (!inBracket) {
394
- current += token[i];
395
- }
396
- }
397
- if (current) literals.push(current);
398
- for (const forbidden of forbiddenBasenames) {
399
- const fl = forbidden.toLowerCase();
400
- for (const lit of literals) {
401
- if (lit.length === 0) continue;
402
- if (fl.includes(lit.toLowerCase())) return forbidden;
403
- }
404
- }
405
- return null;
406
- }
407
- function resolveBraceExpansion(token) {
408
- const match = token.match(/^(.*?)\{([^}]*,[^}]*)\}(.*)$/);
409
- if (!match) return null;
410
- const [, prefix, alternatives, suffix] = match;
411
- const parts = alternatives.split(",");
412
- if (parts.length > MAX_BRACE_EXPANSION) return null;
413
- return parts.map((p) => prefix + p + suffix);
414
- }
415
- function wildcardDispatch(token, forbiddenBasenames, metadataField) {
416
- const result = {
417
- resolvedBasenames: [],
418
- unparseable: false,
419
- metadata: {}
420
- };
421
- if (token === "*" || token === "**" || token === "?") {
422
- return result;
423
- }
424
- if (BRACE_PATTERN_RE.test(token)) {
425
- const expanded = resolveBraceExpansion(token);
426
- if (expanded === null) {
427
- result.unparseable = true;
428
- return result;
429
- }
430
- for (const alt of expanded) {
431
- const hasWildcard = /[?*[]/.test(alt);
432
- if (hasWildcard) {
433
- const sub = wildcardDispatch(alt, forbiddenBasenames, metadataField);
434
- if (sub.resolvedBasenames.length > 0) {
435
- result.resolvedBasenames.push(...sub.resolvedBasenames);
436
- result.metadata["resolvedFromBrace"] = token;
437
- Object.assign(result.metadata, sub.metadata);
438
- }
439
- if (sub.unparseable) result.unparseable = true;
440
- } else {
441
- const altLower = alt.toLowerCase();
442
- for (const forbidden of forbiddenBasenames) {
443
- if (altLower === forbidden.toLowerCase()) {
444
- result.resolvedBasenames.push(forbidden);
445
- result.metadata["resolvedFromBrace"] = token;
446
- break;
447
- }
448
- }
449
- }
450
- }
451
- return result;
452
- }
453
- const hasStar = token.includes("*");
454
- const hasQuestion = token.includes("?");
455
- const hasBracket = token.includes("[");
456
- if (hasBracket) {
457
- const matched = bracketTokenMatchesForbidden(token, forbiddenBasenames);
458
- if (matched) {
459
- result.resolvedBasenames.push(matched);
460
- result.metadata["resolvedFromBracket"] = token;
461
- } else {
462
- result.unparseable = true;
463
- }
464
- return result;
465
- }
466
- if (hasStar && !hasQuestion) {
467
- const matched = starLiteralSubstringCheck(token, forbiddenBasenames);
468
- if (matched) {
469
- result.resolvedBasenames.push(matched);
470
- result.metadata[metadataField] = token;
471
- }
472
- return result;
473
- }
474
- if (hasQuestion && !hasStar) {
475
- for (const forbidden of forbiddenBasenames) {
476
- if (fnmatchBasename(token, forbidden)) {
477
- result.resolvedBasenames.push(forbidden);
478
- result.metadata[metadataField] = token;
479
- break;
480
- }
481
- }
482
- return result;
483
- }
484
- if (hasStar && hasQuestion) {
485
- const starMatch = starLiteralSubstringCheck(token, forbiddenBasenames);
486
- if (starMatch) {
487
- result.resolvedBasenames.push(starMatch);
488
- result.metadata[metadataField] = token;
489
- return result;
490
- }
491
- const segments = token.split("*").filter((s) => s.includes("?"));
492
- for (const seg of segments) {
493
- for (const forbidden of forbiddenBasenames) {
494
- if (fnmatchBasename(seg, forbidden)) {
495
- result.resolvedBasenames.push(forbidden);
496
- result.metadata[metadataField] = token;
497
- return result;
498
- }
499
- }
500
- }
501
- return result;
502
- }
503
- return result;
504
- }
505
- function starLiteralSubstringCheck(token, forbiddenBasenames) {
506
- const literals = token.split("*").filter((s) => s.length > 0);
507
- for (const forbidden of forbiddenBasenames) {
508
- const fl = forbidden.toLowerCase();
509
- for (const lit of literals) {
510
- if (fl.includes(lit.toLowerCase())) return forbidden;
511
- }
512
- }
513
- return null;
514
- }
515
- function shouldDispatchWildcard(token) {
516
- const hasMetachar = /[?*[{]/.test(token);
517
- if (!hasMetachar) return false;
518
- if (isPathShaped(token)) return true;
519
- if (token.includes("[")) return true;
520
- if (BRACE_PATTERN_RE.test(token)) return true;
521
- return false;
522
- }
523
- var SENSITIVE_BASENAME_RE = /(?:\.env|\.ssh|secrets|credentials|id_rsa|id_dsa|id_ecdsa|id_ed25519|\.pem|\.key)/i;
524
- var DANGEROUS_COMMAND_TOKENS = /* @__PURE__ */ new Set(["eval"]);
525
- var COMMAND_SUBSTITUTION_RE = /\$\(|`/;
526
- var DANGEROUS_RAW_RE = /<<<|<\(|>\(/;
527
- function isVarMarker(token) {
528
- return typeof token === "object" && token !== null && "__sentinel_var" in token && typeof token.__sentinel_var === "string";
529
- }
530
- function tokenizePaths(command) {
531
- const result = {
532
- paths: [],
533
- unparseable: false,
534
- hasDangerousConstruct: false
535
- };
536
- if (DANGEROUS_RAW_RE.test(command)) {
537
- result.hasDangerousConstruct = true;
538
- }
539
- if (COMMAND_SUBSTITUTION_RE.test(command)) {
540
- result.hasDangerousConstruct = true;
541
- }
542
- let tokens;
543
- try {
544
- tokens = shellParse(command, (key) => ({ __sentinel_var: key }));
545
- } catch {
546
- result.unparseable = true;
547
- return result;
548
- }
549
- if (!Array.isArray(tokens)) {
550
- result.unparseable = true;
551
- return result;
552
- }
553
- let prevToken = null;
554
- for (let i = 0; i < tokens.length; i++) {
555
- const token = tokens[i];
556
- if (isVarMarker(token)) {
557
- const nextToken = tokens[i + 1];
558
- const nextIsPathRelevant = nextToken === void 0 || // end of tokens — var is complete argument
559
- typeof nextToken === "object" && nextToken !== null && "op" in nextToken || // followed by operator — var is complete argument
560
- typeof nextToken === "string" && isPathShaped(nextToken);
561
- const prevIsPathRelevant = prevToken !== null && isPathShaped(prevToken);
562
- if (nextIsPathRelevant || prevIsPathRelevant) {
563
- result.unparseable = true;
564
- }
565
- prevToken = null;
566
- continue;
567
- }
568
- if (typeof token === "object" && token !== null) {
569
- if ("pattern" in token) {
570
- const globPattern = token.pattern;
571
- const lastSlash = globPattern.lastIndexOf("/");
572
- const dispatchTarget = lastSlash >= 0 ? globPattern.slice(lastSlash + 1) : globPattern;
573
- const dispatch = wildcardDispatch(dispatchTarget, FORBIDDEN_BASENAMES, "resolvedFromGlob");
574
- if (dispatch.resolvedBasenames.length > 0) {
575
- for (const resolved of dispatch.resolvedBasenames) {
576
- result.paths.push(resolved);
577
- }
578
- }
579
- if (dispatch.unparseable) {
580
- result.unparseable = true;
581
- }
582
- if (SENSITIVE_BASENAME_RE.test(globPattern)) {
583
- result.unparseable = true;
584
- }
585
- prevToken = null;
586
- continue;
587
- }
588
- if ("op" in token) {
589
- if (token.op === "<(") {
590
- result.hasDangerousConstruct = true;
591
- }
592
- prevToken = null;
593
- continue;
594
- }
595
- prevToken = null;
596
- continue;
597
- }
598
- if (typeof token !== "string") {
599
- prevToken = null;
600
- continue;
601
- }
602
- if (DANGEROUS_COMMAND_TOKENS.has(token.toLowerCase())) {
603
- result.hasDangerousConstruct = true;
604
- }
605
- if ((prevToken === "sh" || prevToken === "bash" || prevToken === "/bin/sh" || prevToken === "/bin/bash") && token === "-c") {
606
- result.hasDangerousConstruct = true;
607
- }
608
- if (shouldDispatchWildcard(token)) {
609
- const metaField = "resolvedFromQuotedGlob";
610
- const dispatch = wildcardDispatch(token, FORBIDDEN_BASENAMES, metaField);
611
- if (dispatch.resolvedBasenames.length > 0) {
612
- for (const resolved of dispatch.resolvedBasenames) {
613
- result.paths.push(resolved);
614
- }
615
- }
616
- if (dispatch.unparseable) {
617
- result.unparseable = true;
618
- }
619
- } else if (isPathShaped(token)) {
620
- const resolved = resolvePathToken(token);
621
- result.paths.push(resolved);
622
- }
623
- prevToken = token;
624
- }
625
- return result;
626
- }
627
- function isPathShaped(token) {
628
- if (token.includes("/")) return true;
629
- if (token.startsWith(".")) return true;
630
- if (SENSITIVE_BASENAME_RE.test(token)) return true;
631
- return false;
632
- }
633
- function resolvePathToken(token) {
634
- const normalized = normalize(token);
635
- try {
636
- return realpathSync(normalized);
637
- } catch (err) {
638
- const code = err.code;
639
- if (code === "ENOENT") {
640
- return resolveNonexistentPathToken(normalized);
641
- }
642
- return normalized;
643
- }
644
- }
645
- function resolveNonexistentPathToken(normalizedPath) {
646
- let current = normalizedPath;
647
- let suffix = "";
648
- for (let i = 0; i < 50; i++) {
649
- const parent = dirname2(current);
650
- if (parent === current) {
651
- return normalizedPath;
652
- }
653
- if (parent === ".") {
654
- return normalizedPath;
655
- }
656
- suffix = suffix ? join(basename(current), suffix) : basename(current);
657
- current = parent;
658
- try {
659
- const resolved = realpathSync(current);
660
- if (resolved !== current) {
661
- return join(resolved, suffix);
662
- }
663
- return join(resolved, suffix);
664
- } catch {
665
- continue;
666
- }
667
- }
668
- return normalizedPath;
669
- }
670
- var FORBIDDEN_BASENAMES = [
671
- ".env",
672
- ".ssh",
673
- ".aws",
674
- "secrets",
675
- "credentials",
676
- "id_rsa",
677
- "id_dsa",
678
- "id_ecdsa",
679
- "id_ed25519",
680
- ".pem",
681
- ".key"
682
- ];
683
- function scanBashCommand(command, forbiddenBasenames) {
684
- const basenames = forbiddenBasenames ?? FORBIDDEN_BASENAMES;
685
- const hits = [];
686
- for (const basename2 of basenames) {
687
- const pattern = buildPattern(basename2);
688
- if (pattern.test(command)) {
689
- hits.push(basename2);
690
- }
691
- }
692
- return { matched: hits.length > 0, hits };
693
- }
694
- function buildPattern(basename2) {
695
- const escaped = escapeRegex(basename2);
696
- if (basename2.startsWith(".") && basename2.length <= 4 && !isAlphaAfterDot(basename2)) {
697
- return new RegExp(`\\w${escaped}(?=$|[\\s;&|<>()'"=\\/])`, "i");
698
- }
699
- if (basename2.startsWith(".")) {
700
- return new RegExp(`(?:^|[\\s;&|<>()\\/'"=])${escaped}(?=$|[\\s;&|<>()\\/'"=.])`, "i");
701
- }
702
- return new RegExp(`\\b${escaped}\\b`, "i");
703
- }
704
- function scanContentForForbiddenBasenames(content, forbiddenBasenames) {
705
- const hits = [];
706
- for (const basename2 of forbiddenBasenames) {
707
- const pattern = buildContentPattern(basename2);
708
- if (pattern.test(content)) {
709
- hits.push(basename2);
710
- }
711
- }
712
- return { matched: hits.length > 0, hits };
713
- }
714
- function buildContentPattern(basename2) {
715
- const escaped = escapeRegex(basename2);
716
- if (basename2.startsWith(".") && basename2.length <= 4 && !isAlphaAfterDot(basename2)) {
717
- return new RegExp(`\\w${escaped}(?=$|[\\s;&|<>()'"=\\/])`, "i");
718
- }
719
- if (basename2.startsWith(".")) {
720
- return new RegExp(`(?:^|[\\s;&|<>()\\/'"=])${escaped}(?=$|[\\s;&|<>()\\/'"=.])`, "i");
721
- }
722
- return new RegExp(`(?<=[/\\\\]\\.?)${escaped}\\b`, "i");
723
- }
724
- function isAlphaAfterDot(s) {
725
- return /^\.[a-zA-Z]+$/.test(s);
726
- }
727
- function escapeRegex(s) {
728
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
729
- }
730
- function scanGlobPattern(pattern, forbiddenBasenames) {
731
- const basenames = forbiddenBasenames ?? FORBIDDEN_BASENAMES;
732
- const hits = [];
733
- for (const basename2 of basenames) {
734
- const re = buildGlobContextPattern(basename2);
735
- if (re.test(pattern)) {
736
- hits.push(basename2);
737
- }
738
- }
739
- return { matched: hits.length > 0, hits };
740
- }
741
- function buildGlobContextPattern(basename2) {
742
- const escaped = escapeRegex(basename2);
743
- const GLOB_DELIM = String.raw`\s;&|<>()\\/'"=.*?{}\[\]`;
744
- if (basename2.startsWith(".") && basename2.length <= 4 && !isAlphaAfterDot(basename2)) {
745
- return new RegExp(`[\\w*]${escaped}(?=$|[${GLOB_DELIM}])`, "i");
746
- }
747
- if (basename2.startsWith(".")) {
748
- return new RegExp(`(?:^|[${GLOB_DELIM}])${escaped}(?=$|[${GLOB_DELIM}])`, "i");
749
- }
750
- return new RegExp(`\\b${escaped}\\b`, "i");
751
- }
752
-
753
347
  // src/gateway/runtimeConstructionResolvers.ts
754
348
  var MAX_RECURSION_DEPTH = 3;
755
349
  var INTERPRETER_RE = /\b(?:python[23]?|node|ruby|perl|php)\s+(?:-[cer])\s+(.+)/s;
@@ -1397,9 +991,23 @@ function buildModifiedGrepInput(originalInput, exclusions) {
1397
991
  }
1398
992
 
1399
993
  // src/gateway/server.ts
994
+ import { timingSafeEqual } from "crypto";
1400
995
  var DEFAULT_PORT = 7847;
1401
996
  var MAX_BODY_SIZE = 1024 * 1024;
1402
997
  var GATEWAY_VERSION = "0.1.0";
998
+ function isLoopbackAddress(addr) {
999
+ if (!addr) return false;
1000
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1" || addr.startsWith("127.");
1001
+ }
1002
+ function constantTimeEqual(a, b) {
1003
+ const ab = Buffer.from(a, "utf-8");
1004
+ const bb = Buffer.from(b, "utf-8");
1005
+ if (ab.length !== bb.length) {
1006
+ timingSafeEqual(bb, bb);
1007
+ return false;
1008
+ }
1009
+ return timingSafeEqual(ab, bb);
1010
+ }
1403
1011
  function parseIntentLine(prompt) {
1404
1012
  if (typeof prompt !== "string") return null;
1405
1013
  const firstNonEmpty = prompt.split("\n").find((line) => line.trim().length > 0);
@@ -1440,6 +1048,7 @@ var SentinelGateway = class {
1440
1048
  workspaceIsolation;
1441
1049
  operatorCeiling;
1442
1050
  home;
1051
+ releaseToken;
1443
1052
  server = null;
1444
1053
  running = false;
1445
1054
  signalHandlersInstalled = false;
@@ -1453,6 +1062,7 @@ var SentinelGateway = class {
1453
1062
  this.workspaceIsolation = options.workspaceIsolation ?? process.env.SENTINEL_WORKSPACE_ISOLATION === "1";
1454
1063
  this.operatorCeiling = options.operatorCeiling ?? null;
1455
1064
  this.home = options.home ?? "";
1065
+ this.releaseToken = options.releaseToken ?? null;
1456
1066
  const internal = options;
1457
1067
  if (internal.registry) {
1458
1068
  this.registry = internal.registry;
@@ -1580,6 +1190,10 @@ var SentinelGateway = class {
1580
1190
  return;
1581
1191
  }
1582
1192
  }
1193
+ if (method === "POST" && url === "/api/sentinel/release") {
1194
+ this.handleReleaseRoute(req, res);
1195
+ return;
1196
+ }
1583
1197
  if (method === "POST") {
1584
1198
  const match = url.match(
1585
1199
  /^\/api\/sentinel\/(pre-tool-use|post-tool-use|session-end|user-prompt-submit)\/(.+)$/
@@ -1606,6 +1220,58 @@ var SentinelGateway = class {
1606
1220
  }
1607
1221
  this.sendJson(res, 404, { error: "not found" });
1608
1222
  }
1223
+ /**
1224
+ * Operator release route (Sprint 0.1.1). Loopback-only + token-gated. On a
1225
+ * valid request, calls sentinel.release() on the LIVE instance — flipping the
1226
+ * in-memory mode, writing mode.json, and logging the signed mode_change anchor
1227
+ * in-process (single writer). Never re-reads mode.json or trusts file content.
1228
+ */
1229
+ handleReleaseRoute(req, res) {
1230
+ const remote = req.socket?.remoteAddress;
1231
+ if (!isLoopbackAddress(remote)) {
1232
+ console.warn(`[SENTINEL GATEWAY] /release refused non-loopback origin: ${remote ?? "?"}`);
1233
+ this.sendJson(res, 403, { ok: false, error: "release is loopback-only" });
1234
+ return;
1235
+ }
1236
+ if (!this.releaseToken) {
1237
+ this.sendJson(res, 503, { ok: false, error: "release endpoint not configured" });
1238
+ return;
1239
+ }
1240
+ const provided = req.headers["x-sentinel-token"];
1241
+ const token = typeof provided === "string" ? provided : "";
1242
+ if (!constantTimeEqual(token, this.releaseToken)) {
1243
+ console.warn(`[SENTINEL GATEWAY] /release rejected: invalid token from ${remote}`);
1244
+ this.sendJson(res, 401, { ok: false, error: "invalid or missing token" });
1245
+ return;
1246
+ }
1247
+ this.readBody(req, res, (body) => {
1248
+ let payload;
1249
+ try {
1250
+ payload = JSON.parse(body);
1251
+ } catch {
1252
+ this.sendJson(res, 400, { ok: false, error: "invalid JSON" });
1253
+ return;
1254
+ }
1255
+ const p = payload ?? {};
1256
+ const agentId = typeof p.agentId === "string" ? p.agentId : "";
1257
+ if (!agentId) {
1258
+ this.sendJson(res, 400, { ok: false, error: "agentId required" });
1259
+ return;
1260
+ }
1261
+ const reason = typeof p.reason === "string" ? p.reason : "operator release (live)";
1262
+ const previousMode = this.sentinel.getMode(agentId);
1263
+ this.sentinel.release(agentId, reason).then(() => {
1264
+ this.sendJson(res, 200, {
1265
+ ok: true,
1266
+ agentId,
1267
+ previousMode,
1268
+ mode: this.sentinel.getMode(agentId)
1269
+ });
1270
+ }).catch((err) => {
1271
+ this.sendJson(res, 500, { ok: false, error: String(err.message) });
1272
+ });
1273
+ });
1274
+ }
1609
1275
  // -------------------------------------------------------------------------
1610
1276
  // Endpoint handlers
1611
1277
  // -------------------------------------------------------------------------
@@ -1740,7 +1406,13 @@ var SentinelGateway = class {
1740
1406
  routingId = routed.agentId;
1741
1407
  event.agentId = routingId;
1742
1408
  }
1409
+ let suppressForbiddenBasename = false;
1743
1410
  if (event.action === "command_exec" && event.targets && event.targets.length > 0) {
1411
+ const literalCommand = event.targets[0] ?? "";
1412
+ const decodedImplicated = event.targets.slice(1).some((t) => scanBashCommand(t, FORBIDDEN_BASENAMES).matched);
1413
+ suppressForbiddenBasename = isPositionallySafeMention(literalCommand) && !decodedImplicated;
1414
+ }
1415
+ if (event.action === "command_exec" && event.targets && event.targets.length > 0 && !suppressForbiddenBasename) {
1744
1416
  const allL2Hits = [];
1745
1417
  for (const scanTarget of event.targets) {
1746
1418
  const scan = scanBashCommand(scanTarget, FORBIDDEN_BASENAMES);
@@ -2239,14 +1911,16 @@ async function runGatewayDaemon({
2239
1911
  policyPath,
2240
1912
  port = DEFAULT_PORT
2241
1913
  }) {
2242
- const { Sentinel: SentinelClass } = await import("./Sentinel-JLQL3YRD.js");
2243
- const { writePidFile } = await import("./pidManager-ZYC7SICM.js");
1914
+ const { Sentinel: SentinelClass } = await import("./Sentinel-QHMQ67W3.js");
1915
+ const { writePidFile, writeReleaseToken } = await import("./pidManager-DOGVN6ZT.js");
2244
1916
  const { homedir } = await import("os");
1917
+ const { randomBytes } = await import("crypto");
2245
1918
  const { loadPolicy: loadPolicy2, policyToRole: policyToRole2 } = await import("./policyLoader-6KR5VFVV.js");
2246
1919
  const sentinel = await SentinelClass.fromPolicy(policyPath);
2247
1920
  const baseline = await sentinel.computeBaseline("claude-code");
2248
1921
  sentinel.setBaseline("claude-code", baseline);
2249
1922
  const operatorCeiling = policyToRole2(await loadPolicy2(policyPath));
1923
+ const releaseToken = randomBytes(32).toString("hex");
2250
1924
  const gateway = new SentinelGateway({
2251
1925
  port,
2252
1926
  sentinel,
@@ -2254,10 +1928,13 @@ async function runGatewayDaemon({
2254
1928
  agentId: "claude-code",
2255
1929
  workspaceIsolation: process.env.SENTINEL_WORKSPACE_ISOLATION !== "0",
2256
1930
  operatorCeiling,
2257
- home: homedir()
1931
+ home: homedir(),
1932
+ releaseToken
2258
1933
  });
2259
1934
  await gateway.start();
2260
- writePidFile(homedir(), process.pid);
1935
+ const home = homedir();
1936
+ writePidFile(home, process.pid);
1937
+ writeReleaseToken(home, releaseToken, gateway.port);
2261
1938
  console.log(`[SENTINEL GATEWAY] PID ${process.pid} written`);
2262
1939
  }
2263
1940
 
@@ -2265,4 +1942,4 @@ export {
2265
1942
  SentinelGateway,
2266
1943
  runGatewayDaemon
2267
1944
  };
2268
- //# sourceMappingURL=chunk-Z3PWIJKT.js.map
1945
+ //# sourceMappingURL=chunk-IYC5E7RL.js.map
@@ -3,6 +3,34 @@ import { readFileSync, writeFileSync, unlinkSync, renameSync } from "fs";
3
3
  import { execSync } from "child_process";
4
4
  import { join } from "path";
5
5
  var PID_FILENAME = "sentinel-gateway.pid";
6
+ var TOKEN_FILENAME = "sentinel-gateway.token";
7
+ function releaseTokenPath(home) {
8
+ return join(home, ".dahlia", TOKEN_FILENAME);
9
+ }
10
+ function writeReleaseToken(home, token, port) {
11
+ const p = releaseTokenPath(home);
12
+ const tmp = p + `.tmp.${process.pid}`;
13
+ writeFileSync(tmp, JSON.stringify({ token, port }) + "\n", { mode: 384 });
14
+ renameSync(tmp, p);
15
+ }
16
+ function readReleaseToken(home) {
17
+ try {
18
+ const raw = readFileSync(releaseTokenPath(home), "utf-8").trim();
19
+ const j = JSON.parse(raw);
20
+ if (typeof j.token === "string" && j.token.length > 0 && typeof j.port === "number") {
21
+ return { token: j.token, port: j.port };
22
+ }
23
+ return null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ function removeReleaseToken(home) {
29
+ try {
30
+ unlinkSync(releaseTokenPath(home));
31
+ } catch {
32
+ }
33
+ }
6
34
  function readPidFile(home) {
7
35
  try {
8
36
  const raw = readFileSync(join(home, ".dahlia", PID_FILENAME), "utf-8").trim();
@@ -53,10 +81,14 @@ function acquireGatewayLock(home) {
53
81
  }
54
82
 
55
83
  export {
84
+ releaseTokenPath,
85
+ writeReleaseToken,
86
+ readReleaseToken,
87
+ removeReleaseToken,
56
88
  readPidFile,
57
89
  writePidFile,
58
90
  removePidFile,
59
91
  verifyPidIsGateway,
60
92
  acquireGatewayLock
61
93
  };
62
- //# sourceMappingURL=chunk-CUJKNIKT.js.map
94
+ //# sourceMappingURL=chunk-LATQNIRW.js.map