sentinelayer-cli 0.46.4 → 0.48.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 +63 -3
- package/package.json +12 -7
- package/skills/omar-gate-setup/SKILL.md +103 -0
- package/skills/senti-agent-identity/SKILL.md +80 -0
- package/skills/senti-judge/SKILL.md +139 -0
- package/skills/senti-room-notifications/SKILL.md +73 -0
- package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
- package/skills/senti-room-notifications/package.json +1 -0
- package/skills/senti-room-notifications/test-classifier.js +109 -0
- package/skills/senti-room-notifications/test-receipt.js +54 -0
- package/skills/sentinelayer-cli/SKILL.md +222 -0
- package/src/agents/devtestbot/runner.js +22 -11
- package/src/agents/devtestbot/tool.js +24 -11
- package/src/agents/jules/fix-cycle.js +7 -1
- package/src/agents/jules/tools/runtime-audit.js +17 -12
- package/src/agents/shared-tools/dispatch-core.js +10 -5
- package/src/ai/aidenid.js +4 -2
- package/src/ai/domain-target-store.js +11 -5
- package/src/ai/identity-store.js +11 -5
- package/src/ai/site-store.js +8 -4
- package/src/audit/orchestrator.js +21 -14
- package/src/audit/package.js +6 -4
- package/src/audit/persona-loop.js +7 -0
- package/src/audit/replay.js +4 -2
- package/src/auth/gate.js +46 -8
- package/src/auth/service.js +34 -15
- package/src/auth/session-store.js +105 -47
- package/src/board/evidence-resolvers.js +3 -4
- package/src/cli.js +23 -3
- package/src/commands/ai/identity-lifecycle.js +30 -26
- package/src/commands/ai/provision-governance.js +32 -24
- package/src/commands/ai/shared.js +6 -4
- package/src/commands/audit.js +14 -12
- package/src/commands/auth.js +24 -4
- package/src/commands/board.js +2 -2
- package/src/commands/chat.js +8 -6
- package/src/commands/judge.js +125 -0
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/mcp.js +45 -52
- package/src/commands/omargate.js +2 -1
- package/src/commands/plugin.js +11 -3
- package/src/commands/review.js +9 -4
- package/src/commands/scan.js +7 -7
- package/src/commands/session.js +607 -57
- package/src/commands/skills.js +45 -0
- package/src/commands/watch.js +9 -7
- package/src/config/output-filesystem.js +156 -0
- package/src/config/schema.js +4 -2
- package/src/config/service.js +40 -4
- package/src/cost/history.js +15 -9
- package/src/daemon/artifact-lineage.js +15 -12
- package/src/daemon/assignment-ledger.js +31 -25
- package/src/daemon/ast-drift.js +7 -3
- package/src/daemon/budget-governor.js +26 -20
- package/src/daemon/error-worker.js +28 -22
- package/src/daemon/fix-cycle.js +5 -4
- package/src/daemon/hybrid-mapper.js +19 -16
- package/src/daemon/ingest-refresh.js +4 -0
- package/src/daemon/jira-lifecycle.js +21 -17
- package/src/daemon/operator-control.js +18 -15
- package/src/daemon/reliability-lane.js +18 -15
- package/src/daemon/scope-engine.js +12 -8
- package/src/daemon/watchdog.js +15 -12
- package/src/engram/build-state.js +24 -3
- package/src/engram/capability-index.js +367 -0
- package/src/engram/compile-graph.js +1063 -0
- package/src/engram/contracts/action-receipt.js +82 -0
- package/src/engram/contracts/capability-index.js +259 -0
- package/src/engram/contracts/capability.js +377 -0
- package/src/engram/contracts/content-hash.js +8 -0
- package/src/engram/contracts/evidence.js +258 -0
- package/src/engram/contracts/graph.js +594 -0
- package/src/engram/contracts/identity.js +8 -0
- package/src/engram/contracts/index.js +149 -0
- package/src/engram/contracts/media-asset.js +85 -0
- package/src/engram/contracts/money.js +249 -0
- package/src/engram/contracts/source-snapshot.js +57 -0
- package/src/engram/contracts/task-context-pack.js +114 -0
- package/src/engram/contracts/temporal-segment.js +72 -0
- package/src/engram/contracts/versions.js +96 -0
- package/src/engram/extract-html.js +692 -0
- package/src/engram/graph-store.js +877 -0
- package/src/engram/index.js +98 -14
- package/src/engram/ingest-site.js +132 -0
- package/src/engram/live-state.js +1180 -0
- package/src/engram/namespace.js +34 -1
- package/src/engram/retrieve-site.js +753 -0
- package/src/engram/safe-reader.js +597 -0
- package/src/engram/source-intake.js +251 -0
- package/src/engram/store.js +227 -9
- package/src/ingest/engine.js +21 -28
- package/src/judge/verdict.js +305 -0
- package/src/legacy-cli.js +96 -19
- package/src/mcp/bridge-inputs.js +28 -4
- package/src/mcp/cli-command-tools.js +70 -0
- package/src/mcp/registry.js +17 -13
- package/src/mcp/session-stdio-server.js +297 -19
- package/src/memory/blackboard.js +5 -4
- package/src/net/gh-api-path.js +35 -9
- package/src/net/test-egress-guard.js +16 -4
- package/src/plugin/manifest.js +12 -8
- package/src/review/ai-review.js +110 -10
- package/src/review/investor-dd-devtestbot.js +17 -6
- package/src/review/investor-dd-orchestrator.js +21 -13
- package/src/review/local-review.js +26 -16
- package/src/review/omargate-cache.js +5 -5
- package/src/review/omargate-orchestrator.js +89 -17
- package/src/review/replay.js +9 -4
- package/src/review/report.js +22 -13
- package/src/review/typesafe-severity.js +201 -0
- package/src/session/admission-auth.js +34 -7
- package/src/session/admission.js +322 -71
- package/src/session/agent-access.js +137 -0
- package/src/session/agent-registry.js +2 -1
- package/src/session/attachment-summary.js +77 -0
- package/src/session/edit-summary.js +19 -0
- package/src/session/invitations.js +65 -1
- package/src/session/mentions.js +17 -9
- package/src/session/message-edits.js +5 -4
- package/src/session/preview.js +10 -2
- package/src/session/recap.js +3 -2
- package/src/session/room-files.js +519 -0
- package/src/session/route-classes.js +22 -8
- package/src/session/setup-guides.js +4 -0
- package/src/session/standup.js +192 -0
- package/src/session/sync.js +229 -10
- package/src/session/tickets.js +41 -0
- package/src/session/transcript.js +2 -1
- package/src/session/wake/resolve-target.js +7 -3
- package/src/skills/listener-runtime.cjs +92 -0
- package/src/skills/service.js +207 -0
- package/src/swarm/factory.js +5 -5
- package/src/swarm/pentest.js +9 -9
- package/src/swarm/report.js +4 -4
- package/src/swarm/runtime.js +15 -12
- package/src/swarm/scenario-dsl.js +7 -5
- package/src/telemetry/ledger.js +16 -7
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { requireOutputFileSystem } from "../config/output-filesystem.js";
|
|
3
4
|
|
|
4
5
|
import { createMultiProviderApiClient } from "../ai/client.js";
|
|
5
6
|
import { serializeProxyError } from "../ai/proxy.js";
|
|
@@ -376,6 +377,7 @@ export function createIsolatedPersonaContext({
|
|
|
376
377
|
runId = "",
|
|
377
378
|
rootPath = ".",
|
|
378
379
|
artifactDir = "",
|
|
380
|
+
outputFs,
|
|
379
381
|
provider = null,
|
|
380
382
|
budget = {},
|
|
381
383
|
onEvent = null,
|
|
@@ -384,6 +386,7 @@ export function createIsolatedPersonaContext({
|
|
|
384
386
|
isolation = DEFAULT_ISOLATION_MODE,
|
|
385
387
|
eventContext = null,
|
|
386
388
|
} = {}) {
|
|
389
|
+
if (artifactDir) requireOutputFileSystem(outputFs);
|
|
387
390
|
const normalizedAgent = normalizeAuditPersona(agent || {});
|
|
388
391
|
const resolvedRootPath = path.resolve(String(rootPath || "."));
|
|
389
392
|
const resolvedRunId =
|
|
@@ -399,6 +402,7 @@ export function createIsolatedPersonaContext({
|
|
|
399
402
|
budget: loopBudget,
|
|
400
403
|
runId: resolvedRunId,
|
|
401
404
|
artifactDir,
|
|
405
|
+
outputFs,
|
|
402
406
|
onEvent,
|
|
403
407
|
});
|
|
404
408
|
const emitter = (event, payload = {}) => {
|
|
@@ -701,6 +705,7 @@ async function runSinglePersonaAgenticLoop({
|
|
|
701
705
|
sharedContext = null,
|
|
702
706
|
hybridContext = null,
|
|
703
707
|
artifactDir = "",
|
|
708
|
+
outputFs,
|
|
704
709
|
provider = null,
|
|
705
710
|
budget = {},
|
|
706
711
|
maxTurns = null,
|
|
@@ -722,6 +727,7 @@ async function runSinglePersonaAgenticLoop({
|
|
|
722
727
|
runId: requestedRunId,
|
|
723
728
|
rootPath: resolvedRootPath,
|
|
724
729
|
artifactDir,
|
|
730
|
+
outputFs,
|
|
725
731
|
provider,
|
|
726
732
|
budget,
|
|
727
733
|
onEvent,
|
|
@@ -1053,6 +1059,7 @@ function summarizeSwarmStatus(results = []) {
|
|
|
1053
1059
|
}
|
|
1054
1060
|
|
|
1055
1061
|
export async function runPersonaAgenticLoop(options = {}) {
|
|
1062
|
+
if (options.artifactDir) requireOutputFileSystem(options.outputFs);
|
|
1056
1063
|
const {
|
|
1057
1064
|
agent,
|
|
1058
1065
|
rootPath,
|
package/src/audit/replay.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { requireOutputFileSystem } from "../config/output-filesystem.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
function normalizeString(value) {
|
|
@@ -87,15 +87,17 @@ export function compareAuditReports(baseReport = {}, candidateReport = {}) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export async function writeAuditComparisonArtifact({
|
|
90
|
+
outputFs,
|
|
90
91
|
baseReport = {},
|
|
91
92
|
candidateReport = {},
|
|
92
93
|
outputDirectory = "",
|
|
93
94
|
} = {}) {
|
|
95
|
+
requireOutputFileSystem(outputFs);
|
|
94
96
|
const resolvedOutputDirectory = path.resolve(String(outputDirectory || "."));
|
|
95
97
|
const comparison = compareAuditReports(baseReport, candidateReport);
|
|
96
98
|
const fileName = `AUDIT_COMPARISON_${comparison.baseRunId}_vs_${comparison.candidateRunId}.json`;
|
|
97
99
|
const outputPath = path.join(resolvedOutputDirectory, fileName);
|
|
98
|
-
await
|
|
100
|
+
await outputFs.writeFile(outputPath, `${JSON.stringify(comparison, null, 2)}\n`, "utf-8");
|
|
99
101
|
return {
|
|
100
102
|
comparison,
|
|
101
103
|
outputPath,
|
package/src/auth/gate.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
import { resolveActiveAuthSession } from "./service.js";
|
|
8
|
+
import { readStoredSessionMetadata } from "./session-store.js";
|
|
8
9
|
import { resolveAgentAdmissionTarget } from "../session/admission-auth.js";
|
|
9
10
|
import { authLoginHint } from "../ui/command-hints.js";
|
|
10
11
|
import { isAuthenticated } from "./credential-destinations.js";
|
|
@@ -16,6 +17,8 @@ import { isAuthenticated } from "./credential-destinations.js";
|
|
|
16
17
|
* - auth login, auth status, auth --help
|
|
17
18
|
* - --help, --version, -h, -v
|
|
18
19
|
* - config (read-only config inspection)
|
|
20
|
+
* - skills install/list (local packaged files only)
|
|
21
|
+
* - judge check (validates a local verdict file; sends nothing)
|
|
19
22
|
*
|
|
20
23
|
* All other commands require a valid session.
|
|
21
24
|
* If not authenticated, prints instructions and exits.
|
|
@@ -30,7 +33,7 @@ const AUTH_BYPASS_COMMANDS = new Set([
|
|
|
30
33
|
"-v",
|
|
31
34
|
]);
|
|
32
35
|
|
|
33
|
-
// Commands that work without auth (
|
|
36
|
+
// Commands that work without auth (local only, no API calls)
|
|
34
37
|
const NO_AUTH_REQUIRED = new Set([
|
|
35
38
|
"config", // local config inspection
|
|
36
39
|
]);
|
|
@@ -330,6 +333,25 @@ function isAuthenticatedSessionValid(session) {
|
|
|
330
333
|
return true;
|
|
331
334
|
}
|
|
332
335
|
|
|
336
|
+
/**
|
|
337
|
+
* A help request a newcomer can make before signing in (`sl session --help`, `sl session say --help`): the help flag
|
|
338
|
+
* is the LAST argument and every argument before it is a plain word (command, subcommand, positional). Then no option
|
|
339
|
+
* can take "--help" as its value (`--message-file --help` stays gated), so Commander prints help and runs nothing.
|
|
340
|
+
* The caller applies it only to Commander-dispatched commands. Tokens are judged exactly as Commander will see them,
|
|
341
|
+
* never trimmed: `" --help"` is message text to Commander, so it must not skip the gate either.
|
|
342
|
+
*
|
|
343
|
+
* @param {string[]} args - CLI arguments (after normalization)
|
|
344
|
+
* @returns {boolean}
|
|
345
|
+
*/
|
|
346
|
+
export function isHelpOnlyInvocation(args = []) {
|
|
347
|
+
const list = Array.isArray(args) ? args : [];
|
|
348
|
+
const last = list[list.length - 1];
|
|
349
|
+
if (last !== "--help" && last !== "-h") {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
return list.slice(0, -1).every((arg) => typeof arg === "string" && arg !== "" && !arg.startsWith("-"));
|
|
353
|
+
}
|
|
354
|
+
|
|
333
355
|
/**
|
|
334
356
|
* Check if the current command requires authentication.
|
|
335
357
|
* Returns true if auth is required but user is not logged in.
|
|
@@ -348,6 +370,16 @@ export async function checkAuthGate(args) {
|
|
|
348
370
|
return { authenticated: true, session: null, bypassReason: "no_auth_required", failureReason: null };
|
|
349
371
|
}
|
|
350
372
|
|
|
373
|
+
// These two commands only inspect/copy packaged files. Future skills subcommands stay gated.
|
|
374
|
+
if (args[0] === "skills" && (args[1] === "install" || args[1] === "list")) {
|
|
375
|
+
return { authenticated: true, session: null, bypassReason: "local_skills", failureReason: null };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Validating a verdict file is local; `judge verdict` sends, so it stays gated.
|
|
379
|
+
if (args[0] === "judge" && args[1] === "check") {
|
|
380
|
+
return { authenticated: true, session: null, bypassReason: "local_judge_check", failureReason: null };
|
|
381
|
+
}
|
|
382
|
+
|
|
351
383
|
if (isSessionNoAuthCommand(args)) {
|
|
352
384
|
return { authenticated: true, session: null, bypassReason: "session_no_auth_required", failureReason: null };
|
|
353
385
|
}
|
|
@@ -378,7 +410,9 @@ export async function checkAuthGate(args) {
|
|
|
378
410
|
resolveError = "session_invalid";
|
|
379
411
|
}
|
|
380
412
|
} else {
|
|
381
|
-
|
|
413
|
+
// A keyring/decryption failure leaves the metadata intact. That is not
|
|
414
|
+
// absence, and recommending logout would destroy recoverable sign-in.
|
|
415
|
+
resolveError = (await readStoredSessionMetadata()) ? "session_token_missing" : "no_session";
|
|
382
416
|
}
|
|
383
417
|
} catch (error) {
|
|
384
418
|
resolveError = error instanceof Error ? `session_read_error: ${error.message}` : "session_read_error";
|
|
@@ -405,15 +439,19 @@ export function printAuthRequired(failureReason = null) {
|
|
|
405
439
|
if (reason === "session_expired") {
|
|
406
440
|
console.error(" Your stored session has expired. Log in again:");
|
|
407
441
|
} else if (reason === "session_token_missing") {
|
|
408
|
-
console.error(" Your session metadata is present but
|
|
409
|
-
console.error("
|
|
442
|
+
console.error(" Your session metadata is present but its credential cannot be read.");
|
|
443
|
+
console.error(" Check the current user/home, keyring, encryption key and file permissions.");
|
|
444
|
+
console.error(" Your stored credentials were not cleared; repair storage before signing in again.");
|
|
410
445
|
console.error("");
|
|
411
|
-
|
|
412
|
-
|
|
446
|
+
process.exitCode = 1;
|
|
447
|
+
return;
|
|
413
448
|
} else if (reason && reason.startsWith("session_read_error")) {
|
|
414
449
|
console.error(" Session read failed: " + pc.yellow(reason.replace(/^session_read_error:\s*/, "")));
|
|
415
|
-
console.error("
|
|
416
|
-
console.error("
|
|
450
|
+
console.error(" Check the current user/home and credential-file permissions.");
|
|
451
|
+
console.error(" Your stored credentials were not cleared; do not log out to repair a read error.");
|
|
452
|
+
console.error("");
|
|
453
|
+
process.exitCode = 1;
|
|
454
|
+
return;
|
|
417
455
|
} else {
|
|
418
456
|
console.error(" Log in to SentinelLayer to use CLI commands:");
|
|
419
457
|
}
|
package/src/auth/service.js
CHANGED
|
@@ -7,6 +7,7 @@ import open from "open";
|
|
|
7
7
|
import { loadConfig } from "../config/service.js";
|
|
8
8
|
import { scopedAdmissionAuth } from "./admission-scope.js";
|
|
9
9
|
import {
|
|
10
|
+
CredentialDestinationRefused,
|
|
10
11
|
DEFAULT_API_URL,
|
|
11
12
|
assertTrustedApiUrl,
|
|
12
13
|
credentialFor,
|
|
@@ -453,6 +454,7 @@ async function rotateStoredApiTokenIfNeeded({
|
|
|
453
454
|
tokenPrefix: issued.token_prefix || null,
|
|
454
455
|
tokenExpiresAt: issued.expires_at || null,
|
|
455
456
|
user: session.user,
|
|
457
|
+
aidenid: session.aidenid,
|
|
456
458
|
},
|
|
457
459
|
{ homeDir }
|
|
458
460
|
);
|
|
@@ -490,7 +492,8 @@ async function rotateStoredApiTokenIfNeeded({
|
|
|
490
492
|
* tokenTtlDays?: number,
|
|
491
493
|
* ide?: string,
|
|
492
494
|
* cliVersion?: string,
|
|
493
|
-
* homeDir?: string
|
|
495
|
+
* homeDir?: string,
|
|
496
|
+
* onAuthorize?: (details: { authorizeUrl: string, browserOpened: boolean }) => void | Promise<void>
|
|
494
497
|
* }} [options]
|
|
495
498
|
* @returns {Promise<{
|
|
496
499
|
* apiUrl: string,
|
|
@@ -521,6 +524,7 @@ export async function loginAndPersistSession({
|
|
|
521
524
|
ide = DEFAULT_IDE_NAME,
|
|
522
525
|
cliVersion = "",
|
|
523
526
|
homeDir,
|
|
527
|
+
onAuthorize = () => {},
|
|
524
528
|
} = {}) {
|
|
525
529
|
const apiUrl = await resolveApiUrl({ cwd, env, explicitApiUrl, homeDir });
|
|
526
530
|
// The token this flow issues is the user's, so the API that issues it must be the configured
|
|
@@ -548,6 +552,10 @@ export async function loginAndPersistSession({
|
|
|
548
552
|
}
|
|
549
553
|
}
|
|
550
554
|
|
|
555
|
+
// Headless hosts must receive the URL before approval polling can complete.
|
|
556
|
+
// The command renders this on stderr in JSON mode, keeping stdout parseable.
|
|
557
|
+
await onAuthorize({ authorizeUrl, browserOpened });
|
|
558
|
+
|
|
551
559
|
const approval = await pollCliAuthSession({
|
|
552
560
|
apiUrl,
|
|
553
561
|
sessionId: String(session.session_id || "").trim(),
|
|
@@ -768,7 +776,8 @@ export async function resolveActiveAuthSession({
|
|
|
768
776
|
* homeDir?: string
|
|
769
777
|
* }} [options]
|
|
770
778
|
* @returns {Promise<{
|
|
771
|
-
* authenticated: boolean,
|
|
779
|
+
* authenticated: boolean | null,
|
|
780
|
+
* validation: "none" | "not_checked" | "verified" | "rejected" | "unavailable" | "storage_unavailable",
|
|
772
781
|
* apiUrl: string,
|
|
773
782
|
* source: string | null,
|
|
774
783
|
* storage: string | null,
|
|
@@ -805,20 +814,25 @@ export async function getAuthStatus({
|
|
|
805
814
|
});
|
|
806
815
|
|
|
807
816
|
if (!session) {
|
|
817
|
+
// Encrypted metadata can survive a missing/unreadable key or keyring.
|
|
818
|
+
// Distinguish recoverable storage from actual absence without creating a
|
|
819
|
+
// replacement key, validating remotely, or claiming authentication.
|
|
820
|
+
const stored = await readStoredSessionMetadata({ homeDir });
|
|
808
821
|
return {
|
|
809
|
-
authenticated: false,
|
|
822
|
+
authenticated: stored ? null : false,
|
|
823
|
+
validation: stored ? "storage_unavailable" : "none",
|
|
810
824
|
apiUrl: await resolveApiUrl({ cwd, env, explicitApiUrl, homeDir }),
|
|
811
|
-
source: null,
|
|
812
|
-
storage: null,
|
|
813
|
-
user: null,
|
|
814
|
-
aidenid: null,
|
|
825
|
+
source: stored ? "storage_unknown" : null,
|
|
826
|
+
storage: stored?.storage || null,
|
|
827
|
+
user: stored ? normalizeUser(stored.user || {}) : null,
|
|
828
|
+
aidenid: stored?.aidenid || null,
|
|
815
829
|
remoteUser: null,
|
|
816
830
|
remoteError: null,
|
|
817
831
|
rotated: false,
|
|
818
|
-
tokenExpiresAt: null,
|
|
819
|
-
tokenPrefix: null,
|
|
820
|
-
tokenId: null,
|
|
821
|
-
filePath: null,
|
|
832
|
+
tokenExpiresAt: stored?.tokenExpiresAt || null,
|
|
833
|
+
tokenPrefix: stored?.tokenPrefix || null,
|
|
834
|
+
tokenId: stored?.tokenId || null,
|
|
835
|
+
filePath: stored?.filePath || null,
|
|
822
836
|
};
|
|
823
837
|
}
|
|
824
838
|
|
|
@@ -829,12 +843,12 @@ export async function getAuthStatus({
|
|
|
829
843
|
remoteUser = normalizeUser(await fetchCurrentUser({ apiUrl: session.apiUrl, credential: session.credential }));
|
|
830
844
|
} catch (error) {
|
|
831
845
|
remoteError =
|
|
832
|
-
error instanceof SentinelayerApiError
|
|
846
|
+
error instanceof SentinelayerApiError || error instanceof CredentialDestinationRefused
|
|
833
847
|
? {
|
|
834
848
|
code: error.code,
|
|
835
849
|
message: error.message,
|
|
836
|
-
status: error.status,
|
|
837
|
-
requestId: error.requestId,
|
|
850
|
+
status: error instanceof CredentialDestinationRefused ? 403 : error.status,
|
|
851
|
+
requestId: error.requestId || null,
|
|
838
852
|
}
|
|
839
853
|
: {
|
|
840
854
|
code: "UNKNOWN",
|
|
@@ -845,8 +859,13 @@ export async function getAuthStatus({
|
|
|
845
859
|
}
|
|
846
860
|
}
|
|
847
861
|
|
|
862
|
+
const validationUnavailable = remoteError && (
|
|
863
|
+
[408, 425, 429, 499].includes(remoteError.status) || remoteError.status >= 500
|
|
864
|
+
);
|
|
848
865
|
return {
|
|
849
|
-
|
|
866
|
+
// Unknown remote health is neither a successful validation nor a logout.
|
|
867
|
+
authenticated: validationUnavailable ? null : !remoteError,
|
|
868
|
+
validation: validationUnavailable ? "unavailable" : remoteError ? "rejected" : checkRemote ? "verified" : "not_checked",
|
|
850
869
|
apiUrl: session.apiUrl,
|
|
851
870
|
source: session.source,
|
|
852
871
|
storage: session.storage,
|
|
@@ -19,6 +19,19 @@ const SESSION_WARNING_ALLOWED_FIELDS = new Set([
|
|
|
19
19
|
]);
|
|
20
20
|
const emittedSessionWarningKeys = new Set();
|
|
21
21
|
let keytarClientOverrideForTests;
|
|
22
|
+
let sessionOperationTail = Promise.resolve();
|
|
23
|
+
|
|
24
|
+
// A store operation spans metadata, keyring and file publication/cleanup. Reads
|
|
25
|
+
// can migrate plaintext too. Keep that transaction in order within this module;
|
|
26
|
+
// serializing only rename would still permit stale migrations and logout races.
|
|
27
|
+
// One queue also covers path aliases and keyring accounts shared across homes.
|
|
28
|
+
// Other processes still rely on the existing key publication and bounded retries.
|
|
29
|
+
function runSessionOperation(operation) {
|
|
30
|
+
const result = sessionOperationTail.then(operation);
|
|
31
|
+
// A failed operation must not poison the queue or retain returned credentials.
|
|
32
|
+
sessionOperationTail = result.then(() => undefined, () => undefined);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
22
35
|
|
|
23
36
|
function nowIso() {
|
|
24
37
|
return new Date().toISOString();
|
|
@@ -135,60 +148,68 @@ function resolveLegacyCredentialsKeyPath({ homeDir } = {}) {
|
|
|
135
148
|
return path.join(resolvedHome, ".sentinelayer", "credentials.key");
|
|
136
149
|
}
|
|
137
150
|
|
|
138
|
-
async function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
} catch (error) {
|
|
148
|
-
if (!(error && typeof error === "object" && error.code === "ENOENT")) {
|
|
151
|
+
async function readFileKeyAt(keyPath) {
|
|
152
|
+
// Exclusive-create fallback filesystems may expose a partially written key.
|
|
153
|
+
// Retry that bounded race, but never replace an invalid or unreadable key.
|
|
154
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
155
|
+
let raw;
|
|
156
|
+
try {
|
|
157
|
+
raw = await fsp.readFile(keyPath, "utf-8");
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error?.code === "ENOENT") return null;
|
|
149
160
|
throw error;
|
|
150
161
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await fsp.mkdir(path.dirname(keyPath), { recursive: true, mode: 0o700 });
|
|
158
|
-
await fsp.writeFile(keyPath, legacyKey.toString("base64"), { encoding: "utf-8", mode: 0o600 });
|
|
159
|
-
try {
|
|
160
|
-
await fsp.chmod(keyPath, 0o600);
|
|
161
|
-
} catch {
|
|
162
|
-
// Windows does not reliably support POSIX chmod semantics.
|
|
163
|
-
}
|
|
164
|
-
let verified = false;
|
|
165
|
-
try {
|
|
166
|
-
const verifyRaw = await fsp.readFile(keyPath, "utf-8");
|
|
167
|
-
const verifyKey = Buffer.from(String(verifyRaw || "").trim(), "base64");
|
|
168
|
-
verified = verifyKey.length === 32 && verifyKey.equals(legacyKey);
|
|
169
|
-
} catch {
|
|
170
|
-
verified = false;
|
|
171
|
-
}
|
|
172
|
-
if (verified) {
|
|
173
|
-
await fsp.rm(legacyKeyPath, { force: true });
|
|
174
|
-
}
|
|
175
|
-
return legacyKey;
|
|
176
|
-
}
|
|
177
|
-
} catch (error) {
|
|
178
|
-
if (!(error && typeof error === "object" && error.code === "ENOENT")) {
|
|
162
|
+
const encoded = String(raw || "").trim();
|
|
163
|
+
const key = Buffer.from(encoded, "base64");
|
|
164
|
+
if (key.length === 32 && key.toString("base64") === encoded) return key;
|
|
165
|
+
if (attempt >= 5) {
|
|
166
|
+
const error = new Error("Stored credential encryption key is invalid; it was not replaced.");
|
|
167
|
+
error.code = "CREDENTIALS_KEY_INVALID";
|
|
179
168
|
throw error;
|
|
180
169
|
}
|
|
170
|
+
await delayMilliseconds(20);
|
|
181
171
|
}
|
|
172
|
+
}
|
|
182
173
|
|
|
183
|
-
|
|
174
|
+
async function readFileKey({ homeDir } = {}) {
|
|
175
|
+
return (await readFileKeyAt(resolveCredentialsKeyPath({ homeDir }))) ||
|
|
176
|
+
(await readFileKeyAt(resolveLegacyCredentialsKeyPath({ homeDir })));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function loadOrCreateFileKey({ homeDir } = {}) {
|
|
180
|
+
const keyPath = resolveCredentialsKeyPath({ homeDir });
|
|
181
|
+
const existing = await readFileKeyAt(keyPath);
|
|
182
|
+
if (existing) return existing;
|
|
183
|
+
const legacyKey = await readFileKeyAt(resolveLegacyCredentialsKeyPath({ homeDir }));
|
|
184
|
+
const key = legacyKey || crypto.randomBytes(32);
|
|
185
|
+
const encoded = key.toString("base64");
|
|
184
186
|
await fsp.mkdir(path.dirname(keyPath), { recursive: true, mode: 0o700 });
|
|
185
|
-
|
|
187
|
+
const tmpPath = `${keyPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
186
188
|
try {
|
|
187
|
-
await fsp.
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
await fsp.writeFile(tmpPath, encoded, { encoding: "utf-8", mode: 0o600, flag: "wx" });
|
|
190
|
+
await syncFile(tmpPath);
|
|
191
|
+
try {
|
|
192
|
+
// Publish complete bytes once. Every concurrent writer then uses the winner.
|
|
193
|
+
await fsp.link(tmpPath, keyPath);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (error?.code !== "EEXIST") {
|
|
196
|
+
if (!["ENOTSUP", "ENOSYS", "EXDEV", "EPERM"].includes(error?.code)) throw error;
|
|
197
|
+
try {
|
|
198
|
+
await fsp.writeFile(keyPath, encoded, { encoding: "utf-8", mode: 0o600, flag: "wx" });
|
|
199
|
+
} catch (fallbackError) {
|
|
200
|
+
if (fallbackError?.code !== "EEXIST") throw fallbackError;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const persisted = await readFileKeyAt(keyPath);
|
|
205
|
+
if (!persisted) throw new Error("Credential encryption key could not be persisted.");
|
|
206
|
+
await syncFile(keyPath);
|
|
207
|
+
await syncDirectory(path.dirname(keyPath));
|
|
208
|
+
// Keep the legacy key: deleting it can race a read-only legacy reader.
|
|
209
|
+
return persisted;
|
|
210
|
+
} finally {
|
|
211
|
+
await fsp.rm(tmpPath, { force: true }).catch(() => {});
|
|
190
212
|
}
|
|
191
|
-
return key;
|
|
192
213
|
}
|
|
193
214
|
|
|
194
215
|
function encryptToken(token, key) {
|
|
@@ -540,7 +561,11 @@ async function tryDecryptFileToken({ metadata, homeDir }) {
|
|
|
540
561
|
return null;
|
|
541
562
|
}
|
|
542
563
|
try {
|
|
543
|
-
const key = await
|
|
564
|
+
const key = await readFileKey({ homeDir });
|
|
565
|
+
if (!key) {
|
|
566
|
+
emitSessionWarning("FILE_CREDENTIAL_UNAVAILABLE", { reason: "key_missing", source: "file-encrypted" });
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
544
569
|
const token = decryptToken(
|
|
545
570
|
{
|
|
546
571
|
tokenEncrypted: metadata.tokenEncrypted,
|
|
@@ -550,7 +575,13 @@ async function tryDecryptFileToken({ metadata, homeDir }) {
|
|
|
550
575
|
key
|
|
551
576
|
);
|
|
552
577
|
return token || null;
|
|
553
|
-
} catch {
|
|
578
|
+
} catch (error) {
|
|
579
|
+
emitSessionWarning("FILE_CREDENTIAL_UNAVAILABLE", {
|
|
580
|
+
reason: error?.code === "EACCES" || error?.code === "EPERM"
|
|
581
|
+
? "key_unreadable"
|
|
582
|
+
: error?.code === "CREDENTIALS_KEY_INVALID" ? "key_invalid" : "decryption_failed",
|
|
583
|
+
source: "file-encrypted",
|
|
584
|
+
});
|
|
554
585
|
return null;
|
|
555
586
|
}
|
|
556
587
|
}
|
|
@@ -582,6 +613,11 @@ async function tryDecryptFileToken({ metadata, homeDir }) {
|
|
|
582
613
|
* }>}
|
|
583
614
|
*/
|
|
584
615
|
export async function readStoredSession({ homeDir } = {}) {
|
|
616
|
+
const options = { homeDir: resolveHomeDir(homeDir) };
|
|
617
|
+
return runSessionOperation(() => readStoredSessionUnlocked(options));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function readStoredSessionUnlocked({ homeDir } = {}) {
|
|
585
621
|
const { filePath, metadata } = await readMetadata({ homeDir });
|
|
586
622
|
if (!metadata) {
|
|
587
623
|
return null;
|
|
@@ -693,6 +729,11 @@ export async function readStoredSession({ homeDir } = {}) {
|
|
|
693
729
|
* }>}
|
|
694
730
|
*/
|
|
695
731
|
export async function readStoredSessionMetadata({ homeDir } = {}) {
|
|
732
|
+
const options = { homeDir: resolveHomeDir(homeDir) };
|
|
733
|
+
return runSessionOperation(() => readStoredSessionMetadataUnlocked(options));
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function readStoredSessionMetadataUnlocked({ homeDir } = {}) {
|
|
696
737
|
const { filePath, metadata } = await readMetadata({ homeDir });
|
|
697
738
|
if (!metadata) {
|
|
698
739
|
return null;
|
|
@@ -722,6 +763,7 @@ export async function readStoredSessionMetadata({ homeDir } = {}) {
|
|
|
722
763
|
* tokenPrefix?: string | null,
|
|
723
764
|
* tokenExpiresAt?: string | null,
|
|
724
765
|
* user?: Record<string, unknown>
|
|
766
|
+
* aidenid?: Record<string, unknown> | null
|
|
725
767
|
* }} [session]
|
|
726
768
|
* @param {{ homeDir?: string }} [options]
|
|
727
769
|
* @returns {Promise<{
|
|
@@ -754,8 +796,18 @@ export async function writeStoredSession(
|
|
|
754
796
|
tokenPrefix = null,
|
|
755
797
|
tokenExpiresAt = null,
|
|
756
798
|
user = {},
|
|
799
|
+
aidenid = null,
|
|
757
800
|
} = {},
|
|
758
801
|
{ homeDir } = {}
|
|
802
|
+
) {
|
|
803
|
+
const session = { apiUrl, token, tokenId, tokenPrefix, tokenExpiresAt, user, aidenid };
|
|
804
|
+
const options = { homeDir: resolveHomeDir(homeDir) };
|
|
805
|
+
return runSessionOperation(() => writeStoredSessionUnlocked(session, options));
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function writeStoredSessionUnlocked(
|
|
809
|
+
{ apiUrl, token, tokenId, tokenPrefix, tokenExpiresAt, user, aidenid },
|
|
810
|
+
{ homeDir }
|
|
759
811
|
) {
|
|
760
812
|
const normalizedApiUrl = String(apiUrl || "").trim();
|
|
761
813
|
const normalizedToken = String(token || "").trim();
|
|
@@ -778,6 +830,7 @@ export async function writeStoredSession(
|
|
|
778
830
|
tokenPrefix,
|
|
779
831
|
tokenExpiresAt,
|
|
780
832
|
user: normalizeUser(user),
|
|
833
|
+
aidenid,
|
|
781
834
|
createdAt: existingMetadata?.createdAt || updatedAt,
|
|
782
835
|
updatedAt,
|
|
783
836
|
});
|
|
@@ -817,6 +870,11 @@ export async function writeStoredSession(
|
|
|
817
870
|
* @returns {Promise<{ filePath: string, hadSession: boolean }>}
|
|
818
871
|
*/
|
|
819
872
|
export async function clearStoredSession({ homeDir } = {}) {
|
|
873
|
+
const options = { homeDir: resolveHomeDir(homeDir) };
|
|
874
|
+
return runSessionOperation(() => clearStoredSessionUnlocked(options));
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async function clearStoredSessionUnlocked({ homeDir } = {}) {
|
|
820
878
|
const { filePath, metadata } = await readMetadata({ homeDir });
|
|
821
879
|
if (metadata && metadata.storage === "keyring") {
|
|
822
880
|
const keytar = await loadKeytarClient({ allowImplicit: true });
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* refused with an Error, which the gate reports as cannot-verify.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import {
|
|
33
|
+
import { ghCheckRunsApiPath, ghCompareApiPath, ghRepoSlug } from "../net/gh-api-path.js";
|
|
34
34
|
|
|
35
35
|
/** Shape a resolver must return. Anything else is treated as indeterminate. */
|
|
36
36
|
const YES = true;
|
|
@@ -74,8 +74,7 @@ export function createEvidenceResolver({ run, probeDeployedSha } = {}) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
async function shaOnBranch({ repo, sha, branch }) {
|
|
77
|
-
const
|
|
78
|
-
const res = await run(["api", `repos/${ghRepoSlug(repo)}/compare/${basehead}`, "--jq", ".status"]);
|
|
77
|
+
const res = await run(["api", ghCompareApiPath({ repo, branch, sha }), "--jq", ".status"]);
|
|
79
78
|
if (!res || !res.ok) return UNKNOWN;
|
|
80
79
|
const status = String(res.stdout ?? "").trim();
|
|
81
80
|
// GitHub's compare status, base=branch head=sha:
|
|
@@ -87,7 +86,7 @@ export function createEvidenceResolver({ run, probeDeployedSha } = {}) {
|
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
async function checkConclusive({ repo, ref, check }) {
|
|
90
|
-
const res = await run(["api",
|
|
89
|
+
const res = await run(["api", ghCheckRunsApiPath({ repo, ref }), "--jq", ".check_runs"]);
|
|
91
90
|
if (!res || !res.ok) return UNKNOWN;
|
|
92
91
|
const runs = parseJson(res.stdout);
|
|
93
92
|
if (!Array.isArray(runs)) return UNKNOWN;
|
package/src/cli.js
CHANGED
|
@@ -5,6 +5,16 @@ import { CLI_VERSION, runLegacyCliWithErrorHandling } from "./legacy-cli.js";
|
|
|
5
5
|
import { restrictProgramLookupToPath } from "./program-lookup.js";
|
|
6
6
|
|
|
7
7
|
const COMMAND_REGISTRARS = {
|
|
8
|
+
skills: {
|
|
9
|
+
loader: () => import("./commands/skills.js"),
|
|
10
|
+
exportName: "registerSkillsCommand",
|
|
11
|
+
needsLegacy: false,
|
|
12
|
+
},
|
|
13
|
+
judge: {
|
|
14
|
+
loader: () => import("./commands/judge.js"),
|
|
15
|
+
exportName: "registerJudgeCommand",
|
|
16
|
+
needsLegacy: false,
|
|
17
|
+
},
|
|
8
18
|
init: {
|
|
9
19
|
loader: () => import("./commands/init.js"),
|
|
10
20
|
exportName: "registerInitCommand",
|
|
@@ -262,9 +272,11 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
262
272
|
// Normalize slash commands (/omargate → omargate, /audit → audit, etc.)
|
|
263
273
|
const normalizedArgs = normalizeSlashArgs(rawArgs);
|
|
264
274
|
|
|
265
|
-
// Auth gate — require login for all commands except auth/help/version/config
|
|
266
|
-
|
|
267
|
-
const
|
|
275
|
+
// Auth gate — require login for all commands except auth/help/version/config. Help for a Commander command prints
|
|
276
|
+
// without login too: Commander prints it and runs nothing (a newcomer reads `sl session --help` first).
|
|
277
|
+
const { checkAuthGate, isHelpOnlyInvocation, printAuthRequired } = await import("./auth/gate.js");
|
|
278
|
+
const helpOnly = !shouldBypassCommander(normalizedArgs) && isHelpOnlyInvocation(normalizedArgs);
|
|
279
|
+
const authResult = helpOnly ? { authenticated: true } : await checkAuthGate(normalizedArgs);
|
|
268
280
|
if (!authResult.authenticated) {
|
|
269
281
|
printAuthRequired(authResult.failureReason);
|
|
270
282
|
return;
|
|
@@ -272,6 +284,14 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
272
284
|
|
|
273
285
|
if (shouldBypassCommander(normalizedArgs)) {
|
|
274
286
|
await runLegacyCliWithErrorHandling(normalizedArgs);
|
|
287
|
+
if (["--help", "-h", "help"].includes(normalizedArgs[0])) {
|
|
288
|
+
console.log("\nAgent Skills:");
|
|
289
|
+
console.log(" sl skills install [names...] [--dir <path>] [--force] [--json] Install bundled Senti skills (all, or the named ones)");
|
|
290
|
+
console.log(" sl skills list [--dir <path>] [--json] Show bundled and installed versions/hashes");
|
|
291
|
+
console.log("\nJudge Loop:");
|
|
292
|
+
console.log(" sl judge check --file <verdict.json> [--json] Validate a verdict offline and preview the message");
|
|
293
|
+
console.log(" sl judge verdict <sessionId> --agent <id> --file <verdict.json> Send the verdict to the coder");
|
|
294
|
+
}
|
|
275
295
|
return;
|
|
276
296
|
}
|
|
277
297
|
|