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.
- package/README.md +16 -18
- package/package.json +7 -6
- package/src/agents/jules/config/definition.js +13 -62
- package/src/agents/jules/config/system-prompt.js +8 -1
- package/src/agents/jules/fix-cycle.js +12 -372
- package/src/agents/jules/loop.js +116 -26
- package/src/agents/jules/pulse.js +10 -327
- package/src/agents/jules/stream.js +13 -12
- package/src/agents/jules/swarm/orchestrator.js +3 -3
- package/src/agents/jules/swarm/sub-agent.js +6 -3
- package/src/agents/jules/tools/aidenid-email.js +189 -0
- package/src/agents/jules/tools/auth-audit.js +1187 -45
- package/src/agents/jules/tools/dispatch.js +25 -12
- package/src/agents/jules/tools/file-edit.js +2 -180
- package/src/agents/jules/tools/file-read.js +2 -100
- package/src/agents/jules/tools/glob.js +2 -168
- package/src/agents/jules/tools/grep.js +2 -228
- package/src/agents/jules/tools/path-guards.js +2 -161
- package/src/agents/jules/tools/runtime-audit.js +6 -2
- package/src/agents/jules/tools/shell.js +2 -383
- package/src/agents/persona-visuals.js +64 -0
- package/src/agents/shared-tools/dispatch-core.js +320 -0
- package/src/agents/shared-tools/file-edit.js +180 -0
- package/src/agents/shared-tools/file-read.js +100 -0
- package/src/agents/shared-tools/glob.js +168 -0
- package/src/agents/shared-tools/grep.js +228 -0
- package/src/agents/shared-tools/index.js +46 -0
- package/src/agents/shared-tools/path-guards.js +161 -0
- package/src/agents/shared-tools/shell.js +383 -0
- package/src/ai/aidenid.js +56 -7
- package/src/ai/client.js +45 -0
- package/src/ai/proxy.js +137 -0
- package/src/auth/gate.js +290 -16
- package/src/auth/http.js +450 -39
- package/src/auth/service.js +262 -47
- package/src/auth/session-store.js +475 -21
- package/src/cli.js +5 -0
- package/src/commands/audit.js +13 -8
- package/src/commands/auth.js +53 -9
- package/src/commands/omargate.js +10 -2
- package/src/commands/scan.js +10 -4
- package/src/commands/session.js +590 -0
- package/src/commands/spec.js +62 -0
- package/src/commands/watch.js +3 -2
- package/src/daemon/assignment-ledger.js +196 -0
- package/src/daemon/error-worker.js +599 -16
- package/src/daemon/fix-cycle.js +384 -0
- package/src/daemon/ingest-refresh.js +10 -9
- package/src/daemon/jira-lifecycle.js +135 -0
- package/src/daemon/pulse.js +327 -0
- package/src/daemon/scope-engine.js +1068 -0
- package/src/events/schema.js +190 -0
- package/src/interactive/index.js +18 -16
- package/src/legacy-cli.js +606 -37
- package/src/prompt/generator.js +19 -1
- package/src/review/ai-review.js +11 -1
- package/src/review/local-review.js +75 -19
- package/src/review/omargate-interactive.js +68 -0
- package/src/review/omargate-orchestrator.js +404 -0
- package/src/review/persona-prompts.js +296 -0
- package/src/review/scan-modes.js +48 -0
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +352 -0
- package/src/session/daemon.js +801 -0
- package/src/session/paths.js +33 -0
- package/src/session/runtime-bridge.js +739 -0
- package/src/session/store.js +388 -0
- package/src/session/stream.js +325 -0
- package/src/spec/generator.js +100 -0
- package/src/telemetry/session-tracker.js +148 -32
- package/src/telemetry/sync.js +6 -2
- 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 {
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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.
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
98
|
-
|
|
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(
|
|
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.");
|