sentinelayer-cli 0.4.5 → 0.8.0

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.
Files changed (72) hide show
  1. package/README.md +16 -18
  2. package/package.json +7 -6
  3. package/src/agents/jules/config/definition.js +13 -62
  4. package/src/agents/jules/config/system-prompt.js +8 -1
  5. package/src/agents/jules/fix-cycle.js +12 -372
  6. package/src/agents/jules/loop.js +116 -26
  7. package/src/agents/jules/pulse.js +10 -327
  8. package/src/agents/jules/stream.js +13 -12
  9. package/src/agents/jules/swarm/orchestrator.js +3 -3
  10. package/src/agents/jules/swarm/sub-agent.js +6 -3
  11. package/src/agents/jules/tools/aidenid-email.js +189 -0
  12. package/src/agents/jules/tools/auth-audit.js +1187 -45
  13. package/src/agents/jules/tools/dispatch.js +25 -12
  14. package/src/agents/jules/tools/file-edit.js +2 -180
  15. package/src/agents/jules/tools/file-read.js +2 -100
  16. package/src/agents/jules/tools/glob.js +2 -168
  17. package/src/agents/jules/tools/grep.js +2 -228
  18. package/src/agents/jules/tools/path-guards.js +2 -161
  19. package/src/agents/jules/tools/runtime-audit.js +6 -2
  20. package/src/agents/jules/tools/shell.js +2 -383
  21. package/src/agents/persona-visuals.js +64 -0
  22. package/src/agents/shared-tools/dispatch-core.js +320 -0
  23. package/src/agents/shared-tools/file-edit.js +180 -0
  24. package/src/agents/shared-tools/file-read.js +100 -0
  25. package/src/agents/shared-tools/glob.js +168 -0
  26. package/src/agents/shared-tools/grep.js +228 -0
  27. package/src/agents/shared-tools/index.js +46 -0
  28. package/src/agents/shared-tools/path-guards.js +161 -0
  29. package/src/agents/shared-tools/shell.js +383 -0
  30. package/src/ai/aidenid.js +56 -7
  31. package/src/ai/client.js +45 -0
  32. package/src/ai/proxy.js +137 -0
  33. package/src/auth/gate.js +290 -16
  34. package/src/auth/http.js +450 -39
  35. package/src/auth/service.js +262 -47
  36. package/src/auth/session-store.js +475 -21
  37. package/src/cli.js +5 -0
  38. package/src/commands/audit.js +13 -8
  39. package/src/commands/auth.js +53 -9
  40. package/src/commands/omargate.js +10 -2
  41. package/src/commands/scan.js +10 -4
  42. package/src/commands/session.js +590 -0
  43. package/src/commands/spec.js +62 -0
  44. package/src/commands/watch.js +3 -2
  45. package/src/daemon/assignment-ledger.js +196 -0
  46. package/src/daemon/error-worker.js +599 -16
  47. package/src/daemon/fix-cycle.js +384 -0
  48. package/src/daemon/ingest-refresh.js +10 -9
  49. package/src/daemon/jira-lifecycle.js +135 -0
  50. package/src/daemon/pulse.js +327 -0
  51. package/src/daemon/scope-engine.js +1068 -0
  52. package/src/events/schema.js +190 -0
  53. package/src/interactive/index.js +18 -16
  54. package/src/legacy-cli.js +606 -37
  55. package/src/prompt/generator.js +19 -1
  56. package/src/review/ai-review.js +11 -1
  57. package/src/review/local-review.js +75 -19
  58. package/src/review/omargate-interactive.js +68 -0
  59. package/src/review/omargate-orchestrator.js +404 -0
  60. package/src/review/persona-prompts.js +296 -0
  61. package/src/review/scan-modes.js +48 -0
  62. package/src/scan/generator.js +1 -1
  63. package/src/session/agent-registry.js +352 -0
  64. package/src/session/daemon.js +801 -0
  65. package/src/session/paths.js +33 -0
  66. package/src/session/runtime-bridge.js +739 -0
  67. package/src/session/store.js +388 -0
  68. package/src/session/stream.js +325 -0
  69. package/src/spec/generator.js +100 -0
  70. package/src/telemetry/session-tracker.js +148 -32
  71. package/src/telemetry/sync.js +6 -2
  72. package/src/ui/command-hints.js +13 -0
package/src/auth/gate.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import process from "node:process";
2
+ import crypto from "node:crypto";
3
+ import os from "node:os";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
2
6
  import pc from "picocolors";
3
- import { readStoredSession } from "./session-store.js";
7
+ import { resolveActiveAuthSession } from "./service.js";
8
+ import { authLoginHint } from "../ui/command-hints.js";
4
9
 
