sentinelayer-cli 0.44.0 → 0.45.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 +8 -4
- 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/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 +14 -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/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/review/ai-review.js +2 -2
- package/src/review/dd-report-email-client.js +4 -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/auth/gate.js
CHANGED
|
@@ -7,6 +7,7 @@ import pc from "picocolors";
|
|
|
7
7
|
import { resolveActiveAuthSession } from "./service.js";
|
|
8
8
|
import { resolveAgentAdmissionTarget } from "../session/admission-auth.js";
|
|
9
9
|
import { authLoginHint } from "../ui/command-hints.js";
|
|
10
|
+
import { isAuthenticated } from "./credential-destinations.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Auth gate — ensures user is logged in before running any command.
|
|
@@ -316,12 +317,11 @@ function isSessionUnexpired(tokenExpiresAt) {
|
|
|
316
317
|
// without fixing anything.
|
|
317
318
|
//
|
|
318
319
|
// So the gate checks:
|
|
319
|
-
// - session
|
|
320
|
+
// - the session carries a credential
|
|
320
321
|
// - for source === "session", expiry is in the future
|
|
321
322
|
// - for source === "env" or "config", the downstream API call is the gate
|
|
322
323
|
function isAuthenticatedSessionValid(session) {
|
|
323
|
-
|
|
324
|
-
if (!token) {
|
|
324
|
+
if (!isAuthenticated(session)) {
|
|
325
325
|
return false;
|
|
326
326
|
}
|
|
327
327
|
if (String(session?.source || "").trim() === "session") {
|
|
@@ -369,7 +369,7 @@ export async function checkAuthGate(args) {
|
|
|
369
369
|
}
|
|
370
370
|
if (session) {
|
|
371
371
|
// Session resolved but failed validation (empty token or expired).
|
|
372
|
-
const tokenPresent =
|
|
372
|
+
const tokenPresent = isAuthenticated(session);
|
|
373
373
|
if (!tokenPresent) {
|
|
374
374
|
resolveError = "session_token_missing";
|
|
375
375
|
} else if (String(session?.source || "").trim() === "session" && !isSessionUnexpired(session?.tokenExpiresAt)) {
|
package/src/auth/http.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { installTestEgressGuard } from "../net/test-egress-guard.js";
|
|
7
|
+
import { CredentialDestinationRefused, credentialedRequest, isCredential } from "./credential-destinations.js";
|
|
7
8
|
|
|
8
9
|
// No-op outside tests; see src/net/test-egress-guard.js.
|
|
9
10
|
installTestEgressGuard();
|
|
@@ -20,7 +21,10 @@ export const CIRCUIT_BREAKER_THRESHOLD = 5;
|
|
|
20
21
|
export const CIRCUIT_BREAKER_COOLDOWN_MS = 30_000;
|
|
21
22
|
|
|
22
23
|
const RETRYABLE_STATUS_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
23
|
-
|
|
24
|
+
// 401 and 403 are not tracked: they refuse one credential (for example an expired or
|
|
25
|
+
// revoked agent admission) and say nothing about the origin's health, so they must not
|
|
26
|
+
// open the per-origin breaker for every other credential on this machine.
|
|
27
|
+
const CIRCUIT_TRACK_STATUS_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
24
28
|
const circuitStateByScope = new Map();
|
|
25
29
|
|
|
26
30
|
// File-backed cache. Lets cooldown + failure counts survive across CLI
|
|
@@ -422,7 +426,8 @@ export function __resetRequestCircuitForTests(scope) {
|
|
|
422
426
|
* maxRetries?: number,
|
|
423
427
|
* retryDelayMs?: number
|
|
424
428
|
* allowEmptyBody?: boolean
|
|
425
|
-
*
|
|
429
|
+
* credential?: object
|
|
430
|
+
* }} [options] `credential` (src/auth/credential-destinations.js) is sent with credentialedRequest.
|
|
426
431
|
* @returns {Promise<any>}
|
|
427
432
|
*/
|
|
428
433
|
export async function requestJson(
|
|
@@ -430,6 +435,7 @@ export async function requestJson(
|
|
|
430
435
|
{
|
|
431
436
|
method = "GET",
|
|
432
437
|
headers = {},
|
|
438
|
+
credential = null,
|
|
433
439
|
body,
|
|
434
440
|
idempotencyKey = null,
|
|
435
441
|
allowNonIdempotent = false,
|
|
@@ -439,6 +445,13 @@ export async function requestJson(
|
|
|
439
445
|
allowEmptyBody = false,
|
|
440
446
|
} = {}
|
|
441
447
|
) {
|
|
448
|
+
if (credential && !isCredential(credential)) {
|
|
449
|
+
throw new TypeError("requestJson: credential must come from src/auth/credential-destinations.js.");
|
|
450
|
+
}
|
|
451
|
+
// Without a credential, nothing here attaches one: a caller-built Authorization header is refused.
|
|
452
|
+
if (Object.keys(headers || {}).some((name) => /^(?:proxy-)?authorization$/i.test(name))) {
|
|
453
|
+
throw new TypeError("requestJson: send a credential with { credential }, not an Authorization header.");
|
|
454
|
+
}
|
|
442
455
|
const normalizedMethod = String(method || "GET").trim().toUpperCase();
|
|
443
456
|
const explicitIdempotencyKey = String(idempotencyKey || "").trim() || null;
|
|
444
457
|
const existingIdempotencyKey = explicitIdempotencyKey || resolveIdempotencyKey(headers);
|
|
@@ -492,13 +505,14 @@ export async function requestJson(
|
|
|
492
505
|
}, normalizedTimeoutMs);
|
|
493
506
|
|
|
494
507
|
try {
|
|
495
|
-
const
|
|
508
|
+
const init = {
|
|
496
509
|
method: normalizedMethod,
|
|
497
510
|
headers: outgoingHeaders,
|
|
498
511
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
499
512
|
redirect: "error",
|
|
500
513
|
signal: controller.signal,
|
|
501
|
-
}
|
|
514
|
+
};
|
|
515
|
+
const response = credential ? await credentialedRequest(credential, url, init) : await fetch(String(url), init);
|
|
502
516
|
|
|
503
517
|
const rawBody = await response.text();
|
|
504
518
|
const trimmedBody = rawBody.trim();
|
|
@@ -575,7 +589,8 @@ export async function requestJson(
|
|
|
575
589
|
await sleep(delayMs);
|
|
576
590
|
continue;
|
|
577
591
|
} catch (error) {
|
|
578
|
-
|
|
592
|
+
// A refused destination is final: nothing was sent, so no retry and no breaker count.
|
|
593
|
+
if (error instanceof SentinelayerApiError || error instanceof CredentialDestinationRefused) {
|
|
579
594
|
throw error;
|
|
580
595
|
}
|
|
581
596
|
|
|
@@ -647,6 +662,7 @@ export async function requestJsonMutation(
|
|
|
647
662
|
{
|
|
648
663
|
method = "POST",
|
|
649
664
|
headers = {},
|
|
665
|
+
credential = null,
|
|
650
666
|
body,
|
|
651
667
|
idempotencyKey = null,
|
|
652
668
|
operationName,
|
|
@@ -675,6 +691,7 @@ export async function requestJsonMutation(
|
|
|
675
691
|
return requestJson(url, {
|
|
676
692
|
method: normalizedMethod,
|
|
677
693
|
headers,
|
|
694
|
+
credential,
|
|
678
695
|
body,
|
|
679
696
|
idempotencyKey: resolvedIdempotencyKey,
|
|
680
697
|
timeoutMs,
|
package/src/auth/service.js
CHANGED
|
@@ -6,6 +6,14 @@ import open from "open";
|
|
|
6
6
|
|
|
7
7
|
import { loadConfig } from "../config/service.js";
|
|
8
8
|
import { scopedAdmissionAuth } from "./admission-scope.js";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_API_URL,
|
|
11
|
+
assertTrustedApiUrl,
|
|
12
|
+
credentialFor,
|
|
13
|
+
isAuthenticated,
|
|
14
|
+
resolveTrustContext,
|
|
15
|
+
userCredential,
|
|
16
|
+
} from "./credential-destinations.js";
|
|
9
17
|
import { SentinelayerApiError, requestJson, requestJsonMutation } from "./http.js";
|
|
10
18
|
import {
|
|
11
19
|
clearStoredSession,
|
|
@@ -15,7 +23,6 @@ import {
|
|
|
15
23
|
} from "./session-store.js";
|
|
16
24
|
import { authLoginHint } from "../ui/command-hints.js";
|
|
17
25
|
|
|
18
|
-
const DEFAULT_API_URL = "https://api.sentinelayer.com";
|
|
19
26
|
/** Default maximum wall-clock wait for browser-based CLI auth approval (ms). */
|
|
20
27
|
export const DEFAULT_AUTH_TIMEOUT_MS = 10 * 60 * 1000;
|
|
21
28
|
/** Default lifetime for issued API tokens used by CLI sessions (days). */
|
|
@@ -57,12 +64,6 @@ function normalizePositiveNumber(rawValue, field, fallbackValue) {
|
|
|
57
64
|
return normalized;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
function toAuthHeader(token) {
|
|
61
|
-
return {
|
|
62
|
-
Authorization: `Bearer ${String(token || "").trim()}`,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
67
|
function normalizeUser(user = {}) {
|
|
67
68
|
return {
|
|
68
69
|
id: String(user.id || "").trim(),
|
|
@@ -171,7 +172,6 @@ function isNearExpiry(tokenExpiresAt, thresholdDays) {
|
|
|
171
172
|
* @returns {Promise<string>}
|
|
172
173
|
*/
|
|
173
174
|
export async function resolveApiUrl({
|
|
174
|
-
cwd = process.cwd(),
|
|
175
175
|
env = process.env,
|
|
176
176
|
explicitApiUrl = "",
|
|
177
177
|
homeDir,
|
|
@@ -180,19 +180,9 @@ export async function resolveApiUrl({
|
|
|
180
180
|
if (overrideUrl) {
|
|
181
181
|
return normalizeApiUrl(overrideUrl);
|
|
182
182
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return normalizeApiUrl(envUrl);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const config = await loadConfig({ cwd, env, homeDir });
|
|
190
|
-
const configuredApiUrl = String(config.resolved.apiUrl || "").trim();
|
|
191
|
-
if (configuredApiUrl) {
|
|
192
|
-
return normalizeApiUrl(configuredApiUrl);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return normalizeApiUrl(DEFAULT_API_URL);
|
|
183
|
+
// The trust context's API (SENTINELAYER_API_URL, else the global config, else the default). A
|
|
184
|
+
// workspace .sentinelayer.yml does not choose the API.
|
|
185
|
+
return normalizeApiUrl((await resolveTrustContext({ env, homeDir })).apiOrigin);
|
|
196
186
|
}
|
|
197
187
|
|
|
198
188
|
async function startCliAuthSession({ apiUrl, challenge, ide, cliVersion, flowRequestId }) {
|
|
@@ -379,20 +369,21 @@ async function pollCliAuthSession({
|
|
|
379
369
|
});
|
|
380
370
|
}
|
|
381
371
|
|
|
382
|
-
async function fetchCurrentUser({ apiUrl,
|
|
372
|
+
async function fetchCurrentUser({ apiUrl, credential, flowRequestId }) {
|
|
383
373
|
return requestAuthJson(
|
|
384
374
|
flowRequestId,
|
|
385
375
|
buildApiPath(apiUrl, "/api/v1/auth/me"),
|
|
386
376
|
{
|
|
387
377
|
method: "GET",
|
|
388
|
-
|
|
378
|
+
credential,
|
|
379
|
+
headers: withFlowRequestHeaders({}, flowRequestId),
|
|
389
380
|
}
|
|
390
381
|
);
|
|
391
382
|
}
|
|
392
383
|
|
|
393
384
|
async function issueApiToken({
|
|
394
385
|
apiUrl,
|
|
395
|
-
|
|
386
|
+
credential,
|
|
396
387
|
tokenLabel,
|
|
397
388
|
tokenTtlDays,
|
|
398
389
|
flowRequestId,
|
|
@@ -406,12 +397,8 @@ async function issueApiToken({
|
|
|
406
397
|
{
|
|
407
398
|
method: "POST",
|
|
408
399
|
operationName: "issue-token",
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
...toAuthHeader(authToken),
|
|
412
|
-
},
|
|
413
|
-
flowRequestId
|
|
414
|
-
),
|
|
400
|
+
credential,
|
|
401
|
+
headers: withFlowRequestHeaders({}, flowRequestId),
|
|
415
402
|
body: {
|
|
416
403
|
label: String(tokenLabel || "").trim() || defaultTokenLabel(),
|
|
417
404
|
scope: "cli",
|
|
@@ -422,7 +409,7 @@ async function issueApiToken({
|
|
|
422
409
|
);
|
|
423
410
|
}
|
|
424
411
|
|
|
425
|
-
async function revokeApiToken({ apiUrl,
|
|
412
|
+
async function revokeApiToken({ apiUrl, credential, tokenId }) {
|
|
426
413
|
const normalizedTokenId = String(tokenId || "").trim();
|
|
427
414
|
if (!normalizedTokenId) {
|
|
428
415
|
return false;
|
|
@@ -430,9 +417,7 @@ async function revokeApiToken({ apiUrl, authToken, tokenId }) {
|
|
|
430
417
|
await requestJsonMutation(buildApiPath(apiUrl, `/api/v1/auth/api-tokens/${encodeURIComponent(normalizedTokenId)}`), {
|
|
431
418
|
method: "DELETE",
|
|
432
419
|
operationName: "revoke-token",
|
|
433
|
-
|
|
434
|
-
...toAuthHeader(authToken),
|
|
435
|
-
},
|
|
420
|
+
credential,
|
|
436
421
|
});
|
|
437
422
|
return true;
|
|
438
423
|
}
|
|
@@ -443,6 +428,7 @@ async function rotateStoredApiTokenIfNeeded({
|
|
|
443
428
|
tokenLabel,
|
|
444
429
|
tokenTtlDays,
|
|
445
430
|
homeDir,
|
|
431
|
+
context,
|
|
446
432
|
}) {
|
|
447
433
|
if (!session || !session.token || !session.tokenExpiresAt) {
|
|
448
434
|
return { session, rotated: false };
|
|
@@ -453,7 +439,7 @@ async function rotateStoredApiTokenIfNeeded({
|
|
|
453
439
|
|
|
454
440
|
const issued = await issueApiToken({
|
|
455
441
|
apiUrl: session.apiUrl,
|
|
456
|
-
|
|
442
|
+
credential: await userCredential(session.token, { context, source: "session" }),
|
|
457
443
|
tokenLabel,
|
|
458
444
|
tokenTtlDays,
|
|
459
445
|
});
|
|
@@ -475,7 +461,7 @@ async function rotateStoredApiTokenIfNeeded({
|
|
|
475
461
|
try {
|
|
476
462
|
await revokeApiToken({
|
|
477
463
|
apiUrl: session.apiUrl,
|
|
478
|
-
|
|
464
|
+
credential: await userCredential(nextSession.token, { context, source: "session" }),
|
|
479
465
|
tokenId: session.tokenId,
|
|
480
466
|
});
|
|
481
467
|
} catch (error) {
|
|
@@ -536,6 +522,10 @@ export async function loginAndPersistSession({
|
|
|
536
522
|
homeDir,
|
|
537
523
|
} = {}) {
|
|
538
524
|
const apiUrl = await resolveApiUrl({ cwd, env, explicitApiUrl, homeDir });
|
|
525
|
+
// The token this flow issues is the user's, so the API that issues it must be the configured
|
|
526
|
+
// one: refuse any other before the browser step. Resolution and trust share env and homeDir.
|
|
527
|
+
const context = await resolveTrustContext({ env, homeDir });
|
|
528
|
+
await assertTrustedApiUrl(apiUrl, { context });
|
|
539
529
|
const challenge = generateChallenge();
|
|
540
530
|
const flowRequestId = createFlowRequestId();
|
|
541
531
|
const session = await startCliAuthSession({
|
|
@@ -574,13 +564,14 @@ export async function loginAndPersistSession({
|
|
|
574
564
|
requestId: flowRequestId || null,
|
|
575
565
|
});
|
|
576
566
|
}
|
|
567
|
+
const approvalCredential = await userCredential(approvalToken, { context, source: "login_approval" });
|
|
577
568
|
|
|
578
569
|
const user = normalizeUser(
|
|
579
|
-
approval.user || (await fetchCurrentUser({ apiUrl,
|
|
570
|
+
approval.user || (await fetchCurrentUser({ apiUrl, credential: approvalCredential, flowRequestId }))
|
|
580
571
|
);
|
|
581
572
|
const issuedApiToken = await issueApiToken({
|
|
582
573
|
apiUrl,
|
|
583
|
-
|
|
574
|
+
credential: approvalCredential,
|
|
584
575
|
tokenLabel,
|
|
585
576
|
tokenTtlDays,
|
|
586
577
|
flowRequestId,
|
|
@@ -641,7 +632,7 @@ export async function loginAndPersistSession({
|
|
|
641
632
|
* }} [options]
|
|
642
633
|
* @returns {Promise<null | {
|
|
643
634
|
* apiUrl: string,
|
|
644
|
-
*
|
|
635
|
+
* credential: object,
|
|
645
636
|
* source: "env" | "config" | "session",
|
|
646
637
|
* user: {
|
|
647
638
|
* id: string,
|
|
@@ -674,12 +665,15 @@ export async function resolveActiveAuthSession({
|
|
|
674
665
|
if (scoped !== undefined) return scoped;
|
|
675
666
|
|
|
676
667
|
const apiUrl = await resolveApiUrl({ cwd, env, explicitApiUrl, homeDir });
|
|
668
|
+
// Each token below is bound to the configured API by the one trust context; `apiUrl` may name
|
|
669
|
+
// another origin (--api-url, a project config), and then a request carrying it is refused.
|
|
670
|
+
const context = await resolveTrustContext({ env, homeDir });
|
|
677
671
|
|
|
678
672
|
const envToken = String(env.SENTINELAYER_TOKEN || "").trim();
|
|
679
673
|
if (envToken) {
|
|
680
674
|
return {
|
|
681
675
|
apiUrl,
|
|
682
|
-
|
|
676
|
+
credential: await userCredential(envToken, { context, source: "env" }),
|
|
683
677
|
source: "env",
|
|
684
678
|
user: null,
|
|
685
679
|
aidenid: null,
|
|
@@ -692,12 +686,13 @@ export async function resolveActiveAuthSession({
|
|
|
692
686
|
};
|
|
693
687
|
}
|
|
694
688
|
|
|
689
|
+
// The user's own config only: a workspace .sentinelayer.yml does not choose whose token is used.
|
|
695
690
|
const config = await loadConfig({ cwd, env, homeDir });
|
|
696
|
-
const configuredToken = String(config.
|
|
691
|
+
const configuredToken = String(config.layers.global.sentinelayerToken || "").trim();
|
|
697
692
|
if (configuredToken) {
|
|
698
693
|
return {
|
|
699
694
|
apiUrl,
|
|
700
|
-
|
|
695
|
+
credential: await userCredential(configuredToken, { context, source: "config" }),
|
|
701
696
|
source: "config",
|
|
702
697
|
user: null,
|
|
703
698
|
aidenid: null,
|
|
@@ -725,6 +720,7 @@ export async function resolveActiveAuthSession({
|
|
|
725
720
|
tokenLabel,
|
|
726
721
|
tokenTtlDays,
|
|
727
722
|
homeDir,
|
|
723
|
+
context,
|
|
728
724
|
});
|
|
729
725
|
active = rotateResult.session;
|
|
730
726
|
rotated = rotateResult.rotated;
|
|
@@ -743,7 +739,7 @@ export async function resolveActiveAuthSession({
|
|
|
743
739
|
|
|
744
740
|
return {
|
|
745
741
|
apiUrl,
|
|
746
|
-
|
|
742
|
+
credential: await userCredential(active.token, { context, source: "session" }),
|
|
747
743
|
source: "session",
|
|
748
744
|
user: normalizeUser(active.user || {}),
|
|
749
745
|
aidenid: active.aidenid || null,
|
|
@@ -829,7 +825,7 @@ export async function getAuthStatus({
|
|
|
829
825
|
let remoteError = null;
|
|
830
826
|
if (checkRemote) {
|
|
831
827
|
try {
|
|
832
|
-
remoteUser = normalizeUser(await fetchCurrentUser({ apiUrl: session.apiUrl,
|
|
828
|
+
remoteUser = normalizeUser(await fetchCurrentUser({ apiUrl: session.apiUrl, credential: session.credential }));
|
|
833
829
|
} catch (error) {
|
|
834
830
|
remoteError =
|
|
835
831
|
error instanceof SentinelayerApiError
|
|
@@ -938,7 +934,7 @@ export async function revokeAuthToken({
|
|
|
938
934
|
autoRotate: false,
|
|
939
935
|
homeDir,
|
|
940
936
|
});
|
|
941
|
-
if (!active
|
|
937
|
+
if (!isAuthenticated(active)) {
|
|
942
938
|
throw new SentinelayerApiError(`No active auth token found. Run \`${authLoginHint()}\` first.`, {
|
|
943
939
|
status: 401,
|
|
944
940
|
code: "AUTH_REQUIRED",
|
|
@@ -954,7 +950,7 @@ export async function revokeAuthToken({
|
|
|
954
950
|
|
|
955
951
|
await revokeApiToken({
|
|
956
952
|
apiUrl: active.apiUrl,
|
|
957
|
-
|
|
953
|
+
credential: active.credential,
|
|
958
954
|
tokenId: targetTokenId,
|
|
959
955
|
});
|
|
960
956
|
|
|
@@ -1020,7 +1016,7 @@ export async function logoutSession({
|
|
|
1020
1016
|
try {
|
|
1021
1017
|
await revokeApiToken({
|
|
1022
1018
|
apiUrl: await resolveApiUrl({ cwd, env, explicitApiUrl, homeDir }),
|
|
1023
|
-
|
|
1019
|
+
credential: await userCredential(stored.token, { env, homeDir, source: "session" }),
|
|
1024
1020
|
tokenId: stored.tokenId,
|
|
1025
1021
|
});
|
|
1026
1022
|
revokedRemote = true;
|
|
@@ -1043,15 +1039,15 @@ export async function logoutSession({
|
|
|
1043
1039
|
*
|
|
1044
1040
|
* @param {{
|
|
1045
1041
|
* apiUrl: string,
|
|
1046
|
-
*
|
|
1042
|
+
* credential: object,
|
|
1047
1043
|
* runId: string,
|
|
1048
1044
|
* afterEventId?: string | null
|
|
1049
|
-
* }} [options]
|
|
1045
|
+
* }} [options] credential: the resolved auth's `credential` (src/auth/credential-destinations.js)
|
|
1050
1046
|
* @returns {Promise<any>}
|
|
1051
1047
|
*/
|
|
1052
1048
|
export async function listRuntimeRunEvents({
|
|
1053
1049
|
apiUrl,
|
|
1054
|
-
|
|
1050
|
+
credential,
|
|
1055
1051
|
runId,
|
|
1056
1052
|
afterEventId = null,
|
|
1057
1053
|
} = {}) {
|
|
@@ -1062,7 +1058,7 @@ export async function listRuntimeRunEvents({
|
|
|
1062
1058
|
buildApiPath(apiUrl, `/api/v1/runtime/runs/${encodeURIComponent(String(runId || ""))}/events/list${query}`),
|
|
1063
1059
|
{
|
|
1064
1060
|
method: "GET",
|
|
1065
|
-
|
|
1061
|
+
credential,
|
|
1066
1062
|
}
|
|
1067
1063
|
);
|
|
1068
1064
|
}
|
|
@@ -1070,7 +1066,7 @@ export async function listRuntimeRunEvents({
|
|
|
1070
1066
|
/**
|
|
1071
1067
|
* Fetch runtime run status snapshot from the Sentinelayer API.
|
|
1072
1068
|
*
|
|
1073
|
-
* @param {{ apiUrl: string,
|
|
1069
|
+
* @param {{ apiUrl: string, credential: object, runId: string }} [options]
|
|
1074
1070
|
* @returns {Promise<any>}
|
|
1075
1071
|
*/
|
|
1076
1072
|
/**
|
|
@@ -1079,13 +1075,13 @@ export async function listRuntimeRunEvents({
|
|
|
1079
1075
|
*/
|
|
1080
1076
|
export async function fetchAidenIdCredentials({
|
|
1081
1077
|
apiUrl = DEFAULT_API_URL,
|
|
1082
|
-
|
|
1078
|
+
auth = null,
|
|
1083
1079
|
} = {}) {
|
|
1084
1080
|
const response = await requestJson(
|
|
1085
1081
|
buildApiPath(apiUrl, "/api/v1/aidenid/credentials"),
|
|
1086
1082
|
{
|
|
1087
1083
|
method: "GET",
|
|
1088
|
-
|
|
1084
|
+
credential: await credentialFor(auth),
|
|
1089
1085
|
}
|
|
1090
1086
|
);
|
|
1091
1087
|
return {
|
|
@@ -1099,14 +1095,14 @@ export async function fetchAidenIdCredentials({
|
|
|
1099
1095
|
|
|
1100
1096
|
export async function getRuntimeRunStatus({
|
|
1101
1097
|
apiUrl,
|
|
1102
|
-
|
|
1098
|
+
credential,
|
|
1103
1099
|
runId,
|
|
1104
1100
|
} = {}) {
|
|
1105
1101
|
return requestJson(
|
|
1106
1102
|
buildApiPath(apiUrl, `/api/v1/runtime/runs/${encodeURIComponent(String(runId || ""))}/status`),
|
|
1107
1103
|
{
|
|
1108
1104
|
method: "GET",
|
|
1109
|
-
|
|
1105
|
+
credential,
|
|
1110
1106
|
}
|
|
1111
1107
|
);
|
|
1112
1108
|
}
|
package/src/cli.js
CHANGED
|
@@ -242,6 +242,11 @@ export async function buildCliProgram({
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
245
|
+
// Where the user's credentials may go is decided once, here, from the real environment and the
|
|
246
|
+
// global config, and frozen: nothing that changes process.env later can move it.
|
|
247
|
+
const { freezeProcessTrustContext } = await import("./auth/credential-destinations.js");
|
|
248
|
+
await freezeProcessTrustContext();
|
|
249
|
+
|
|
245
250
|
// Normalize slash commands (/omargate → omargate, /audit → audit, etc.)
|
|
246
251
|
const normalizedArgs = normalizeSlashArgs(rawArgs);
|
|
247
252
|
|
|
@@ -269,15 +274,18 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
269
274
|
// the subcommand (say, post-agent, observe, locks, checkpoints, ...), so no
|
|
270
275
|
// subcommand, present or future, can reach the human token. A tombstoned
|
|
271
276
|
// admission refuses here, before any request.
|
|
272
|
-
const { assertDispatchMatchesScope, resolveAgentAdmissionTarget, withAgentAdmission } =
|
|
273
|
-
"./session/admission-auth.js"
|
|
274
|
-
);
|
|
277
|
+
const { assertDispatchMatchesScope, resolveAgentAdmissionTarget, withAgentAdmission } =
|
|
278
|
+
await import("./session/admission-auth.js");
|
|
275
279
|
const target = await resolveAgentAdmissionTarget(normalizedArgs);
|
|
276
|
-
// Execution must match authorization: before ANY action runs,
|
|
277
|
-
//
|
|
280
|
+
// Execution must match authorization: before ANY action runs, decide on Commander's parsed
|
|
281
|
+
// arguments and options -- the values the action will use. Through the MCP CLI bridge in an
|
|
282
|
+
// agent context the action binds to its session's admission, runs a legacy data-plane route
|
|
283
|
+
// as before, or is refused (src/session/route-classes.js); and the identity it resolves must
|
|
284
|
+
// be the one whose scope is in force.
|
|
278
285
|
program.hook("preAction", (_hooked, actionCommand) => assertDispatchMatchesScope(actionCommand));
|
|
279
286
|
if (target) {
|
|
280
|
-
|
|
287
|
+
// An admission seen above that is gone by the time it is read refuses: never a fallback.
|
|
288
|
+
await withAgentAdmission(target.sessionId, target.agentId, dispatch, { requireStored: true });
|
|
281
289
|
return;
|
|
282
290
|
}
|
|
283
291
|
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 */
|