sentinelayer-cli 0.6.2 → 0.8.1
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 +1009 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +64 -63
- package/src/agents/ai-governance/index.js +12 -0
- package/src/agents/ai-governance/tools/base.js +171 -0
- package/src/agents/ai-governance/tools/eval-regression.js +47 -0
- package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
- package/src/agents/ai-governance/tools/index.js +52 -0
- package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
- package/src/agents/ai-governance/tools/provenance-check.js +69 -0
- package/src/agents/backend/index.js +12 -0
- package/src/agents/backend/tools/base.js +189 -0
- package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
- package/src/agents/backend/tools/idempotency-audit.js +105 -0
- package/src/agents/backend/tools/index.js +87 -0
- package/src/agents/backend/tools/retry-audit.js +132 -0
- package/src/agents/backend/tools/timeout-audit.js +144 -0
- package/src/agents/code-quality/index.js +12 -0
- package/src/agents/code-quality/tools/base.js +159 -0
- package/src/agents/code-quality/tools/complexity-measure.js +197 -0
- package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
- package/src/agents/code-quality/tools/cycle-detect.js +49 -0
- package/src/agents/code-quality/tools/dep-graph.js +196 -0
- package/src/agents/code-quality/tools/index.js +89 -0
- package/src/agents/data-layer/index.js +12 -0
- package/src/agents/data-layer/tools/base.js +181 -0
- package/src/agents/data-layer/tools/index-audit.js +165 -0
- package/src/agents/data-layer/tools/index.js +83 -0
- package/src/agents/data-layer/tools/migration-scan.js +135 -0
- package/src/agents/data-layer/tools/query-explain.js +120 -0
- package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
- package/src/agents/documentation/index.js +12 -0
- package/src/agents/documentation/tools/api-diff.js +91 -0
- package/src/agents/documentation/tools/base.js +151 -0
- package/src/agents/documentation/tools/dead-link-check.js +58 -0
- package/src/agents/documentation/tools/docstring-coverage.js +78 -0
- package/src/agents/documentation/tools/index.js +52 -0
- package/src/agents/documentation/tools/readme-freshness.js +61 -0
- package/src/agents/envelope/fix-cycle.js +45 -0
- package/src/agents/envelope/index.js +31 -0
- package/src/agents/envelope/loop.js +150 -0
- package/src/agents/envelope/pulse.js +18 -0
- package/src/agents/envelope/stream.js +40 -0
- package/src/agents/infrastructure/index.js +12 -0
- package/src/agents/infrastructure/tools/base.js +171 -0
- package/src/agents/infrastructure/tools/checkov-run.js +32 -0
- package/src/agents/infrastructure/tools/drift-detect.js +59 -0
- package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
- package/src/agents/infrastructure/tools/index.js +52 -0
- package/src/agents/infrastructure/tools/tflint-run.js +31 -0
- package/src/agents/jules/config/definition.js +160 -160
- package/src/agents/jules/config/system-prompt.js +182 -182
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +17 -17
- package/src/agents/jules/loop.js +460 -450
- package/src/agents/jules/pulse.js +10 -10
- package/src/agents/jules/stream.js +187 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +315 -309
- package/src/agents/jules/tools/aidenid-email.js +189 -189
- package/src/agents/jules/tools/auth-audit.js +1708 -1691
- package/src/agents/jules/tools/dispatch.js +340 -335
- package/src/agents/jules/tools/file-edit.js +2 -2
- package/src/agents/jules/tools/file-read.js +2 -2
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +2 -2
- package/src/agents/jules/tools/grep.js +2 -2
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +2 -2
- package/src/agents/jules/tools/runtime-audit.js +507 -507
- package/src/agents/jules/tools/shell.js +2 -2
- package/src/agents/jules/tools/url-policy.js +100 -100
- package/src/agents/mode.js +113 -0
- package/src/agents/observability/index.js +12 -0
- package/src/agents/observability/tools/alert-audit.js +39 -0
- package/src/agents/observability/tools/base.js +181 -0
- package/src/agents/observability/tools/dashboard-gap.js +42 -0
- package/src/agents/observability/tools/index.js +54 -0
- package/src/agents/observability/tools/log-schema-check.js +74 -0
- package/src/agents/observability/tools/span-coverage.js +74 -0
- package/src/agents/persona-visuals.js +102 -61
- package/src/agents/release/index.js +12 -0
- package/src/agents/release/tools/base.js +181 -0
- package/src/agents/release/tools/changelog-diff.js +86 -0
- package/src/agents/release/tools/feature-flag-audit.js +126 -0
- package/src/agents/release/tools/index.js +61 -0
- package/src/agents/release/tools/rollback-verify.js +129 -0
- package/src/agents/release/tools/semver-check.js +109 -0
- package/src/agents/reliability/index.js +12 -0
- package/src/agents/reliability/tools/backpressure-check.js +129 -0
- package/src/agents/reliability/tools/base.js +181 -0
- package/src/agents/reliability/tools/chaos-probe.js +109 -0
- package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
- package/src/agents/reliability/tools/health-check-audit.js +111 -0
- package/src/agents/reliability/tools/index.js +87 -0
- package/src/agents/run-persona.js +109 -0
- package/src/agents/security/index.js +12 -0
- package/src/agents/security/tools/authz-audit.js +134 -0
- package/src/agents/security/tools/base.js +190 -0
- package/src/agents/security/tools/crypto-review.js +175 -0
- package/src/agents/security/tools/index.js +97 -0
- package/src/agents/security/tools/sast-scan.js +175 -0
- package/src/agents/security/tools/secrets-scan.js +216 -0
- package/src/agents/shared-tools/dispatch-core.js +320 -315
- package/src/agents/shared-tools/file-edit.js +180 -180
- package/src/agents/shared-tools/file-read.js +100 -100
- package/src/agents/shared-tools/glob.js +168 -168
- package/src/agents/shared-tools/grep.js +228 -228
- package/src/agents/shared-tools/index.js +46 -46
- package/src/agents/shared-tools/path-guards.js +161 -161
- package/src/agents/shared-tools/shell.js +383 -383
- package/src/agents/supply-chain/index.js +12 -0
- package/src/agents/supply-chain/tools/attestation-check.js +42 -0
- package/src/agents/supply-chain/tools/base.js +151 -0
- package/src/agents/supply-chain/tools/index.js +52 -0
- package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
- package/src/agents/supply-chain/tools/package-verify.js +56 -0
- package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
- package/src/agents/testing/index.js +12 -0
- package/src/agents/testing/tools/base.js +202 -0
- package/src/agents/testing/tools/coverage-gap.js +144 -0
- package/src/agents/testing/tools/flake-detect.js +125 -0
- package/src/agents/testing/tools/index.js +85 -0
- package/src/agents/testing/tools/mutation-test.js +143 -0
- package/src/agents/testing/tools/snapshot-diff.js +103 -0
- package/src/ai/aidenid.js +1021 -1009
- package/src/ai/client.js +553 -553
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/proxy.js +137 -137
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/gate.js +428 -371
- package/src/auth/http.js +681 -611
- package/src/auth/service.js +1106 -1106
- package/src/auth/session-store.js +813 -813
- package/src/cli.js +257 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1338
- package/src/commands/ai/provision-governance.js +1272 -1272
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1171 -1166
- package/src/commands/auth.js +419 -419
- package/src/commands/chat.js +184 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +20 -10
- package/src/commands/mcp.js +461 -461
- package/src/commands/omargate.js +63 -29
- package/src/commands/persona.js +65 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +865 -872
- package/src/commands/session.js +1238 -0
- package/src/commands/spec.js +771 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +511 -511
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/coord/events-log.js +141 -0
- package/src/coord/handshake.js +719 -0
- package/src/coord/index.js +35 -0
- package/src/coord/paths.js +84 -0
- package/src/coord/priority.js +62 -0
- package/src/coord/tarjan.js +157 -0
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tokenizer.js +160 -0
- package/src/cost/tracker.js +232 -171
- package/src/daemon/artifact-lineage.js +896 -534
- package/src/daemon/assignment-ledger.js +1083 -770
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +1209 -626
- package/src/daemon/fix-cycle.js +384 -377
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/ingest-refresh.js +79 -11
- package/src/daemon/jira-lifecycle.js +767 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/pulse.js +327 -327
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/scope-engine.js +1068 -0
- package/src/daemon/watchdog.js +971 -971
- package/src/events/schema.js +190 -0
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +933 -918
- package/src/ingest/ownership.js +380 -0
- package/src/interactive/index.js +97 -97
- package/src/legacy-cli.js +3228 -2994
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +136 -118
- package/src/review/ai-review.js +672 -679
- package/src/review/compliance-pack.js +389 -0
- package/src/review/investor-dd-config.js +54 -0
- package/src/review/investor-dd-file-loop.js +303 -0
- package/src/review/investor-dd-file-router.js +406 -0
- package/src/review/investor-dd-html-report.js +233 -0
- package/src/review/investor-dd-notification.js +120 -0
- package/src/review/investor-dd-orchestrator.js +405 -0
- package/src/review/investor-dd-persona-runner.js +275 -0
- package/src/review/live-validator.js +253 -0
- package/src/review/local-review.js +1351 -1305
- package/src/review/omargate-interactive.js +68 -68
- package/src/review/omargate-orchestrator.js +492 -300
- package/src/review/persona-prompts.js +484 -296
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +147 -42
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -67
- package/src/scaffold/templates.js +150 -150
- package/src/scan/generator.js +418 -418
- package/src/scan/gh-secrets.js +107 -107
- package/src/session/agent-registry.js +359 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +1396 -0
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +37 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +762 -0
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +704 -0
- package/src/session/stream.js +333 -0
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- package/src/spec/generator.js +619 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +569 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/session-tracker.js +234 -234
- package/src/telemetry/sync.js +203 -203
- package/src/ui/command-hints.js +13 -13
- package/src/ui/markdown.js +220 -220
package/src/scan/gh-secrets.js
CHANGED
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import process from "node:process";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
|
|
5
|
-
function getGhCommand() {
|
|
6
|
-
return String(process.env.SENTINELAYER_GH_BIN || "").trim() || "gh";
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function normalizeRepoSlug(value) {
|
|
10
|
-
return String(value || "").trim().replace(/\.git$/i, "");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function isValidRepoSlug(value) {
|
|
14
|
-
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalizeRepoSlug(value));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isValidSecretName(value) {
|
|
18
|
-
return /^[A-Z][A-Z0-9_]{1,127}$/.test(String(value || "").trim());
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function ensureGhCliAvailable(ghCommand) {
|
|
22
|
-
const ghVersion = spawnSync(ghCommand, ["--version"], { encoding: "utf-8" });
|
|
23
|
-
if (ghVersion.status !== 0) {
|
|
24
|
-
throw new Error("GitHub CLI (gh) is not installed or not in PATH.");
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function detectRepoSlug(cwd) {
|
|
29
|
-
const ghCommand = getGhCommand();
|
|
30
|
-
const result = spawnSync(ghCommand, ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], {
|
|
31
|
-
cwd,
|
|
32
|
-
encoding: "utf-8",
|
|
33
|
-
});
|
|
34
|
-
if (result.status === 0 && result.stdout) {
|
|
35
|
-
return String(result.stdout).trim();
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function setupSecrets({ repoSlug, secretName, secretValue, dryRun }) {
|
|
41
|
-
const normalizedRepo = normalizeRepoSlug(repoSlug);
|
|
42
|
-
const ghCommand = getGhCommand();
|
|
43
|
-
|
|
44
|
-
if (!isValidRepoSlug(normalizedRepo)) {
|
|
45
|
-
return { ok: false, reason: "Invalid repo format. Use owner/repo." };
|
|
46
|
-
}
|
|
47
|
-
if (!isValidSecretName(secretName)) {
|
|
48
|
-
return { ok: false, reason: `Invalid secret name: ${secretName}. Must match /^[A-Z][A-Z0-9_]{1,127}$/.` };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (dryRun) {
|
|
52
|
-
return {
|
|
53
|
-
ok: true,
|
|
54
|
-
dryRun: true,
|
|
55
|
-
repo: normalizedRepo,
|
|
56
|
-
secretName,
|
|
57
|
-
instructions: [
|
|
58
|
-
`gh secret set ${secretName} --repo ${normalizedRepo}`,
|
|
59
|
-
`# Paste your SentinelLayer token when prompted`,
|
|
60
|
-
`gh secret list --repo ${normalizedRepo}`,
|
|
61
|
-
`# Verify ${secretName} appears in the list`,
|
|
62
|
-
],
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const secretSinkFile = String(process.env.SENTINELAYER_SECRET_SINK_FILE || "").trim();
|
|
67
|
-
if (secretSinkFile) {
|
|
68
|
-
try {
|
|
69
|
-
fs.appendFileSync(secretSinkFile, `${normalizedRepo}|${secretName}|${secretValue}\n`, "utf-8");
|
|
70
|
-
return { ok: true, repo: normalizedRepo, secretName, method: "sink-file" };
|
|
71
|
-
} catch (error) {
|
|
72
|
-
return { ok: false, reason: `Failed to write SENTINELAYER_SECRET_SINK_FILE: ${error instanceof Error ? error.message : String(error)}` };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
ensureGhCliAvailable(ghCommand);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const result = spawnSync(ghCommand, ["secret", "set", secretName, "--repo", normalizedRepo], {
|
|
83
|
-
encoding: "utf-8",
|
|
84
|
-
input: `${secretValue}\n`,
|
|
85
|
-
});
|
|
86
|
-
if (result.status !== 0) {
|
|
87
|
-
return { ok: false, reason: String(result.stderr || result.stdout || "gh secret set failed").trim() };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const verifyResult = spawnSync(ghCommand, ["secret", "list", "--repo", normalizedRepo], {
|
|
91
|
-
encoding: "utf-8",
|
|
92
|
-
});
|
|
93
|
-
if (verifyResult.status !== 0) {
|
|
94
|
-
return { ok: false, reason: String(verifyResult.stderr || verifyResult.stdout || "gh secret list failed").trim() };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const listedSecrets = String(verifyResult.stdout || "");
|
|
98
|
-
const escapedSecretName = String(secretName || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
99
|
-
const secretRegex = new RegExp(`(^|\\r?\\n)\\s*${escapedSecretName}(\\s|$)`, "m");
|
|
100
|
-
if (!secretRegex.test(listedSecrets)) {
|
|
101
|
-
return { ok: false, reason: `Secret '${secretName}' was not visible in gh secret list output after injection.` };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return { ok: true, repo: normalizedRepo, secretName, method: "gh-cli" };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export { detectRepoSlug };
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
|
|
5
|
+
function getGhCommand() {
|
|
6
|
+
return String(process.env.SENTINELAYER_GH_BIN || "").trim() || "gh";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeRepoSlug(value) {
|
|
10
|
+
return String(value || "").trim().replace(/\.git$/i, "");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isValidRepoSlug(value) {
|
|
14
|
+
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalizeRepoSlug(value));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isValidSecretName(value) {
|
|
18
|
+
return /^[A-Z][A-Z0-9_]{1,127}$/.test(String(value || "").trim());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureGhCliAvailable(ghCommand) {
|
|
22
|
+
const ghVersion = spawnSync(ghCommand, ["--version"], { encoding: "utf-8" });
|
|
23
|
+
if (ghVersion.status !== 0) {
|
|
24
|
+
throw new Error("GitHub CLI (gh) is not installed or not in PATH.");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function detectRepoSlug(cwd) {
|
|
29
|
+
const ghCommand = getGhCommand();
|
|
30
|
+
const result = spawnSync(ghCommand, ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], {
|
|
31
|
+
cwd,
|
|
32
|
+
encoding: "utf-8",
|
|
33
|
+
});
|
|
34
|
+
if (result.status === 0 && result.stdout) {
|
|
35
|
+
return String(result.stdout).trim();
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function setupSecrets({ repoSlug, secretName, secretValue, dryRun }) {
|
|
41
|
+
const normalizedRepo = normalizeRepoSlug(repoSlug);
|
|
42
|
+
const ghCommand = getGhCommand();
|
|
43
|
+
|
|
44
|
+
if (!isValidRepoSlug(normalizedRepo)) {
|
|
45
|
+
return { ok: false, reason: "Invalid repo format. Use owner/repo." };
|
|
46
|
+
}
|
|
47
|
+
if (!isValidSecretName(secretName)) {
|
|
48
|
+
return { ok: false, reason: `Invalid secret name: ${secretName}. Must match /^[A-Z][A-Z0-9_]{1,127}$/.` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (dryRun) {
|
|
52
|
+
return {
|
|
53
|
+
ok: true,
|
|
54
|
+
dryRun: true,
|
|
55
|
+
repo: normalizedRepo,
|
|
56
|
+
secretName,
|
|
57
|
+
instructions: [
|
|
58
|
+
`gh secret set ${secretName} --repo ${normalizedRepo}`,
|
|
59
|
+
`# Paste your SentinelLayer token when prompted`,
|
|
60
|
+
`gh secret list --repo ${normalizedRepo}`,
|
|
61
|
+
`# Verify ${secretName} appears in the list`,
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const secretSinkFile = String(process.env.SENTINELAYER_SECRET_SINK_FILE || "").trim();
|
|
67
|
+
if (secretSinkFile) {
|
|
68
|
+
try {
|
|
69
|
+
fs.appendFileSync(secretSinkFile, `${normalizedRepo}|${secretName}|${secretValue}\n`, "utf-8");
|
|
70
|
+
return { ok: true, repo: normalizedRepo, secretName, method: "sink-file" };
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return { ok: false, reason: `Failed to write SENTINELAYER_SECRET_SINK_FILE: ${error instanceof Error ? error.message : String(error)}` };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
ensureGhCliAvailable(ghCommand);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = spawnSync(ghCommand, ["secret", "set", secretName, "--repo", normalizedRepo], {
|
|
83
|
+
encoding: "utf-8",
|
|
84
|
+
input: `${secretValue}\n`,
|
|
85
|
+
});
|
|
86
|
+
if (result.status !== 0) {
|
|
87
|
+
return { ok: false, reason: String(result.stderr || result.stdout || "gh secret set failed").trim() };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const verifyResult = spawnSync(ghCommand, ["secret", "list", "--repo", normalizedRepo], {
|
|
91
|
+
encoding: "utf-8",
|
|
92
|
+
});
|
|
93
|
+
if (verifyResult.status !== 0) {
|
|
94
|
+
return { ok: false, reason: String(verifyResult.stderr || verifyResult.stdout || "gh secret list failed").trim() };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const listedSecrets = String(verifyResult.stdout || "");
|
|
98
|
+
const escapedSecretName = String(secretName || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
99
|
+
const secretRegex = new RegExp(`(^|\\r?\\n)\\s*${escapedSecretName}(\\s|$)`, "m");
|
|
100
|
+
if (!secretRegex.test(listedSecrets)) {
|
|
101
|
+
return { ok: false, reason: `Secret '${secretName}' was not visible in gh secret list output after injection.` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { ok: true, repo: normalizedRepo, secretName, method: "gh-cli" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { detectRepoSlug };
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
import { STUCK_THRESHOLDS } from "../agents/jules/pulse.js";
|
|
7
|
+
import { createAgentEvent } from "../events/schema.js";
|
|
8
|
+
import { resolveSessionPaths } from "./paths.js";
|
|
9
|
+
import { emitContextBriefing } from "./recap.js";
|
|
10
|
+
import { appendToStream } from "./stream.js";
|
|
11
|
+
|
|
12
|
+
const AGENT_SNAPSHOT_SCHEMA_VERSION = "1.0.0";
|
|
13
|
+
|
|
14
|
+
const AGENT_ROLES = new Set([
|
|
15
|
+
"coder",
|
|
16
|
+
"reviewer",
|
|
17
|
+
"tester",
|
|
18
|
+
"daemon",
|
|
19
|
+
"observer",
|
|
20
|
+
"persona",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const AGENT_STATUSES = new Set([
|
|
24
|
+
"coding",
|
|
25
|
+
"reviewing",
|
|
26
|
+
"testing",
|
|
27
|
+
"idle",
|
|
28
|
+
"blocked",
|
|
29
|
+
"watching",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const LEAVE_REASONS = new Set([
|
|
33
|
+
"task_complete",
|
|
34
|
+
"error",
|
|
35
|
+
"timeout",
|
|
36
|
+
"manual",
|
|
37
|
+
"killed",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function normalizeString(value) {
|
|
41
|
+
return String(value || "").trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeIsoTimestamp(value, fallbackIso = new Date().toISOString()) {
|
|
45
|
+
const normalized = normalizeString(value);
|
|
46
|
+
if (!normalized) {
|
|
47
|
+
return fallbackIso;
|
|
48
|
+
}
|
|
49
|
+
const epoch = Date.parse(normalized);
|
|
50
|
+
if (!Number.isFinite(epoch)) {
|
|
51
|
+
return fallbackIso;
|
|
52
|
+
}
|
|
53
|
+
return new Date(epoch).toISOString();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeRole(value) {
|
|
57
|
+
const normalized = normalizeString(value).toLowerCase();
|
|
58
|
+
if (!AGENT_ROLES.has(normalized)) {
|
|
59
|
+
throw new Error(`role must be one of: ${[...AGENT_ROLES].join(", ")}.`);
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeStatus(value, fallbackValue = "idle") {
|
|
65
|
+
const normalized = normalizeString(value).toLowerCase();
|
|
66
|
+
if (!normalized) {
|
|
67
|
+
return fallbackValue;
|
|
68
|
+
}
|
|
69
|
+
if (!AGENT_STATUSES.has(normalized)) {
|
|
70
|
+
throw new Error(`status must be one of: ${[...AGENT_STATUSES].join(", ")}.`);
|
|
71
|
+
}
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeLeaveReason(value) {
|
|
76
|
+
const normalized = normalizeString(value).toLowerCase();
|
|
77
|
+
if (!normalized) {
|
|
78
|
+
return "manual";
|
|
79
|
+
}
|
|
80
|
+
if (!LEAVE_REASONS.has(normalized)) {
|
|
81
|
+
throw new Error(`reason must be one of: ${[...LEAVE_REASONS].join(", ")}.`);
|
|
82
|
+
}
|
|
83
|
+
return normalized;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sanitizePrefix(value) {
|
|
87
|
+
const normalized = normalizeString(value)
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
90
|
+
.replace(/^-+|-+$/g, "");
|
|
91
|
+
if (!normalized) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
return normalized.slice(0, 12);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function deriveModelPrefix(modelName) {
|
|
98
|
+
const normalized = normalizeString(modelName).toLowerCase();
|
|
99
|
+
if (!normalized) {
|
|
100
|
+
return "agent";
|
|
101
|
+
}
|
|
102
|
+
if (normalized.includes("claude")) return "claude";
|
|
103
|
+
if (normalized.includes("codex")) return "codex";
|
|
104
|
+
if (normalized.includes("gpt")) return "codex";
|
|
105
|
+
if (normalized.includes("sonnet")) return "sonnet";
|
|
106
|
+
if (normalized.includes("senti") || normalized.includes("sentinel")) return "senti";
|
|
107
|
+
|
|
108
|
+
const token = normalized.split(/[\s:/_-]+/).find(Boolean) || normalized;
|
|
109
|
+
return sanitizePrefix(token) || "agent";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeAgentSnapshot(snapshot = {}, nowIso = new Date().toISOString()) {
|
|
113
|
+
return {
|
|
114
|
+
schemaVersion: AGENT_SNAPSHOT_SCHEMA_VERSION,
|
|
115
|
+
sessionId: normalizeString(snapshot.sessionId),
|
|
116
|
+
agentId: normalizeString(snapshot.agentId),
|
|
117
|
+
model: normalizeString(snapshot.model) || "unknown",
|
|
118
|
+
role: normalizeRole(snapshot.role || "observer"),
|
|
119
|
+
status: normalizeStatus(snapshot.status, "idle"),
|
|
120
|
+
detail: normalizeString(snapshot.detail) || "",
|
|
121
|
+
file: normalizeString(snapshot.file) || null,
|
|
122
|
+
joinedAt: normalizeIsoTimestamp(snapshot.joinedAt, nowIso),
|
|
123
|
+
lastActivityAt: normalizeIsoTimestamp(snapshot.lastActivityAt, nowIso),
|
|
124
|
+
leftAt: snapshot.leftAt ? normalizeIsoTimestamp(snapshot.leftAt, nowIso) : null,
|
|
125
|
+
leaveReason: snapshot.leaveReason ? normalizeLeaveReason(snapshot.leaveReason) : null,
|
|
126
|
+
active: snapshot.active !== false,
|
|
127
|
+
updatedAt: normalizeIsoTimestamp(snapshot.updatedAt, nowIso),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function readAgentSnapshot(snapshotPath) {
|
|
132
|
+
try {
|
|
133
|
+
const raw = await fsp.readFile(snapshotPath, "utf-8");
|
|
134
|
+
const parsed = JSON.parse(raw);
|
|
135
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return parsed;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function writeAgentSnapshot(snapshotPath, snapshot) {
|
|
148
|
+
await fsp.mkdir(path.dirname(snapshotPath), { recursive: true });
|
|
149
|
+
const tmpPath = `${snapshotPath}.${process.pid}.${Date.now()}.tmp`;
|
|
150
|
+
await fsp.writeFile(tmpPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf-8");
|
|
151
|
+
await fsp.rename(tmpPath, snapshotPath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function emitAgentEvent(sessionId, event, payload, { targetPath = process.cwd() } = {}) {
|
|
155
|
+
const envelope = createAgentEvent({
|
|
156
|
+
event,
|
|
157
|
+
agentId: payload.agentId,
|
|
158
|
+
sessionId,
|
|
159
|
+
payload,
|
|
160
|
+
});
|
|
161
|
+
await appendToStream(sessionId, envelope, { targetPath });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildAgentSnapshotPath(paths, agentId) {
|
|
165
|
+
const normalizedAgentId = normalizeString(agentId);
|
|
166
|
+
if (!normalizedAgentId) {
|
|
167
|
+
throw new Error("agentId is required.");
|
|
168
|
+
}
|
|
169
|
+
return path.join(paths.agentsDir, `${normalizedAgentId}.json`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function generateAgentId(modelName) {
|
|
173
|
+
const prefix = deriveModelPrefix(modelName);
|
|
174
|
+
const suffix = randomBytes(2).toString("hex");
|
|
175
|
+
return `${prefix}-${suffix}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function registerAgent(
|
|
179
|
+
sessionId,
|
|
180
|
+
{ agentId = "", model = "", role = "observer", targetPath = process.cwd() } = {}
|
|
181
|
+
) {
|
|
182
|
+
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
183
|
+
const nowIso = new Date().toISOString();
|
|
184
|
+
const resolvedAgentId = normalizeString(agentId) || generateAgentId(model);
|
|
185
|
+
const snapshotPath = buildAgentSnapshotPath(paths, resolvedAgentId);
|
|
186
|
+
|
|
187
|
+
const snapshot = normalizeAgentSnapshot(
|
|
188
|
+
{
|
|
189
|
+
sessionId: paths.sessionId,
|
|
190
|
+
agentId: resolvedAgentId,
|
|
191
|
+
model: normalizeString(model) || "unknown",
|
|
192
|
+
role,
|
|
193
|
+
status: "idle",
|
|
194
|
+
detail: "",
|
|
195
|
+
file: null,
|
|
196
|
+
joinedAt: nowIso,
|
|
197
|
+
lastActivityAt: nowIso,
|
|
198
|
+
leftAt: null,
|
|
199
|
+
leaveReason: null,
|
|
200
|
+
active: true,
|
|
201
|
+
updatedAt: nowIso,
|
|
202
|
+
},
|
|
203
|
+
nowIso
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await writeAgentSnapshot(snapshotPath, snapshot);
|
|
207
|
+
await emitAgentEvent(paths.sessionId, "agent_join", {
|
|
208
|
+
agentId: snapshot.agentId,
|
|
209
|
+
model: snapshot.model,
|
|
210
|
+
role: snapshot.role,
|
|
211
|
+
status: snapshot.status,
|
|
212
|
+
}, { targetPath });
|
|
213
|
+
if (normalizeString(snapshot.agentId).toLowerCase() !== "senti") {
|
|
214
|
+
await emitContextBriefing(paths.sessionId, {
|
|
215
|
+
forAgentId: snapshot.agentId,
|
|
216
|
+
targetPath,
|
|
217
|
+
}).catch(() => {});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
...snapshot,
|
|
222
|
+
snapshotPath,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function heartbeatAgent(
|
|
227
|
+
sessionId,
|
|
228
|
+
agentId,
|
|
229
|
+
{ status = "", detail = "", file = "", targetPath = process.cwd() } = {}
|
|
230
|
+
) {
|
|
231
|
+
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
232
|
+
const nowIso = new Date().toISOString();
|
|
233
|
+
const snapshotPath = buildAgentSnapshotPath(paths, agentId);
|
|
234
|
+
const existing = await readAgentSnapshot(snapshotPath);
|
|
235
|
+
if (!existing) {
|
|
236
|
+
throw new Error(`Agent '${normalizeString(agentId)}' is not registered in session '${paths.sessionId}'.`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const snapshot = normalizeAgentSnapshot(
|
|
240
|
+
{
|
|
241
|
+
...existing,
|
|
242
|
+
status: normalizeStatus(status, normalizeStatus(existing.status || "idle")),
|
|
243
|
+
detail: normalizeString(detail) || normalizeString(existing.detail),
|
|
244
|
+
file: normalizeString(file) || normalizeString(existing.file) || null,
|
|
245
|
+
lastActivityAt: nowIso,
|
|
246
|
+
updatedAt: nowIso,
|
|
247
|
+
active: true,
|
|
248
|
+
},
|
|
249
|
+
nowIso
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
await writeAgentSnapshot(snapshotPath, snapshot);
|
|
253
|
+
return {
|
|
254
|
+
...snapshot,
|
|
255
|
+
snapshotPath,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function unregisterAgent(
|
|
260
|
+
sessionId,
|
|
261
|
+
agentId,
|
|
262
|
+
{ reason = "manual", targetPath = process.cwd() } = {}
|
|
263
|
+
) {
|
|
264
|
+
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
265
|
+
const nowIso = new Date().toISOString();
|
|
266
|
+
const snapshotPath = buildAgentSnapshotPath(paths, agentId);
|
|
267
|
+
const existing = await readAgentSnapshot(snapshotPath);
|
|
268
|
+
if (!existing) {
|
|
269
|
+
throw new Error(`Agent '${normalizeString(agentId)}' is not registered in session '${paths.sessionId}'.`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const normalizedReason = normalizeLeaveReason(reason);
|
|
273
|
+
const snapshot = normalizeAgentSnapshot(
|
|
274
|
+
{
|
|
275
|
+
...existing,
|
|
276
|
+
active: false,
|
|
277
|
+
leftAt: nowIso,
|
|
278
|
+
leaveReason: normalizedReason,
|
|
279
|
+
updatedAt: nowIso,
|
|
280
|
+
},
|
|
281
|
+
nowIso
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
await writeAgentSnapshot(snapshotPath, snapshot);
|
|
285
|
+
await emitAgentEvent(paths.sessionId, "agent_leave", {
|
|
286
|
+
agentId: snapshot.agentId,
|
|
287
|
+
reason: normalizedReason,
|
|
288
|
+
role: snapshot.role,
|
|
289
|
+
model: snapshot.model,
|
|
290
|
+
}, { targetPath });
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
...snapshot,
|
|
294
|
+
snapshotPath,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function listAgents(
|
|
299
|
+
sessionId,
|
|
300
|
+
{ targetPath = process.cwd(), includeInactive = true } = {}
|
|
301
|
+
) {
|
|
302
|
+
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
303
|
+
let entries = [];
|
|
304
|
+
try {
|
|
305
|
+
entries = await fsp.readdir(paths.agentsDir, { withFileTypes: true });
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const agents = [];
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
316
|
+
const snapshotPath = path.join(paths.agentsDir, entry.name);
|
|
317
|
+
const raw = await readAgentSnapshot(snapshotPath);
|
|
318
|
+
if (!raw) continue;
|
|
319
|
+
const normalized = normalizeAgentSnapshot(raw);
|
|
320
|
+
if (normalized.sessionId !== paths.sessionId) continue;
|
|
321
|
+
if (!includeInactive && normalized.active === false) continue;
|
|
322
|
+
agents.push({
|
|
323
|
+
...normalized,
|
|
324
|
+
snapshotPath,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
agents.sort((left, right) => right.lastActivityAt.localeCompare(left.lastActivityAt));
|
|
329
|
+
return agents;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function detectStaleAgents(
|
|
333
|
+
agents,
|
|
334
|
+
{
|
|
335
|
+
idleThresholdSeconds = Number(STUCK_THRESHOLDS?.noToolCallSeconds || 90),
|
|
336
|
+
nowIso = new Date().toISOString(),
|
|
337
|
+
} = {}
|
|
338
|
+
) {
|
|
339
|
+
const normalizedThreshold = Math.max(1, Math.floor(Number(idleThresholdSeconds || 90)));
|
|
340
|
+
const nowEpoch = Date.parse(normalizeIsoTimestamp(nowIso, new Date().toISOString()));
|
|
341
|
+
if (!Number.isFinite(nowEpoch)) {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
const list = Array.isArray(agents) ? agents : [];
|
|
345
|
+
return list
|
|
346
|
+
.map((agent) => normalizeAgentSnapshot(agent, nowIso))
|
|
347
|
+
.filter((agent) => agent.active !== false)
|
|
348
|
+
.map((agent) => {
|
|
349
|
+
const activityEpoch = Date.parse(normalizeIsoTimestamp(agent.lastActivityAt, agent.joinedAt || nowIso));
|
|
350
|
+
const idleSeconds = Number.isFinite(activityEpoch)
|
|
351
|
+
? Math.max(0, Math.floor((nowEpoch - activityEpoch) / 1000))
|
|
352
|
+
: normalizedThreshold + 1;
|
|
353
|
+
return {
|
|
354
|
+
...agent,
|
|
355
|
+
idleSeconds,
|
|
356
|
+
};
|
|
357
|
+
})
|
|
358
|
+
.filter((agent) => agent.idleSeconds >= normalizedThreshold);
|
|
359
|
+
}
|