5
10
  /**
6
11
  * Auth gate — ensures user is logged in before running any command.
@@ -27,14 +32,264 @@ const AUTH_BYPASS_COMMANDS = new Set([
27
32
  const NO_AUTH_REQUIRED = new Set([
28
33
  "config", // local config inspection
29
34
  ]);
35
+ const SESSION_NO_AUTH_SUBCOMMANDS = new Set([
36
+ "read",
37
+ "list",
38
+ "status",
39
+ ]);
40
+
41
+ const TEST_BYPASS_NONCE_ENV = "SENTINELAYER_CLI_TEST_BYPASS_NONCE";
42
+ const TEST_BYPASS_SECRET_ENV = "SENTINELAYER_CLI_TEST_BYPASS_SECRET";
43
+ const TEST_BYPASS_TOKEN_ENV = "SENTINELAYER_CLI_TEST_BYPASS_TOKEN";
44
+ const TEST_BYPASS_NONCE_FILENAME_PREFIX = "sentinelayer-cli-test-bypass";
45
+ const TEST_BYPASS_NONCE_MAX_AGE_MS = 5 * 60 * 1000;
46
+ const TEST_BYPASS_ALLOWED_EXECUTABLES = new Set([
47
+ "create-sentinelayer.js",
48
+ "sentinelayer-cli.js",
49
+ "sl.js",
50
+ "cli.js",
51
+ ]);
52
+ const TEST_BYPASS_ALLOWED_COMMANDS = new Set([
53
+ "audit",
54
+ "chat",
55
+ "config",
56
+ "cost",
57
+ "daemon",
58
+ "guide",
59
+ "ingest",
60
+ "mcp",
61
+ "plugin",
62
+ "policy",
63
+ "prompt",
64
+ "review",
65
+ "scan",
66
+ "spec",
67
+ "swarm",
68
+ "telemetry",
69
+ "watch",
70
+ ]);
71
+ const TEST_BYPASS_BLOCKED_FLAGS = new Set([
72
+ "--apply",
73
+ "--delete",
74
+ "--deploy",
75
+ "--destroy",
76
+ "--execute",
77
+ "--fix",
78
+ "--force",
79
+ "--merge",
80
+ "--promote",
81
+ "--publish",
82
+ "--push",
83
+ "--regenerate",
84
+ "--revoke",
85
+ ]);
86
+
87
+ function isTruthy(value) {
88
+ const normalized = String(value || "").trim().toLowerCase();
89
+ return normalized === "1" || normalized === "true" || normalized === "yes";
90
+ }
91
+
92
+ function isPackagedBuild() {
93
+ if (process.pkg) {
94
+ return true;
95
+ }
96
+ const execPath = String(process.execPath || "");
97
+ const execBase = path.basename(execPath).toLowerCase();
98
+ return execBase !== "node" && execBase !== "node.exe";
99
+ }
100
+
101
+ function readNonceEnvelope(nonce) {
102
+ const normalizedNonce = String(nonce || "").trim();
103
+ if (!normalizedNonce) {
104
+ return null;
105
+ }
106
+ const nonceFile = path.join(os.tmpdir(), `${TEST_BYPASS_NONCE_FILENAME_PREFIX}-${normalizedNonce}.nonce`);
107
+ try {
108
+ const stats = fs.statSync(nonceFile);
109
+ if (!stats.isFile()) {
110
+ return null;
111
+ }
112
+ if (typeof process.getuid === "function") {
113
+ if (stats.uid !== process.getuid()) {
114
+ return null;
115
+ }
116
+ if (stats.mode & 0o022) {
117
+ return null;
118
+ }
119
+ }
120
+ if (stats.size <= 0 || stats.size > 1024) {
121
+ return null;
122
+ }
123
+ const payloadRaw = fs.readFileSync(nonceFile, "utf-8");
124
+ const payload = JSON.parse(payloadRaw);
125
+ const payloadNonce = String(payload?.nonce || "").trim();
126
+ const payloadPid = Number(payload?.pid);
127
+ const payloadTs = Number(payload?.ts);
128
+ if (payloadNonce !== normalizedNonce) {
129
+ return null;
130
+ }
131
+ if (!Number.isInteger(payloadPid) || payloadPid <= 0) {
132
+ return null;
133
+ }
134
+ if (!Number.isFinite(payloadTs) || payloadTs <= 0) {
135
+ return null;
136
+ }
137
+ if (Math.abs(Date.now() - payloadTs) > TEST_BYPASS_NONCE_MAX_AGE_MS) {
138
+ return null;
139
+ }
140
+ return {
141
+ nonce: payloadNonce,
142
+ pid: payloadPid,
143
+ ts: payloadTs,
144
+ nonceFile,
145
+ };
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ function consumeNonceEnvelope(nonceFile) {
152
+ const normalizedPath = String(nonceFile || "").trim();
153
+ if (!normalizedPath) {
154
+ return false;
155
+ }
156
+ const consumedPath = `${normalizedPath}.used.${process.pid}.${Date.now()}`;
157
+ try {
158
+ fs.renameSync(normalizedPath, consumedPath);
159
+ } catch {
160
+ return false;
161
+ }
162
+ try {
163
+ fs.rmSync(consumedPath, { force: true });
164
+ } catch {
165
+ // Best effort cleanup only.
166
+ }
167
+ return true;
168
+ }
169
+
170
+ function isValidTestBypassToken({ nonce, pid, ts, secret, token }) {
171
+ const normalizedNonce = String(nonce || "").trim();
172
+ const normalizedPid = Number(pid);
173
+ const normalizedTs = Number(ts);
174
+ const normalizedSecret = String(secret || "").trim();
175
+ const rawToken = String(token || "").trim();
176
+ if (
177
+ !normalizedNonce ||
178
+ !Number.isInteger(normalizedPid) ||
179
+ normalizedPid <= 0 ||
180
+ !Number.isFinite(normalizedTs) ||
181
+ normalizedTs <= 0 ||
182
+ !normalizedSecret ||
183
+ !rawToken
184
+ ) {
185
+ return false;
186
+ }
187
+ const normalizedToken = rawToken.replace(/^sha256:/i, "");
188
+ if (!/^[a-f0-9]{64}$/i.test(normalizedToken)) {
189
+ return false;
190
+ }
191
+ const message = `${normalizedNonce}|${normalizedPid}|${normalizedTs}`;
192
+ const computed = crypto.createHmac("sha256", normalizedSecret).update(message).digest("hex");
193
+ const expectedBuffer = Buffer.from(computed, "hex");
194
+ const candidateBuffer = Buffer.from(normalizedToken.toLowerCase(), "hex");
195
+ if (expectedBuffer.length !== candidateBuffer.length) {
196
+ return false;
197
+ }
198
+ return crypto.timingSafeEqual(expectedBuffer, candidateBuffer);
199
+ }
200
+
201
+ function isBypassCommandAllowed(args = []) {
202
+ const first = String(args[0] || "").trim().toLowerCase();
203
+ if (!first) {
204
+ return false;
205
+ }
206
+ if (first.startsWith("/")) {
207
+ return true;
208
+ }
209
+ if (!TEST_BYPASS_ALLOWED_COMMANDS.has(first)) {
210
+ return false;
211
+ }
212
+ for (const rawArg of args.slice(1)) {
213
+ const arg = String(rawArg || "").trim().toLowerCase();
214
+ if (!arg.startsWith("--")) {
215
+ continue;
216
+ }
217
+ const normalizedFlag = arg.split("=")[0];
218
+ if (TEST_BYPASS_BLOCKED_FLAGS.has(normalizedFlag)) {
219
+ return false;
220
+ }
221
+ }
222
+ return true;
223
+ }
224
+
225
+ function firstPositionalArg(args = [], startIndex = 0) {
226
+ for (const rawArg of args.slice(startIndex)) {
227
+ const normalized = String(rawArg || "").trim().toLowerCase();
228
+ if (!normalized || normalized.startsWith("-")) {
229
+ continue;
230
+ }
231
+ return normalized;
232
+ }
233
+ return "";
234
+ }
235
+
236
+ function isSessionNoAuthCommand(args = []) {
237
+ const first = String(args[0] || "").trim().toLowerCase();
238
+ if (first !== "session") {
239
+ return false;
240
+ }
241
+ const subcommand = firstPositionalArg(args, 1);
242
+ return SESSION_NO_AUTH_SUBCOMMANDS.has(subcommand);
243
+ }
244
+
245
+ function hasTrustedBypassExecutableContext() {
246
+ const argvPath = String(process.argv[1] || "").trim();
247
+ if (!argvPath) {
248
+ return false;
249
+ }
250
+ const executableName = path.basename(argvPath).toLowerCase();
251
+ if (!TEST_BYPASS_ALLOWED_EXECUTABLES.has(executableName)) {
252
+ return false;
253
+ }
254
+ const normalizedPath = argvPath.replace(/\\/g, "/").toLowerCase();
255
+ if (!normalizedPath.includes("/bin/") && !normalizedPath.endsWith("/src/cli.js")) {
256
+ return false;
257
+ }
258
+ return true;
259
+ }
30
260
 
31
- function hasTrustedBypassContext() {
32
- const nonce = String(process.env.SENTINELAYER_CLI_TEST_BYPASS_NONCE || "").trim();
33
- return (
34
- process.env.NODE_ENV === "test" &&
35
- process.env.SENTINELAYER_CLI_TEST_MODE === "1" &&
36
- nonce.length >= 12
37
- );
261
+ function hasTrustedBypassContext(args = []) {
262
+ if (isTruthy(process.env.CI)) {
263
+ return false;
264
+ }
265
+ if (process.env.NODE_ENV !== "test" || process.env.SENTINELAYER_CLI_TEST_MODE !== "1") {
266
+ return false;
267
+ }
268
+ if (isPackagedBuild()) {
269
+ return false;
270
+ }
271
+ if (!hasTrustedBypassExecutableContext()) {
272
+ return false;
273
+ }
274
+ if (!isBypassCommandAllowed(args)) {
275
+ return false;
276
+ }
277
+ const nonceEnvelope = readNonceEnvelope(process.env[TEST_BYPASS_NONCE_ENV]);
278
+ if (!nonceEnvelope) {
279
+ return false;
280
+ }
281
+ if (
282
+ !isValidTestBypassToken({
283
+ nonce: nonceEnvelope.nonce,
284
+ pid: nonceEnvelope.pid,
285
+ ts: nonceEnvelope.ts,
286
+ secret: process.env[TEST_BYPASS_SECRET_ENV],
287
+ token: process.env[TEST_BYPASS_TOKEN_ENV],
288
+ })
289
+ ) {
290
+ return false;
291
+ }
292
+ return consumeNonceEnvelope(nonceEnvelope.nonceFile);
38
293
  }
39
294
 
40
295
  function isValidSessionToken(session) {
@@ -50,7 +305,7 @@ function isValidSessionToken(session) {
50
305
  return false;
51
306
  }
52
307
  const tokenPrefix = String(session?.tokenPrefix || "").trim();
53
- if (tokenPrefix && !token.startsWith(tokenPrefix)) {
308
+ if (tokenPrefix && !token.includes(tokenPrefix)) {
54
309
  return false;
55
310
  }
56
311
  return true;
@@ -68,6 +323,19 @@ function isSessionUnexpired(tokenExpiresAt) {
68
323
  return expiresAt >= Date.now();
69
324
  }
70
325
 
326
+ function isAuthenticatedSessionValid(session) {
327
+ if (!isValidSessionToken(session)) {
328
+ return false;
329
+ }
330
+
331
+ // Persisted sessions must include a valid expiry bound. Env/config tokens
332
+ // are accepted as active auth sources and validated downstream by API calls.
333
+ if (String(session?.source || "").trim() === "session") {
334
+ return isSessionUnexpired(session?.tokenExpiresAt);
335
+ }
336
+ return true;
337
+ }
338
+
71
339
  /**
72
340
  * Check if the current command requires authentication.
73
341
  * Returns true if auth is required but user is not logged in.
@@ -78,7 +346,6 @@ function isSessionUnexpired(tokenExpiresAt) {
78
346
  export async function checkAuthGate(args) {
79
347
  const first = String(args[0] || "").trim().toLowerCase();
80
348
 
81
- // Bypass commands
82
349
  if (!first || AUTH_BYPASS_COMMANDS.has(first)) {
83
350
  return { authenticated: true, session: null, bypassReason: "auth_bypass_command" };
84
351
  }
@@ -87,15 +354,22 @@ export async function checkAuthGate(args) {
87
354
  return { authenticated: true, session: null, bypassReason: "no_auth_required" };
88
355
  }
89
356
 
90
- // Explicit bypass is gated to trusted test contexts only.
91
- if (process.env.SENTINELAYER_CLI_SKIP_AUTH === "1" && hasTrustedBypassContext()) {
357
+ if (isSessionNoAuthCommand(args)) {
358
+ return { authenticated: true, session: null, bypassReason: "session_no_auth_required" };
359
+ }
360
+
361
+ if (process.env.SENTINELAYER_CLI_SKIP_AUTH === "1" && hasTrustedBypassContext(args)) {
92
362
  return { authenticated: true, session: null, bypassReason: "env_bypass_guarded" };
93
363
  }
94
364
 
95
- // Check for stored session
365
+ // Check for active auth session across env -> config -> stored session.
96
366
  try {
97
- const session = await readStoredSession();
98
- if (session && isValidSessionToken(session) && isSessionUnexpired(session.tokenExpiresAt)) {
367
+ const session = await resolveActiveAuthSession({
368
+ cwd: process.cwd(),
369
+ env: process.env,
370
+ autoRotate: false,
371
+ });
372
+ if (session && isAuthenticatedSessionValid(session)) {
99
373
  return { authenticated: true, session, bypassReason: null };
100
374
  }
101
375
  } catch {
@@ -114,7 +388,7 @@ export function printAuthRequired() {
114
388
  console.error("");
115
389
  console.error(" Log in to SentinelLayer to use CLI commands:");
116
390
  console.error("");
117
- console.error(" " + pc.cyan("sl auth login"));
391
+ console.error(" " + pc.cyan(authLoginHint()));
118
392
  console.error("");
119
393
  console.error(" This opens your browser to authenticate via GitHub or Google.");
120
394
  console.error(" Your session is encrypted and stored locally.");