@tuent/sentinel 0.1.2 → 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.
- package/SECURITY_MODEL.md +9 -0
- package/dist/Sentinel-4QKPFHTI.js +10 -0
- package/dist/{Sentinel-xFCyXH45.d.ts → Sentinel-DT0IyGQi.d.ts} +127 -9
- package/dist/{chunk-PDWWRZXF.js → chunk-2IPSTUNH.js} +18 -7
- package/dist/chunk-B6S2PBS4.js +47 -0
- package/dist/{chunk-QIYQWOLO.js → chunk-FIEIGBYL.js} +387 -242
- package/dist/{chunk-L4R3LPJS.js → chunk-HRI2Y326.js} +119 -35
- package/dist/{chunk-GRN5P3H2.js → chunk-I2FVDDSG.js} +242 -94
- package/dist/{chunk-WLIDSTS4.js → chunk-KWZ7JKKO.js} +221 -27
- package/dist/{chunk-FWIISAZZ.js → chunk-LTBVWF5H.js} +201 -80
- package/dist/chunk-TKAKHSZ3.js +1 -0
- package/dist/cli.js +33 -35
- package/dist/gateway/index.d.ts +20 -1
- package/dist/gateway/index.js +4 -4
- package/dist/gatewayDaemon.js +38 -16
- package/dist/index.d.ts +31 -19
- package/dist/index.js +9 -8
- package/dist/logAdapter-WM43W3S7.js +7 -0
- package/dist/{mcpAdapter-R47GX2P3.js → mcpAdapter-WYAXUE7T.js} +2 -2
- package/dist/{policyLoader-KZL2U4M2.js → policyLoader-XX6BQXNB.js} +8 -4
- package/package.json +1 -1
- package/dist/Sentinel-XMSJE4DZ.js +0 -10
- package/dist/chunk-FMZWHT4M.js +0 -20
- package/dist/logAdapter-IB6ZDEV2.js +0 -7
package/SECURITY_MODEL.md
CHANGED
|
@@ -68,6 +68,15 @@ All target paths are normalized with `path.normalize()` before pattern matching.
|
|
|
68
68
|
- `src/../.env` is normalized to `.env` before checking against `**/.env`
|
|
69
69
|
- `project/subdir/../../.ssh/id_rsa` is normalized to `.ssh/id_rsa`
|
|
70
70
|
|
|
71
|
+
Conversely, `command_exec` targets (whole shell command strings) are **never glob-matched as if they were a path**. They are screened by a scanner: the command is tokenized, path-shaped tokens are resolved and matched against the forbidden patterns, and every argv token's basename is matched against each pattern's basename component with **component-boundary** semantics (the basename must match as a whole, not as a substring). So `process.env` and similarly-shaped code constructs are not treated as file access (the basename `process.env` is not the basename `.env`), while a real operand like `payroll.csv` against a `payroll.csv` forbid pattern is denied — in any command position, not only at the end. This screen is the single source of truth for command screening on **both** the gateway and the embedded-SDK (`wrap()`/`check()`) paths; the gateway additionally keeps its inline L1/L2 scan as defense-in-depth. Correspondingly, `command_exec` is **exempt from the allowed-target allowlist** (and session `SCOPE:` narrowing): those are path globs, and a command string is not a path, so matching it there only produced false `scope_violation`s — which the sensitivity scorer could escalate to a hard block when the command merely named a credential token (e.g. `process.env`). Whether an agent may run commands at all is governed by `allowedActions`; whether a given command is forbidden is the command screen above.
|
|
72
|
+
|
|
73
|
+
Two residuals of letting operators forbid bare basenames in commands (both pre-existing, narrower than the prior whole-string glob, and operator-self-inflicted):
|
|
74
|
+
|
|
75
|
+
- **R1 — generic-word custom basenames.** A custom forbid pattern whose basename is a common word (e.g. a pattern ending in `build`) will also match a command that uses that word as a non-file argument (e.g. `npm run build`). This is intrinsic to forbidding a bare common word; prefer specific basenames for custom command-forbid patterns. The same property holds for path-target matching.
|
|
76
|
+
- **R2 — positional-safety scope.** The echo-class positional-safety suppression (`isPositionallySafeMention`) is scoped to the built-in basename list, not to operator-custom patterns, so a custom basename mentioned in an otherwise-safe position (e.g. as an `echo` argument) is still denied.
|
|
77
|
+
|
|
78
|
+
SDK-path residual: obfuscated/encoded command operands are de-obfuscated only on the gateway path (the translator's resolver pipeline); a direct `wrap()`/`check()` caller gets tokenized + basename + substring screening but not the decode layer.
|
|
79
|
+
|
|
71
80
|
### Self-Protection: Policy and Hook-Wiring Files
|
|
72
81
|
|
|
73
82
|
The agent's own tool-writes to the live policy file (`.sentinel.yaml`) and to Claude Code's hook-wiring settings files (`.claude/settings.json` and `.claude/settings.local.json`, project- and user-level) are **denied by default**. Without this, an agent could rewrite its own policy or remove the hook entries that route its tool calls through the gateway with an ordinary file write. The corresponding forbid patterns ship in the defaults and the starter policy; the read carve-out is appended **code-side at ceiling-role construction** and is deliberately not authorable from the workspace yaml (workspace-authored exceptions are dropped by the ceiling merge, so a policy file can never widen its own protection away).
|
|
@@ -16,6 +16,7 @@ interface SkillNode {
|
|
|
16
16
|
core?: boolean;
|
|
17
17
|
sharable?: boolean;
|
|
18
18
|
files?: string[];
|
|
19
|
+
eventCount?: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
interface DataPoint {
|
|
@@ -29,6 +30,7 @@ interface DataPoint {
|
|
|
29
30
|
files?: string[];
|
|
30
31
|
sharable?: boolean;
|
|
31
32
|
core?: boolean;
|
|
33
|
+
eventCount?: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
interface StoredProfile {
|
|
@@ -89,16 +91,41 @@ declare class ProfileStore {
|
|
|
89
91
|
private petalCount;
|
|
90
92
|
private engine;
|
|
91
93
|
private profile;
|
|
94
|
+
/** Snapshot (label → serialized point) of the on-disk-synced dataPoint set,
|
|
95
|
+
* refreshed on every load/create/write. The base for save()'s three-way merge
|
|
96
|
+
* of concurrent external writes — lets us tell "I deleted this" (in baseline,
|
|
97
|
+
* gone from memory) from "another writer added this" (on disk, not in baseline). */
|
|
98
|
+
private baseline;
|
|
92
99
|
constructor(options: {
|
|
93
100
|
backend: StorageBackend;
|
|
94
101
|
eventBus?: EventBus;
|
|
95
102
|
petalCount?: number;
|
|
96
103
|
});
|
|
97
104
|
load(): Promise<StoredProfile | null>;
|
|
105
|
+
/** Snapshot the on-disk-synced dataPoint set for save()'s three-way merge. */
|
|
106
|
+
private captureBaseline;
|
|
98
107
|
create(name: string): Promise<StoredProfile>;
|
|
99
108
|
addPoint(point: DataPoint): Promise<void>;
|
|
100
109
|
addPoints(points: DataPoint[]): void;
|
|
101
110
|
save(): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Three-way merge of a concurrent external write into the in-memory profile,
|
|
113
|
+
* invoked by save() when the backend reports the file changed since we last
|
|
114
|
+
* synced. Writers in this app are single-user but multi-process (agent scanner,
|
|
115
|
+
* diary watcher, dev server, CLI), so concurrent writes are real but rare.
|
|
116
|
+
*
|
|
117
|
+
* Model (base = the dataPoint set at our last load/write, see `baseline`):
|
|
118
|
+
* - a point present in memory → kept as-is (the SAVING process's adds/edits
|
|
119
|
+
* win on a same-point conflict — save() is an explicit "persist my state");
|
|
120
|
+
* - a disk point whose label is NEW since baseline → another writer's
|
|
121
|
+
* addition → preserved;
|
|
122
|
+
* - a point we dropped (in baseline, absent from memory) → our deletion is
|
|
123
|
+
* honored, UNLESS the other writer modified it (then their version is kept
|
|
124
|
+
* so a concurrent edit is never silently lost).
|
|
125
|
+
* Net: nothing vanishes except a same-point simultaneous edit, resolved
|
|
126
|
+
* last-writer (this save) — a deliberate, documented tie-break.
|
|
127
|
+
*/
|
|
128
|
+
private mergeExternalChanges;
|
|
102
129
|
build(): SkillNode[];
|
|
103
130
|
getProfile(): StoredProfile | null;
|
|
104
131
|
}
|
|
@@ -299,6 +326,21 @@ interface SecurityFinding {
|
|
|
299
326
|
* finding is its own distinct entry (never merged).
|
|
300
327
|
*/
|
|
301
328
|
dedupKey?: string;
|
|
329
|
+
/**
|
|
330
|
+
* Sprint 26B F-5a (corroboration-by-distinct-target). Identity of the RESOLVED
|
|
331
|
+
* forbidden target the deny fired on: the L1-resolved path when one exists,
|
|
332
|
+
* else a basename-family fallback (`l2:<sorted basenames>`) for L2-only hits
|
|
333
|
+
* that never produced a resolved path (unparseable / construct ambiguity).
|
|
334
|
+
* When present, getEffectiveBlockCount keys distinct-counting on it INSTEAD of
|
|
335
|
+
* dedupKey, so repeated denials against the same forbidden target — e.g. many
|
|
336
|
+
* differently-shaped commands all naming one token during self-hosted
|
|
337
|
+
* development — contribute ONE count toward the escalation ladder, while
|
|
338
|
+
* distinct forbidden targets still accumulate (a real multi-file sweep
|
|
339
|
+
* escalates as before). Additive and conditional (soft-signal discipline):
|
|
340
|
+
* absent on pre-F-5a entries, which keep counting by dedupKey / keyless
|
|
341
|
+
* exactly as before. NEVER changes the per-command deny disposition.
|
|
342
|
+
*/
|
|
343
|
+
targetKey?: string;
|
|
302
344
|
}
|
|
303
345
|
/** Computed behavioral baseline for an agent over a time window. */
|
|
304
346
|
/**
|
|
@@ -365,7 +407,6 @@ interface SentinelConfig {
|
|
|
365
407
|
mcpLogDir?: string;
|
|
366
408
|
webhookPort?: number;
|
|
367
409
|
webhookApiKey?: string;
|
|
368
|
-
roleDefinitionPath?: string;
|
|
369
410
|
readExisting?: boolean;
|
|
370
411
|
}[];
|
|
371
412
|
alerts?: {
|
|
@@ -385,9 +426,6 @@ interface SentinelConfig {
|
|
|
385
426
|
minKind?: "informational" | "actionable";
|
|
386
427
|
}[];
|
|
387
428
|
};
|
|
388
|
-
alertWebhook?: string;
|
|
389
|
-
baselineWindowDays?: number;
|
|
390
|
-
checkIntervalMs?: number;
|
|
391
429
|
}
|
|
392
430
|
/** A declared task/intent for an agent. */
|
|
393
431
|
interface TaskIntent {
|
|
@@ -432,13 +470,18 @@ interface IntentAlignmentResult {
|
|
|
432
470
|
bypassed?: boolean;
|
|
433
471
|
bypassReason?: string | null;
|
|
434
472
|
}
|
|
435
|
-
/**
|
|
473
|
+
/**
|
|
474
|
+
* Configuration for intent alignment scoring.
|
|
475
|
+
*
|
|
476
|
+
* NOTE: the keywordBoost / acceptableActionBoost / baseScore /
|
|
477
|
+
* missingTaskScore knobs were removed — they were defined, defaulted, and
|
|
478
|
+
* typed but never read by any scoring path (vestiges of a pre-weighted
|
|
479
|
+
* scoring model superseded by SimilarityEngine.keywordSimilarity). They
|
|
480
|
+
* promised tunability that did not exist; the live weights are fixed in
|
|
481
|
+
* SimilarityEngine. defaultTtlMs is the only consumed numeric knob.
|
|
482
|
+
*/
|
|
436
483
|
interface IntentAlignmentConfig {
|
|
437
484
|
defaultTtlMs: number;
|
|
438
|
-
keywordBoost: number;
|
|
439
|
-
acceptableActionBoost: number;
|
|
440
|
-
baseScore: number;
|
|
441
|
-
missingTaskScore: number;
|
|
442
485
|
disableDefaultAcceptable?: boolean;
|
|
443
486
|
denyAcceptable?: string[];
|
|
444
487
|
similarityThreshold?: number;
|
|
@@ -849,6 +892,20 @@ declare class AuditTrail {
|
|
|
849
892
|
* the file when an external append actually happened.
|
|
850
893
|
*/
|
|
851
894
|
private reanchorFromDiskTail;
|
|
895
|
+
/**
|
|
896
|
+
* Scan the on-disk log backward for the last entry carrying a valid (64-hex)
|
|
897
|
+
* hash, SKIPPING torn/corrupt trailing lines, and return that hash (or null
|
|
898
|
+
* when no parseable hashed entry exists).
|
|
899
|
+
*
|
|
900
|
+
* Shared by open() (chain resume) and reanchorFromDiskTail() (live
|
|
901
|
+
* re-anchor). The earlier inline versions inspected only the LAST non-empty
|
|
902
|
+
* line and broke: a torn tail line (a partial write) left the head at genesis
|
|
903
|
+
* (open) or stale (reanchor), so the next append forked a fresh chain from
|
|
904
|
+
* genesis — destroying all prior linkage. Walking back leaves the torn line
|
|
905
|
+
* as a single localized gap that verify() still flags, while the chain head
|
|
906
|
+
* stays anchored to the last good entry.
|
|
907
|
+
*/
|
|
908
|
+
private lastHashOnDisk;
|
|
852
909
|
private readAllEntries;
|
|
853
910
|
private loadCumulativeStats;
|
|
854
911
|
/**
|
|
@@ -901,6 +958,54 @@ interface AgentClassifierConfig {
|
|
|
901
958
|
minDurationMs: number;
|
|
902
959
|
}
|
|
903
960
|
|
|
961
|
+
interface SensitivityRule {
|
|
962
|
+
pattern: string;
|
|
963
|
+
sensitivity: number;
|
|
964
|
+
category: string;
|
|
965
|
+
description: string;
|
|
966
|
+
}
|
|
967
|
+
interface TargetSensitivityResult {
|
|
968
|
+
sensitivity: number;
|
|
969
|
+
category: string;
|
|
970
|
+
description: string;
|
|
971
|
+
matchedRule: string;
|
|
972
|
+
actionMultiplier: number;
|
|
973
|
+
effectiveScore: number;
|
|
974
|
+
groundedInRepoMap: boolean;
|
|
975
|
+
}
|
|
976
|
+
declare class TargetSensitivityScorer {
|
|
977
|
+
private rules;
|
|
978
|
+
private repoMap;
|
|
979
|
+
private repoRoot;
|
|
980
|
+
private overlay;
|
|
981
|
+
constructor(customRules?: SensitivityRule[], repoMap?: RepoSensitivityMap, repoRoot?: string, overlay?: SensitivityOverlay);
|
|
982
|
+
scoreTarget(target: string, action: string): TargetSensitivityResult;
|
|
983
|
+
/** Pattern-only scoring (Tier 2). Used directly by reject decisions. */
|
|
984
|
+
private _scoreByPatterns;
|
|
985
|
+
/**
|
|
986
|
+
* URL-aware component scoring (Sprint 6b W1 fix).
|
|
987
|
+
* Parses the URL, extracts path/query-values/fragment, decodes each once,
|
|
988
|
+
* scores each against forbidden-pattern rules, takes MAX.
|
|
989
|
+
*/
|
|
990
|
+
private _scoreUrlTarget;
|
|
991
|
+
scoreEvent(event: AgentActivityEvent): TargetSensitivityResult;
|
|
992
|
+
/**
|
|
993
|
+
* Replaces the active repo sensitivity map. Pass null to clear.
|
|
994
|
+
* If newRepoRoot is provided, it overrides the map's repoRoot.
|
|
995
|
+
*/
|
|
996
|
+
updateRepoMap(newMap: RepoSensitivityMap | null, newRepoRoot?: string): void;
|
|
997
|
+
/**
|
|
998
|
+
* Replaces the active sensitivity overlay. Pass null to clear.
|
|
999
|
+
*/
|
|
1000
|
+
updateOverlay(newOverlay: SensitivityOverlay | null): void;
|
|
1001
|
+
/**
|
|
1002
|
+
* Returns all pattern-based rules that match the target. Does NOT
|
|
1003
|
+
* consult the repo map. Used by RepoSensitivityScanner during
|
|
1004
|
+
* scan-time enumeration.
|
|
1005
|
+
*/
|
|
1006
|
+
getAllMatchingRules(target: string): SensitivityRule[];
|
|
1007
|
+
}
|
|
1008
|
+
|
|
904
1009
|
/** Lazy-init wrapper passed via RoleValidatorOptions so Sentinel can own the cache. */
|
|
905
1010
|
interface ForbiddenInodeCacheRef {
|
|
906
1011
|
getOrBuild: () => Set<bigint>;
|
|
@@ -1214,6 +1319,7 @@ declare class SentinelRunner {
|
|
|
1214
1319
|
private _hookEngine?;
|
|
1215
1320
|
private _maturityConfig?;
|
|
1216
1321
|
private _validatorOptions?;
|
|
1322
|
+
private _sensitivityScorer?;
|
|
1217
1323
|
private intentTracker;
|
|
1218
1324
|
private similarityEngine;
|
|
1219
1325
|
private recentAlignmentScores;
|
|
@@ -1234,6 +1340,12 @@ declare class SentinelRunner {
|
|
|
1234
1340
|
setMaturityConfig(config: Partial<BaselineMaturityConfig>): void;
|
|
1235
1341
|
/** Set RoleValidator options (exception approval fn, audit callback). */
|
|
1236
1342
|
setRoleValidatorOptions(options: RoleValidatorOptions): void;
|
|
1343
|
+
/**
|
|
1344
|
+
* Inject the facade's shared sensitivity scorer (overlay + repo map aware)
|
|
1345
|
+
* so processEvent's validator scores identically to the facade's check().
|
|
1346
|
+
* Must be called BEFORE start() builds the validator.
|
|
1347
|
+
*/
|
|
1348
|
+
setSensitivityScorer(scorer: TargetSensitivityScorer): void;
|
|
1237
1349
|
/** Set the behavioral baseline and start periodic gap checking. */
|
|
1238
1350
|
setBaseline(baseline: AgentBaseline): void;
|
|
1239
1351
|
/** Declare a new task intent for this agent. */
|
|
@@ -1250,6 +1362,12 @@ declare class SentinelRunner {
|
|
|
1250
1362
|
getActiveTask(): TaskIntent | null;
|
|
1251
1363
|
/** Expose the similarity engine for pre-execution intent checks. */
|
|
1252
1364
|
getSimilarityEngine(): SimilarityEngine;
|
|
1365
|
+
/**
|
|
1366
|
+
* Read-only view of the rolling misaligned-score window, for the
|
|
1367
|
+
* pre-execution gate (Sentinel.check) to apply the SAME sustained-drift
|
|
1368
|
+
* severity upgrade as the recording path — without mutating the window.
|
|
1369
|
+
*/
|
|
1370
|
+
getRecentAlignmentScores(): readonly number[];
|
|
1253
1371
|
/**
|
|
1254
1372
|
* Side-effect-free pre-execution gate: evaluate an event and fire pre_execution
|
|
1255
1373
|
* hooks WITHOUT running processEvent's full pipeline.
|
|
@@ -16,6 +16,9 @@ var LogAdapter = class {
|
|
|
16
16
|
pendingFragment = "";
|
|
17
17
|
running = false;
|
|
18
18
|
polling = false;
|
|
19
|
+
/** The in-flight poll (if any), so stop() can let it finish emitting its
|
|
20
|
+
* already-read batch before persisting the cursor. */
|
|
21
|
+
pollInFlight = null;
|
|
19
22
|
pollTimer = null;
|
|
20
23
|
onEvent = null;
|
|
21
24
|
readExisting;
|
|
@@ -33,6 +36,7 @@ var LogAdapter = class {
|
|
|
33
36
|
if (this.running) return;
|
|
34
37
|
this.onEvent = onEvent;
|
|
35
38
|
this.running = true;
|
|
39
|
+
this.pendingFragment = "";
|
|
36
40
|
const savedPosition = await this.loadCursor();
|
|
37
41
|
if (savedPosition !== null) {
|
|
38
42
|
this.lastReadPosition = savedPosition;
|
|
@@ -47,7 +51,9 @@ var LogAdapter = class {
|
|
|
47
51
|
} else {
|
|
48
52
|
this.lastReadPosition = 0;
|
|
49
53
|
}
|
|
50
|
-
this.pollTimer = setInterval(() =>
|
|
54
|
+
this.pollTimer = setInterval(() => {
|
|
55
|
+
this.pollInFlight = this.poll();
|
|
56
|
+
}, this.pollIntervalMs);
|
|
51
57
|
console.log(`LogAdapter watching ${this.logPath} for agent ${this.agentId}`);
|
|
52
58
|
}
|
|
53
59
|
async stop() {
|
|
@@ -55,6 +61,13 @@ var LogAdapter = class {
|
|
|
55
61
|
clearInterval(this.pollTimer);
|
|
56
62
|
this.pollTimer = null;
|
|
57
63
|
}
|
|
64
|
+
if (this.pollInFlight) {
|
|
65
|
+
try {
|
|
66
|
+
await this.pollInFlight;
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
this.pollInFlight = null;
|
|
70
|
+
}
|
|
58
71
|
await this.saveCursor();
|
|
59
72
|
this.running = false;
|
|
60
73
|
this.onEvent = null;
|
|
@@ -66,7 +79,6 @@ var LogAdapter = class {
|
|
|
66
79
|
try {
|
|
67
80
|
const lines = await this.readNewLines();
|
|
68
81
|
for (const line of lines) {
|
|
69
|
-
if (!this.running) break;
|
|
70
82
|
const event = this.parseLine(line);
|
|
71
83
|
if (event) {
|
|
72
84
|
try {
|
|
@@ -194,10 +206,9 @@ var LogAdapter = class {
|
|
|
194
206
|
try {
|
|
195
207
|
const { writeFile, mkdir } = await import("fs/promises");
|
|
196
208
|
await mkdir(this.stateDir, { recursive: true });
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
209
|
+
const fragmentBytes = Buffer.byteLength(this.pendingFragment, "utf-8");
|
|
210
|
+
const position = Math.max(0, this.lastReadPosition - fragmentBytes);
|
|
211
|
+
await writeFile(path, JSON.stringify({ position, logPath: this.logPath }) + "\n");
|
|
201
212
|
} catch (err) {
|
|
202
213
|
console.warn(`Failed to save log cursor: ${err}`);
|
|
203
214
|
}
|
|
@@ -235,4 +246,4 @@ var LogAdapter = class {
|
|
|
235
246
|
export {
|
|
236
247
|
LogAdapter
|
|
237
248
|
};
|
|
238
|
-
//# sourceMappingURL=chunk-
|
|
249
|
+
//# sourceMappingURL=chunk-2IPSTUNH.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/setup/policyDiscovery.ts
|
|
2
|
+
import { existsSync, statSync } from "fs";
|
|
3
|
+
import { resolve, join, dirname } from "path";
|
|
4
|
+
function isTrustedPolicyStat(stats, processUid) {
|
|
5
|
+
if ((stats.mode & 2) !== 0) {
|
|
6
|
+
return { trusted: false, reason: "file is world-writable" };
|
|
7
|
+
}
|
|
8
|
+
if (processUid !== void 0 && stats.uid !== processUid) {
|
|
9
|
+
return {
|
|
10
|
+
trusted: false,
|
|
11
|
+
reason: `file is owned by uid ${stats.uid}, not the current user (uid ${processUid})`
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return { trusted: true, reason: null };
|
|
15
|
+
}
|
|
16
|
+
function isTrustedOnDisk(candidate) {
|
|
17
|
+
if (process.platform === "win32") return true;
|
|
18
|
+
let stats;
|
|
19
|
+
try {
|
|
20
|
+
stats = statSync(candidate);
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const processUid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
25
|
+
const verdict = isTrustedPolicyStat({ uid: stats.uid, mode: stats.mode }, processUid);
|
|
26
|
+
if (!verdict.trusted) {
|
|
27
|
+
console.warn(`[Sentinel] Ignoring untrusted policy file ${candidate}: ${verdict.reason}`);
|
|
28
|
+
}
|
|
29
|
+
return verdict.trusted;
|
|
30
|
+
}
|
|
31
|
+
function discoverPolicy(startDir, home) {
|
|
32
|
+
let dir = resolve(startDir);
|
|
33
|
+
const homeAbs = resolve(home);
|
|
34
|
+
while (true) {
|
|
35
|
+
const candidate = join(dir, ".sentinel.yaml");
|
|
36
|
+
if (existsSync(candidate) && isTrustedOnDisk(candidate)) return candidate;
|
|
37
|
+
if (dir === homeAbs) return null;
|
|
38
|
+
const parent = dirname(dir);
|
|
39
|
+
if (parent === dir) return null;
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
discoverPolicy
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=chunk-B6S2PBS4.js.map
|