sentinelayer-cli 0.44.0 → 0.46.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 +10 -5
- package/package.json +1 -1
- package/src/agents/jules/tools/aidenid-email.js +7 -9
- package/src/agents/jules/tools/frontend-analyze.js +15 -3
- package/src/agents/jules/tools/runtime-audit.js +51 -21
- package/src/agents/shared-tools/shell.js +9 -0
- package/src/ai/aidenid.js +5 -4
- package/src/ai/proxy.js +15 -14
- package/src/audit/orchestrator.js +8 -5
- package/src/auth/admission-scope.js +5 -3
- package/src/auth/credential-destinations.js +221 -0
- package/src/auth/gate.js +4 -4
- package/src/auth/http.js +22 -5
- package/src/auth/service.js +52 -56
- package/src/cli.js +24 -6
- package/src/commands/ai/provision-governance.js +7 -6
- package/src/commands/init.js +25 -16
- package/src/commands/scan.js +6 -4
- package/src/commands/session.js +33 -45
- package/src/commands/spec.js +2 -2
- package/src/commands/watch.js +4 -3
- package/src/config/service.js +37 -0
- package/src/daemon/ast-parser-layer.js +8 -3
- package/src/daemon/callgraph-overlay.js +7 -2
- package/src/daemon/pulse.js +2 -1
- package/src/daemon/watchdog.js +117 -68
- package/src/legacy-cli.js +119 -39
- package/src/mcp/bridge-inputs.js +223 -0
- package/src/mcp/cli-command-tools.js +157 -7
- package/src/mcp/registry.js +2 -1
- package/src/mcp/session-stdio-server.js +114 -35
- package/src/mcp/smoke.js +27 -11
- package/src/mcp/token-service.js +7 -9
- package/src/memory/retrieval.js +14 -5
- package/src/pocket/ring-owner.js +13 -8
- package/src/program-lookup.js +54 -0
- package/src/review/ai-review.js +2 -2
- package/src/review/dd-report-email-client.js +4 -5
- package/src/review/local-review.js +11 -5
- package/src/session/admission-access.js +13 -10
- package/src/session/admission-auth.js +95 -16
- package/src/session/admission.js +44 -9
- package/src/session/checkpoints.js +11 -10
- package/src/session/file-locks.js +19 -18
- package/src/session/invitations.js +26 -19
- package/src/session/live-source.js +13 -13
- package/src/session/route-classes.js +107 -0
- package/src/session/sync.js +70 -56
- package/src/session/tickets.js +21 -19
- package/src/session/title-sync.js +4 -3
- package/src/telemetry/sync.js +8 -9
package/src/cli.js
CHANGED
|
@@ -2,6 +2,7 @@ import process from "node:process";
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
|
|
4
4
|
import { CLI_VERSION, runLegacyCliWithErrorHandling } from "./legacy-cli.js";
|
|
5
|
+
import { restrictProgramLookupToPath } from "./program-lookup.js";
|
|
5
6
|
|
|
6
7
|
const COMMAND_REGISTRARS = {
|
|
7
8
|
init: {
|
|
@@ -242,6 +243,20 @@ export async function buildCliProgram({
|
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
246
|
+
// Programs named without a path come from PATH only, never from the working directory, before
|
|
247
|
+
// anything is spawned. On a Windows runtime that cannot guarantee that, nothing runs.
|
|
248
|
+
const programLookup = restrictProgramLookupToPath();
|
|
249
|
+
if (!programLookup.ok) {
|
|
250
|
+
console.error(programLookup.message);
|
|
251
|
+
process.exitCode = 1;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Where the user's credentials may go is decided once, here, from the real environment and the
|
|
256
|
+
// global config, and frozen: nothing that changes process.env later can move it.
|
|
257
|
+
const { freezeProcessTrustContext } = await import("./auth/credential-destinations.js");
|
|
258
|
+
await freezeProcessTrustContext();
|
|
259
|
+
|
|
245
260
|
// Normalize slash commands (/omargate → omargate, /audit → audit, etc.)
|
|
246
261
|
const normalizedArgs = normalizeSlashArgs(rawArgs);
|
|
247
262
|
|
|
@@ -269,15 +284,18 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
269
284
|
// the subcommand (say, post-agent, observe, locks, checkpoints, ...), so no
|
|
270
285
|
// subcommand, present or future, can reach the human token. A tombstoned
|
|
271
286
|
// admission refuses here, before any request.
|
|
272
|
-
const { assertDispatchMatchesScope, resolveAgentAdmissionTarget, withAgentAdmission } =
|
|
273
|
-
"./session/admission-auth.js"
|
|
274
|
-
);
|
|
287
|
+
const { assertDispatchMatchesScope, resolveAgentAdmissionTarget, withAgentAdmission } =
|
|
288
|
+
await import("./session/admission-auth.js");
|
|
275
289
|
const target = await resolveAgentAdmissionTarget(normalizedArgs);
|
|
276
|
-
// Execution must match authorization: before ANY action runs,
|
|
277
|
-
//
|
|
290
|
+
// Execution must match authorization: before ANY action runs, decide on Commander's parsed
|
|
291
|
+
// arguments and options -- the values the action will use. Through the MCP CLI bridge in an
|
|
292
|
+
// agent context the action binds to its session's admission, runs a legacy data-plane route
|
|
293
|
+
// as before, or is refused (src/session/route-classes.js); and the identity it resolves must
|
|
294
|
+
// be the one whose scope is in force.
|
|
278
295
|
program.hook("preAction", (_hooked, actionCommand) => assertDispatchMatchesScope(actionCommand));
|
|
279
296
|
if (target) {
|
|
280
|
-
|
|
297
|
+
// An admission seen above that is gone by the time it is read refuses: never a fallback.
|
|
298
|
+
await withAgentAdmission(target.sessionId, target.agentId, dispatch, { requireStored: true });
|
|
281
299
|
return;
|
|
282
300
|
}
|
|
283
301
|
await dispatch();
|
|
@@ -25,8 +25,7 @@ import {
|
|
|
25
25
|
import { listSites, recordTemporarySite } from "../../ai/site-store.js";
|
|
26
26
|
import { getIdentityById, recordProvisionedIdentity } from "../../ai/identity-store.js";
|
|
27
27
|
import { resolveOutputRoot } from "../../config/service.js";
|
|
28
|
-
import {
|
|
29
|
-
import { fetchAidenIdCredentials } from "../../auth/service.js";
|
|
28
|
+
import { fetchAidenIdCredentials, resolveActiveAuthSession } from "../../auth/service.js";
|
|
30
29
|
import {
|
|
31
30
|
buildCurlPreview,
|
|
32
31
|
normalizeIdempotencyKey,
|
|
@@ -37,6 +36,7 @@ import {
|
|
|
37
36
|
stableTimestampForFile,
|
|
38
37
|
writeArtifact,
|
|
39
38
|
} from "./shared.js";
|
|
39
|
+
import { isAuthenticated } from "../../auth/credential-destinations.js";
|
|
40
40
|
|
|
41
41
|
function addProvisionEmailOptions(cmd) {
|
|
42
42
|
return cmd
|
|
@@ -92,12 +92,13 @@ async function provisionEmailAction(options, command) {
|
|
|
92
92
|
payload,
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
const session = await resolveActiveAuthSession({ cwd: process.cwd(), env: process.env, autoRotate: false }).catch(
|
|
96
|
+
() => null,
|
|
97
|
+
);
|
|
97
98
|
|
|
98
99
|
const makeFetcher = () => {
|
|
99
|
-
if (!session
|
|
100
|
-
return () => fetchAidenIdCredentials({ apiUrl: session.apiUrl,
|
|
100
|
+
if (!isAuthenticated(session)) return null;
|
|
101
|
+
return () => fetchAidenIdCredentials({ apiUrl: session.apiUrl, auth: session });
|
|
101
102
|
};
|
|
102
103
|
|
|
103
104
|
const resolvedCredentials = await resolveAidenIdCredentials({
|
package/src/commands/init.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
|
|
3
|
-
import { loadConfig } from "../config/service.js";
|
|
3
|
+
import { loadConfig, userConfigValues } from "../config/service.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
// Config values the scaffold flow reads from the environment. The API it talks to and the token
|
|
6
|
+
// it sends are never set here: they come from the trust context and the auth service
|
|
7
|
+
// (src/auth/credential-destinations.js), so a workspace .sentinelayer.yml cannot choose them,
|
|
8
|
+
// and child processes inherit neither.
|
|
9
|
+
// Provider keys come from the user's own config (global, or the environment), never the workspace.
|
|
10
|
+
function applyConfigEnvDefaults(config) {
|
|
11
|
+
const resolvedConfig = config.resolved;
|
|
12
|
+
const userValues = userConfigValues(config);
|
|
9
13
|
if (!process.env.SENTINELAYER_WEB_URL && resolvedConfig.webUrl) {
|
|
10
|
-
process.env.SENTINELAYER_WEB_URL = resolvedConfig.webUrl;
|
|
11
|
-
}
|
|
12
|
-
if (!process.env.SENTINELAYER_TOKEN && resolvedConfig.sentinelayerToken) {
|
|
13
|
-
process.env.SENTINELAYER_TOKEN = resolvedConfig.sentinelayerToken;
|
|
14
|
+
process.env.SENTINELAYER_WEB_URL = resolvedConfig.webUrl; // printed as a link only
|
|
14
15
|
}
|
|
15
|
-
if (!process.env.OPENAI_API_KEY &&
|
|
16
|
-
process.env.OPENAI_API_KEY =
|
|
16
|
+
if (!process.env.OPENAI_API_KEY && userValues.openaiApiKey) {
|
|
17
|
+
process.env.OPENAI_API_KEY = userValues.openaiApiKey;
|
|
17
18
|
}
|
|
18
|
-
if (!process.env.ANTHROPIC_API_KEY &&
|
|
19
|
-
process.env.ANTHROPIC_API_KEY =
|
|
19
|
+
if (!process.env.ANTHROPIC_API_KEY && userValues.anthropicApiKey) {
|
|
20
|
+
process.env.ANTHROPIC_API_KEY = userValues.anthropicApiKey;
|
|
20
21
|
}
|
|
21
|
-
if (!process.env.GOOGLE_API_KEY &&
|
|
22
|
-
process.env.GOOGLE_API_KEY =
|
|
22
|
+
if (!process.env.GOOGLE_API_KEY && userValues.googleApiKey) {
|
|
23
|
+
process.env.GOOGLE_API_KEY = userValues.googleApiKey;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -30,9 +31,11 @@ export function registerInitCommand(program, invokeLegacy) {
|
|
|
30
31
|
.option("--non-interactive", "Disable prompts and require interview payload")
|
|
31
32
|
.option("--interview-file <path>", "Load interview JSON from file")
|
|
32
33
|
.option("--skip-browser-open", "Do not auto-open browser during auth")
|
|
34
|
+
.option("--inject-secret", "Set the project token as a GitHub Actions secret on this directory's git remote")
|
|
35
|
+
.option("--inject-openai-key", "Also set OPENAI_API_KEY from your environment as a GitHub Actions secret there")
|
|
33
36
|
.action(async (projectName, options) => {
|
|
34
37
|
const config = await loadConfig();
|
|
35
|
-
applyConfigEnvDefaults(config
|
|
38
|
+
applyConfigEnvDefaults(config);
|
|
36
39
|
|
|
37
40
|
const legacyArgs = [];
|
|
38
41
|
const normalizedProjectName = String(projectName || "").trim();
|
|
@@ -49,6 +52,12 @@ export function registerInitCommand(program, invokeLegacy) {
|
|
|
49
52
|
if (options.skipBrowserOpen) {
|
|
50
53
|
legacyArgs.push("--skip-browser-open");
|
|
51
54
|
}
|
|
55
|
+
if (options.injectSecret) {
|
|
56
|
+
legacyArgs.push("--inject-secret");
|
|
57
|
+
}
|
|
58
|
+
if (options.injectOpenaiKey) {
|
|
59
|
+
legacyArgs.push("--inject-openai-key");
|
|
60
|
+
}
|
|
52
61
|
|
|
53
62
|
await invokeLegacy(legacyArgs);
|
|
54
63
|
});
|
package/src/commands/scan.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
resolveProvider,
|
|
13
13
|
} from "../ai/client.js";
|
|
14
14
|
import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
|
|
15
|
-
import { loadConfig, resolveOutputRoot } from "../config/service.js";
|
|
15
|
+
import { loadConfig, resolveOutputRoot, userConfigValues } from "../config/service.js";
|
|
16
16
|
import { evaluateBudget } from "../cost/budget.js";
|
|
17
17
|
import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
|
|
18
18
|
import { estimateModelCost } from "../cost/tracker.js";
|
|
@@ -34,6 +34,7 @@ import { detectRepoSlug, setupSecrets } from "../scan/gh-secrets.js";
|
|
|
34
34
|
import { appendRunEvent, deriveStopClassFromBudget } from "../telemetry/ledger.js";
|
|
35
35
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
36
36
|
import { authLoginHint } from "../ui/command-hints.js";
|
|
37
|
+
import { exportCredentialToken, isAuthenticated } from "../auth/credential-destinations.js";
|
|
37
38
|
|
|
38
39
|
const LEGACY_SCAN_WORKFLOW_PATH = ".github/workflows/security-review.yml";
|
|
39
40
|
|
|
@@ -578,7 +579,7 @@ export function registerScanCommand(program) {
|
|
|
578
579
|
configModel: config.resolved.defaultModelId,
|
|
579
580
|
});
|
|
580
581
|
const explicitApiKey = String(options.apiKey || "").trim();
|
|
581
|
-
const configuredApiKey = resolveConfiguredApiKey(resolvedProvider, config
|
|
582
|
+
const configuredApiKey = resolveConfiguredApiKey(resolvedProvider, userConfigValues(config));
|
|
582
583
|
|
|
583
584
|
const prompt = buildAiPreScanPrompt({
|
|
584
585
|
targetPath,
|
|
@@ -856,8 +857,9 @@ export function registerScanCommand(program) {
|
|
|
856
857
|
env: process.env,
|
|
857
858
|
autoRotate: false,
|
|
858
859
|
});
|
|
859
|
-
if (session
|
|
860
|
-
|
|
860
|
+
if (isAuthenticated(session)) {
|
|
861
|
+
// the one reviewed export of the token: to a GitHub Actions secret, through gh
|
|
862
|
+
tokenValue = exportCredentialToken(session.credential, { purpose: "github-actions-secret" });
|
|
861
863
|
}
|
|
862
864
|
} catch {
|
|
863
865
|
/* no active auth session */
|
package/src/commands/session.js
CHANGED
|
@@ -7,6 +7,7 @@ import { spawn as defaultSpawn } from "node:child_process";
|
|
|
7
7
|
|
|
8
8
|
import pc from "picocolors";
|
|
9
9
|
|
|
10
|
+
import { credentialFor, isAuthenticated } from "../auth/credential-destinations.js";
|
|
10
11
|
import { SentinelayerApiError, requestJsonMutation } from "../auth/http.js";
|
|
11
12
|
import {
|
|
12
13
|
buildProvisionEmailPayload,
|
|
@@ -15,7 +16,6 @@ import {
|
|
|
15
16
|
resolveAidenIdCredentials,
|
|
16
17
|
} from "../ai/aidenid.js";
|
|
17
18
|
import { recordProvisionedIdentity } from "../ai/identity-store.js";
|
|
18
|
-
import { readStoredSession } from "../auth/session-store.js";
|
|
19
19
|
import { fetchAidenIdCredentials } from "../auth/service.js";
|
|
20
20
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
21
21
|
import { resolveOutputRoot } from "../config/service.js";
|
|
@@ -1322,19 +1322,11 @@ async function resolveSessionRemoteSyncState({ dashboardUrl } = {}) {
|
|
|
1322
1322
|
};
|
|
1323
1323
|
}
|
|
1324
1324
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
storedSession = await readStoredSession();
|
|
1328
|
-
} catch {
|
|
1329
|
-
storedSession = null;
|
|
1330
|
-
}
|
|
1331
|
-
const apiUrl =
|
|
1332
|
-
normalizeString(storedSession?.apiUrl) ||
|
|
1333
|
-
normalizeString(process.env.SENTINELAYER_API_URL) ||
|
|
1334
|
-
"https://api.sentinelayer.com";
|
|
1335
|
-
const hasToken = Boolean(
|
|
1336
|
-
normalizeString(storedSession?.token) || normalizeString(process.env.SENTINELAYER_TOKEN),
|
|
1325
|
+
const auth = await resolveActiveAuthSession({ cwd: process.cwd(), env: process.env, autoRotate: false }).catch(
|
|
1326
|
+
() => null,
|
|
1337
1327
|
);
|
|
1328
|
+
const apiUrl = normalizeString(auth?.apiUrl) || "https://api.sentinelayer.com";
|
|
1329
|
+
const hasToken = isAuthenticated(auth);
|
|
1338
1330
|
if (!hasToken) {
|
|
1339
1331
|
return {
|
|
1340
1332
|
status: "auth_required",
|
|
@@ -1458,12 +1450,12 @@ async function findReusableSessionCandidate({
|
|
|
1458
1450
|
// - When `SENTINELAYER_SKIP_REMOTE_SYNC=1` (test bootstrap), short-circuits
|
|
1459
1451
|
// to `{ ok: true, source: "skipped", session: null }` so unit tests
|
|
1460
1452
|
// can exercise the local materialization path without a real API.
|
|
1461
|
-
async function fetchRemoteSessionDetail(endpoint,
|
|
1453
|
+
async function fetchRemoteSessionDetail(endpoint, credential) {
|
|
1462
1454
|
let response;
|
|
1463
1455
|
let payload;
|
|
1464
1456
|
try {
|
|
1465
1457
|
({ response, payload } = await fetchJsonWithFullTimeout(
|
|
1466
|
-
endpoint, { method: "GET",
|
|
1458
|
+
endpoint, { method: "GET", credential, redirect: "error" }, 2_000,
|
|
1467
1459
|
));
|
|
1468
1460
|
} catch (err) {
|
|
1469
1461
|
return {
|
|
@@ -1516,7 +1508,7 @@ async function verifyRemoteSession(sessionId, { targetPath } = {}) {
|
|
|
1516
1508
|
} catch {
|
|
1517
1509
|
return { ok: false, reason: "no_session" };
|
|
1518
1510
|
}
|
|
1519
|
-
if (!auth
|
|
1511
|
+
if (!isAuthenticated(auth)) {
|
|
1520
1512
|
return { ok: false, reason: "not_authenticated", status: 401 };
|
|
1521
1513
|
}
|
|
1522
1514
|
const apiUrl = String(auth.apiUrl || "").replace(/\/+$/, "");
|
|
@@ -1524,10 +1516,10 @@ async function verifyRemoteSession(sessionId, { targetPath } = {}) {
|
|
|
1524
1516
|
return { ok: false, reason: "no_api_url" };
|
|
1525
1517
|
}
|
|
1526
1518
|
const endpoint = `${apiUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}`;
|
|
1527
|
-
const
|
|
1528
|
-
const firstAttempt = await fetchRemoteSessionDetail(endpoint,
|
|
1519
|
+
const credential = await credentialFor(auth);
|
|
1520
|
+
const firstAttempt = await fetchRemoteSessionDetail(endpoint, credential);
|
|
1529
1521
|
const detail = firstAttempt.retryable
|
|
1530
|
-
? await fetchRemoteSessionDetail(endpoint,
|
|
1522
|
+
? await fetchRemoteSessionDetail(endpoint, credential)
|
|
1531
1523
|
: firstAttempt;
|
|
1532
1524
|
if (detail.ok) {
|
|
1533
1525
|
return {
|
|
@@ -2782,7 +2774,7 @@ async function resolveAdminApiSession({ targetPath, explicitApiUrl }) {
|
|
|
2782
2774
|
explicitApiUrl,
|
|
2783
2775
|
autoRotate: true,
|
|
2784
2776
|
});
|
|
2785
|
-
if (!session
|
|
2777
|
+
if (!isAuthenticated(session)) {
|
|
2786
2778
|
throw new Error(`No active auth token found. Run \`${authLoginHint()}\` first.`);
|
|
2787
2779
|
}
|
|
2788
2780
|
return session;
|
|
@@ -2802,10 +2794,8 @@ async function postAdminSessionMutation({
|
|
|
2802
2794
|
return requestJsonMutation(`${apiUrl}${pathSuffix}`, {
|
|
2803
2795
|
method: "POST",
|
|
2804
2796
|
operationName,
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
...headers,
|
|
2808
|
-
},
|
|
2797
|
+
credential: await credentialFor(session),
|
|
2798
|
+
headers,
|
|
2809
2799
|
body,
|
|
2810
2800
|
});
|
|
2811
2801
|
}
|
|
@@ -3306,7 +3296,7 @@ export function registerSessionCommand(program) {
|
|
|
3306
3296
|
env: process.env,
|
|
3307
3297
|
autoRotate: false,
|
|
3308
3298
|
});
|
|
3309
|
-
if (!session
|
|
3299
|
+
if (!isAuthenticated(session) || !session?.apiUrl) {
|
|
3310
3300
|
throw new Error(`Not authenticated. Run \`${authLoginHint()}\` first.`);
|
|
3311
3301
|
}
|
|
3312
3302
|
const apiUrl = String(session.apiUrl).replace(/\/+$/, "");
|
|
@@ -3315,7 +3305,7 @@ export function registerSessionCommand(program) {
|
|
|
3315
3305
|
{
|
|
3316
3306
|
method: "POST",
|
|
3317
3307
|
operationName: "session.set_title",
|
|
3318
|
-
|
|
3308
|
+
credential: session.credential,
|
|
3319
3309
|
body: { title: normalizedTitle },
|
|
3320
3310
|
},
|
|
3321
3311
|
);
|
|
@@ -3356,7 +3346,7 @@ export function registerSessionCommand(program) {
|
|
|
3356
3346
|
env: process.env,
|
|
3357
3347
|
autoRotate: false,
|
|
3358
3348
|
});
|
|
3359
|
-
if (!session
|
|
3349
|
+
if (!isAuthenticated(session) || !session?.apiUrl) {
|
|
3360
3350
|
throw new Error(`Not authenticated. Run \`${authLoginHint()}\` first.`);
|
|
3361
3351
|
}
|
|
3362
3352
|
const apiUrl = String(session.apiUrl).replace(/\/+$/, "");
|
|
@@ -3365,7 +3355,7 @@ export function registerSessionCommand(program) {
|
|
|
3365
3355
|
{
|
|
3366
3356
|
method: "POST",
|
|
3367
3357
|
operationName: "session.sweep_empty",
|
|
3368
|
-
|
|
3358
|
+
credential: session.credential,
|
|
3369
3359
|
body: {
|
|
3370
3360
|
cutoffMinutes,
|
|
3371
3361
|
maxEvents,
|
|
@@ -6400,7 +6390,7 @@ export function registerSessionCommand(program) {
|
|
|
6400
6390
|
env: process.env,
|
|
6401
6391
|
autoRotate: false,
|
|
6402
6392
|
});
|
|
6403
|
-
if (!authSession
|
|
6393
|
+
if (!isAuthenticated(authSession)) {
|
|
6404
6394
|
throw new Error(`Remote session read requires authentication. Run \`${authLoginHint()}\` first.`);
|
|
6405
6395
|
}
|
|
6406
6396
|
hydration = await hydrateSessionFromRemote({
|
|
@@ -6674,13 +6664,13 @@ export function registerSessionCommand(program) {
|
|
|
6674
6664
|
autoRotate: false,
|
|
6675
6665
|
}).catch(() => null);
|
|
6676
6666
|
const apiBaseUrl = session?.apiUrl || "";
|
|
6677
|
-
const
|
|
6667
|
+
const credential = session?.credential || null;
|
|
6678
6668
|
try {
|
|
6679
6669
|
for await (const item of mergeLiveSources({
|
|
6680
6670
|
sessionId: normalizedSessionId,
|
|
6681
6671
|
targetPath,
|
|
6682
6672
|
apiBaseUrl: apiBaseUrl || undefined,
|
|
6683
|
-
|
|
6673
|
+
credential: credential || undefined,
|
|
6684
6674
|
signal: ac.signal,
|
|
6685
6675
|
})) {
|
|
6686
6676
|
if (item.event) {
|
|
@@ -8603,21 +8593,19 @@ export function registerSessionCommand(program) {
|
|
|
8603
8593
|
return;
|
|
8604
8594
|
}
|
|
8605
8595
|
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
}
|
|
8596
|
+
const storedSession = await resolveActiveAuthSession({
|
|
8597
|
+
cwd: process.cwd(),
|
|
8598
|
+
env: process.env,
|
|
8599
|
+
autoRotate: false,
|
|
8600
|
+
}).catch(() => null);
|
|
8612
8601
|
|
|
8613
|
-
const fetchCredentials =
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
: null;
|
|
8602
|
+
const fetchCredentials = isAuthenticated(storedSession)
|
|
8603
|
+
? () =>
|
|
8604
|
+
fetchAidenIdCredentials({
|
|
8605
|
+
apiUrl: storedSession.apiUrl,
|
|
8606
|
+
auth: storedSession,
|
|
8607
|
+
})
|
|
8608
|
+
: null;
|
|
8621
8609
|
const credentials = await resolveAidenIdCredentials({
|
|
8622
8610
|
apiKey: options.apiKey,
|
|
8623
8611
|
orgId: options.orgId,
|
package/src/commands/spec.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
resolveProvider,
|
|
11
11
|
} from "../ai/client.js";
|
|
12
12
|
import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
|
|
13
|
-
import { loadConfig } from "../config/service.js";
|
|
13
|
+
import { loadConfig, userConfigValues } from "../config/service.js";
|
|
14
14
|
import { evaluateBudget } from "../cost/budget.js";
|
|
15
15
|
import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
|
|
16
16
|
import { estimateModelCost } from "../cost/tracker.js";
|
|
@@ -265,7 +265,7 @@ async function maybeEnhanceSpecWithAi({
|
|
|
265
265
|
configModel: config.resolved.defaultModelId,
|
|
266
266
|
});
|
|
267
267
|
const explicitApiKey = String(options.apiKey || "").trim();
|
|
268
|
-
const configuredApiKey = resolveConfiguredApiKey(resolvedProvider, config
|
|
268
|
+
const configuredApiKey = resolveConfiguredApiKey(resolvedProvider, userConfigValues(config));
|
|
269
269
|
|
|
270
270
|
const prompt = buildAiSpecPrompt({
|
|
271
271
|
baseSpecMarkdown,
|
package/src/commands/watch.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../auth/service.js";
|
|
14
14
|
import { resolveOutputRoot } from "../config/service.js";
|
|
15
15
|
import { authLoginHint } from "../ui/command-hints.js";
|
|
16
|
+
import { isAuthenticated } from "../auth/credential-destinations.js";
|
|
16
17
|
|
|
17
18
|
const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "cancelled"]);
|
|
18
19
|
|
|
@@ -369,7 +370,7 @@ export function registerWatchCommand(program) {
|
|
|
369
370
|
throw new Error(formatApiError(error));
|
|
370
371
|
}
|
|
371
372
|
|
|
372
|
-
if (!session
|
|
373
|
+
if (!isAuthenticated(session)) {
|
|
373
374
|
throw new Error(`No active auth token found. Run \`${authLoginHint()}\` first.`);
|
|
374
375
|
}
|
|
375
376
|
|
|
@@ -395,7 +396,7 @@ export function registerWatchCommand(program) {
|
|
|
395
396
|
while (true) {
|
|
396
397
|
const response = await listRuntimeRunEvents({
|
|
397
398
|
apiUrl: session.apiUrl,
|
|
398
|
-
|
|
399
|
+
credential: session.credential,
|
|
399
400
|
runId,
|
|
400
401
|
afterEventId,
|
|
401
402
|
});
|
|
@@ -428,7 +429,7 @@ export function registerWatchCommand(program) {
|
|
|
428
429
|
|
|
429
430
|
const statusResponse = await getRuntimeRunStatus({
|
|
430
431
|
apiUrl: session.apiUrl,
|
|
431
|
-
|
|
432
|
+
credential: session.credential,
|
|
432
433
|
runId,
|
|
433
434
|
});
|
|
434
435
|
latestStatus = String(statusResponse?.status || "unknown").trim().toLowerCase();
|
package/src/config/service.js
CHANGED
|
@@ -67,12 +67,49 @@ function normalizeLayerScope(scope, { allowResolved = true } = {}) {
|
|
|
67
67
|
return normalized;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Values a workspace .sentinelayer.yml may not set for the user: where the user's token goes, the
|
|
71
|
+
// token itself, provider keys, and where alerts are sent. They come from the environment (where a
|
|
72
|
+
// variable is named) or the global config only.
|
|
73
|
+
const USER_ONLY_KEYS = Object.freeze({
|
|
74
|
+
apiUrl: "SENTINELAYER_API_URL",
|
|
75
|
+
sentinelayerToken: "SENTINELAYER_TOKEN",
|
|
76
|
+
openaiApiKey: "OPENAI_API_KEY",
|
|
77
|
+
anthropicApiKey: "ANTHROPIC_API_KEY",
|
|
78
|
+
googleApiKey: "GOOGLE_API_KEY",
|
|
79
|
+
"alerts.channels": "",
|
|
80
|
+
});
|
|
81
|
+
const noticedProjectFiles = new Set();
|
|
82
|
+
|
|
83
|
+
function workspaceSets(projectConfig, key) {
|
|
84
|
+
const [head, field] = key.split(".");
|
|
85
|
+
const value = projectConfig?.[head];
|
|
86
|
+
return field ? value?.[field] !== undefined : value !== undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// One notice per workspace config that sets any of them, so an ignored value is never silent.
|
|
90
|
+
function noticeIgnoredWorkspaceValues(projectPath, projectConfig) {
|
|
91
|
+
const keys = Object.keys(USER_ONLY_KEYS).filter((key) => workspaceSets(projectConfig, key));
|
|
92
|
+
if (keys.length === 0 || noticedProjectFiles.has(projectPath)) return;
|
|
93
|
+
noticedProjectFiles.add(projectPath);
|
|
94
|
+
const envs = keys.map((key) => USER_ONLY_KEYS[key]).filter(Boolean);
|
|
95
|
+
const instead = envs.length > 0 ? `set ${envs.join(", ")} or the global config` : "set them in the global config";
|
|
96
|
+
process.stderr.write(
|
|
97
|
+
`NOTICE: workspace .sentinelayer.yml ${keys.join(", ")} ignored; ${instead} (~/.sentinelayer/config.yml).\n`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The user's own config values (global, then environment), never the workspace's. */
|
|
102
|
+
export function userConfigValues(config) {
|
|
103
|
+
return { ...(config?.layers?.global || {}), ...(config?.layers?.env || {}) };
|
|
104
|
+
}
|
|
105
|
+
|
|
70
106
|
export async function loadConfig({ cwd = process.cwd(), env = process.env, homeDir } = {}) {
|
|
71
107
|
const paths = getConfigPaths({ cwd, homeDir });
|
|
72
108
|
const [globalConfig, projectConfig] = await Promise.all([
|
|
73
109
|
readConfigFile(paths.global),
|
|
74
110
|
readConfigFile(paths.project),
|
|
75
111
|
]);
|
|
112
|
+
noticeIgnoredWorkspaceValues(paths.project, projectConfig);
|
|
76
113
|
|
|
77
114
|
const envConfig = buildEnvLayer(env);
|
|
78
115
|
const resolved = configSchema.parse(mergeLayers(globalConfig, projectConfig, envConfig));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { extname, resolve as resolvePath } from "node:path";
|
|
3
4
|
import { promisify } from "node:util";
|
|
4
5
|
|
|
5
6
|
import { parse } from "@babel/parser";
|
|
@@ -149,7 +150,7 @@ async function resolvePythonExecutable() {
|
|
|
149
150
|
pythonExecutablePromise = (async () => {
|
|
150
151
|
for (const executable of PYTHON_EXECUTABLE_CANDIDATES) {
|
|
151
152
|
try {
|
|
152
|
-
await execFileAsync(executable, ["--version"], { timeout: 5000 });
|
|
153
|
+
await execFileAsync(executable, ["--version"], { timeout: 5000, cwd: os.tmpdir() });
|
|
153
154
|
return executable;
|
|
154
155
|
} catch {
|
|
155
156
|
continue;
|
|
@@ -193,9 +194,13 @@ async function parsePythonAstSpecifiers(absolutePath = "") {
|
|
|
193
194
|
};
|
|
194
195
|
}
|
|
195
196
|
try {
|
|
196
|
-
|
|
197
|
+
// Isolated (-I: no script or working directory on sys.path, no PYTHON* variables) and run from
|
|
198
|
+
// a neutral directory, so a repository's own ast.py or json.py is never imported; the file to
|
|
199
|
+
// parse is passed as an argument.
|
|
200
|
+
const { stdout } = await execFileAsync(executable, ["-I", "-c", PYTHON_AST_SCRIPT, resolvePath(absolutePath)], {
|
|
197
201
|
timeout: 10000,
|
|
198
202
|
maxBuffer: 1024 * 1024,
|
|
203
|
+
cwd: os.tmpdir(),
|
|
199
204
|
});
|
|
200
205
|
const parsed = JSON.parse(String(stdout || "{}"));
|
|
201
206
|
const specifiers = Array.isArray(parsed.specifiers) ? parsed.specifiers : [];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path, { extname } from "node:path";
|
|
4
5
|
import { promisify } from "node:util";
|
|
5
6
|
|
|
@@ -370,7 +371,7 @@ async function resolvePythonExecutable() {
|
|
|
370
371
|
pythonExecutablePromise = (async () => {
|
|
371
372
|
for (const executable of PYTHON_EXECUTABLE_CANDIDATES) {
|
|
372
373
|
try {
|
|
373
|
-
await execFileAsync(executable, ["--version"], { timeout: 5000 });
|
|
374
|
+
await execFileAsync(executable, ["--version"], { timeout: 5000, cwd: os.tmpdir() });
|
|
374
375
|
return executable;
|
|
375
376
|
} catch {
|
|
376
377
|
continue;
|
|
@@ -454,9 +455,13 @@ async function parsePythonCallgraph(absolutePath = "", content = "", normalizedL
|
|
|
454
455
|
};
|
|
455
456
|
}
|
|
456
457
|
try {
|
|
457
|
-
|
|
458
|
+
// Isolated (-I: no script or working directory on sys.path, no PYTHON* variables) and run from
|
|
459
|
+
// a neutral directory, so a repository's own ast.py or json.py is never imported; the file to
|
|
460
|
+
// parse is passed as an argument.
|
|
461
|
+
const { stdout } = await execFileAsync(executable, ["-I", "-c", PYTHON_CALLGRAPH_SCRIPT, path.resolve(absolutePath)], {
|
|
458
462
|
timeout: 15000,
|
|
459
463
|
maxBuffer: 1024 * 1024,
|
|
464
|
+
cwd: os.tmpdir(),
|
|
460
465
|
});
|
|
461
466
|
const parsed = JSON.parse(String(stdout || "{}"));
|
|
462
467
|
return {
|
package/src/daemon/pulse.js
CHANGED
|
@@ -226,7 +226,8 @@ export function buildHealthSummary(agentStates) {
|
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
228
|
* Send an alert to configured Slack/Telegram webhooks.
|
|
229
|
-
*
|
|
229
|
+
* Channels come from the caller or the environment (SENTINELAYER_SLACK_WEBHOOK_URL,
|
|
230
|
+
* SENTINELAYER_TELEGRAM_BOT_TOKEN, SENTINELAYER_TELEGRAM_CHAT_ID), never a workspace file.
|
|
230
231
|
* Fails silently — alert delivery must never block agent work.
|
|
231
232
|
*
|
|
232
233
|
* @param {object} alert - { headline, body, severity } from buildAlertPayload
|