agent-security-lens 0.1.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/.env.example +10 -0
- package/.mcp/server.json +42 -0
- package/CHANGELOG.md +17 -0
- package/LICENSE +17 -0
- package/PRIVACY.md +37 -0
- package/README.md +150 -0
- package/RELEASE-MANIFEST.json +449 -0
- package/SECURITY.md +24 -0
- package/apps/mcp-server/agent-security-lens-mcp.mjs +441 -0
- package/bin/agent-security-lens.mjs +117 -0
- package/data/ecosystems/agent-candidates.json +230 -0
- package/data/intelligence/components.json +22989 -0
- package/data/intelligence/security-evaluation-standard.json +221 -0
- package/data/recommendations/core/recommendations.json +256 -0
- package/data/trust/signal-taxonomy.json +107 -0
- package/docs/asl-agent-component-safety-standard-v0.2.md +56 -0
- package/examples/dot-hermes/.hermes/config.json +17 -0
- package/examples/dot-openclaw/.openclaw/openclaw.json +17 -0
- package/examples/hermes-like/.env.example +2 -0
- package/examples/hermes-like/config.json +37 -0
- package/examples/hermes-like/optional-mcps/github-tools.json +8 -0
- package/examples/hermes-like/skills/openclaw-imports/browser-skill/SKILL.md +8 -0
- package/examples/openclaw-like/.env.example +2 -0
- package/examples/openclaw-like/AGENTS.md +7 -0
- package/examples/openclaw-like/openclaw.json +28 -0
- package/examples/openclaw-like/workspace/skills/browser-control/SKILL.md +8 -0
- package/llms.txt +25 -0
- package/package.json +50 -0
- package/profiles/generic-agent/profile.json +19 -0
- package/profiles/hermes-like/profile.json +23 -0
- package/profiles/mcp-server/profile.json +18 -0
- package/profiles/openclaw-like/profile.json +22 -0
- package/profiles/skill-runtime/profile.json +19 -0
- package/rule-packs/core/rules.json +82 -0
- package/rule-packs/hermes/rules.json +44 -0
- package/rule-packs/mcp/rules.json +65 -0
- package/rule-packs/openclaw/rules.json +46 -0
- package/rule-packs/skills/rules.json +45 -0
- package/schemas/agent-install-decision.schema.json +432 -0
- package/schemas/agent-usage-event.schema.json +45 -0
- package/schemas/assessment-result.schema.json +361 -0
- package/schemas/comparison-result.schema.json +113 -0
- package/schemas/component-alternative-graph.schema.json +187 -0
- package/schemas/component-intelligence.schema.json +93 -0
- package/schemas/decision-feedback.schema.json +49 -0
- package/schemas/ecosystem-candidate-registry.schema.json +98 -0
- package/schemas/profile.schema.json +65 -0
- package/schemas/recommendation-pack.schema.json +114 -0
- package/schemas/rule-pack.schema.json +113 -0
- package/schemas/trust-signal-taxonomy.schema.json +68 -0
- package/scripts/verify-examples.mjs +121 -0
- package/scripts/verify-mcp-server.mjs +278 -0
- package/scripts/verify-registry.mjs +264 -0
- package/server.json +42 -0
- package/src/assessment/assess.mjs +108 -0
- package/src/assessment/discover-targets.mjs +127 -0
- package/src/assessment/risk-domains.mjs +83 -0
- package/src/assessment/summarize.mjs +57 -0
- package/src/core/files.mjs +74 -0
- package/src/intelligence/cloud-client.mjs +260 -0
- package/src/intelligence/component-intelligence.mjs +358 -0
- package/src/intelligence/decision-engine.mjs +772 -0
- package/src/intelligence/finding-context.mjs +180 -0
- package/src/intelligence/safety-score-v0.2.mjs +294 -0
- package/src/observations/json-observations.mjs +211 -0
- package/src/observations/observation-rules.mjs +157 -0
- package/src/profiles/load-profiles.mjs +130 -0
- package/src/recommendations/component-alternative-graph.mjs +94 -0
- package/src/recommendations/load-recommendations.mjs +17 -0
- package/src/recommendations/match-recommendations.mjs +79 -0
- package/src/report/comparison-console.mjs +71 -0
- package/src/report/console.mjs +103 -0
- package/src/report/markdown.mjs +145 -0
- package/src/results/compare-results.mjs +106 -0
- package/src/results/save-result.mjs +29 -0
- package/src/rules/load-rules.mjs +22 -0
- package/src/rules/match-rules.mjs +99 -0
- package/src/rules/supersedes.mjs +39 -0
- package/src/store/assessment-store.mjs +78 -0
- package/src/trust/derive-trust-signals.mjs +73 -0
- package/src/trust/load-trust-signals.mjs +17 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
const DEFAULT_API_URL = "https://api.agentsecuritylens.com";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 3500;
|
|
3
|
+
|
|
4
|
+
const SECRET_PATTERNS = [
|
|
5
|
+
/\b([A-Za-z_]*API[A-Za-z_]*KEY|TOKEN|SECRET|PASSWORD|PRIVATE_KEY|ACCESS_KEY)\s*[:=]\s*["']?[^"',\s]+/gi,
|
|
6
|
+
/\b(sk-[A-Za-z0-9_-]{12,})\b/g,
|
|
7
|
+
/\b(ghp_[A-Za-z0-9_]{20,})\b/g,
|
|
8
|
+
/\b(xox[baprs]-[A-Za-z0-9-]{20,})\b/g
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function env(name) {
|
|
12
|
+
return process.env[name];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeApiUrl(value) {
|
|
16
|
+
const text = String(value || "").trim();
|
|
17
|
+
if (!text) return null;
|
|
18
|
+
return text.replace(/\/+$/, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function splitApiUrls(value) {
|
|
22
|
+
return String(value || "")
|
|
23
|
+
.split(/[\s,]+/)
|
|
24
|
+
.map((item) => normalizeApiUrl(item))
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function apiEndpoints() {
|
|
29
|
+
const primary = normalizeApiUrl(env("ASL_API_URL"));
|
|
30
|
+
const explicitList = splitApiUrls(env("ASL_API_URLS"));
|
|
31
|
+
|
|
32
|
+
const ordered = [];
|
|
33
|
+
ordered.push(...explicitList);
|
|
34
|
+
if (primary) ordered.push(primary);
|
|
35
|
+
ordered.push(DEFAULT_API_URL);
|
|
36
|
+
|
|
37
|
+
return [...new Set(ordered)];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function apiUrl() {
|
|
41
|
+
return apiEndpoints()[0] || DEFAULT_API_URL;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function cloudIntelligenceEnabled() {
|
|
45
|
+
const mode = (env("ASL_MODE") || "online").toLowerCase();
|
|
46
|
+
return mode !== "local" && env("ASL_DISABLE_CLOUD") !== "1";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function limitString(value, max = 4000) {
|
|
50
|
+
if (value.length <= max) return value;
|
|
51
|
+
return `${value.slice(0, max)}... [truncated by AgentSecurityLens MCP]`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function redactString(value) {
|
|
55
|
+
let redacted = value;
|
|
56
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
57
|
+
redacted = redacted.replace(pattern, "[REDACTED]");
|
|
58
|
+
}
|
|
59
|
+
return limitString(redacted);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sanitize(value) {
|
|
63
|
+
if (value == null) return value;
|
|
64
|
+
if (typeof value === "string") return redactString(value);
|
|
65
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
66
|
+
if (Array.isArray(value)) return value.slice(0, 50).map((item) => sanitize(item));
|
|
67
|
+
if (typeof value === "object") {
|
|
68
|
+
const output = {};
|
|
69
|
+
for (const [key, item] of Object.entries(value)) {
|
|
70
|
+
if (/secret|token|password|private[_-]?key|credential|cookie/i.test(key)) {
|
|
71
|
+
output[key] = "[REDACTED]";
|
|
72
|
+
} else {
|
|
73
|
+
output[key] = sanitize(item);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return output;
|
|
77
|
+
}
|
|
78
|
+
return String(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clientProfile() {
|
|
82
|
+
return {
|
|
83
|
+
name: "agent-security-lens-mcp",
|
|
84
|
+
agent_id: env("ASL_AGENT_ID") || "agent-security-lens-mcp",
|
|
85
|
+
agent_name: env("ASL_AGENT_NAME") || "AgentSecurityLens MCP",
|
|
86
|
+
client_type: "agent",
|
|
87
|
+
mode: env("ASL_MODE") || "online",
|
|
88
|
+
protocol: "mcp",
|
|
89
|
+
tier: env("ASL_CLIENT_TIER") || "free"
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cloudHeaders(includeJson = false) {
|
|
94
|
+
return {
|
|
95
|
+
...(includeJson ? { "content-type": "application/json" } : {}),
|
|
96
|
+
"user-agent": "AgentSecurityLens-MCP/0.1",
|
|
97
|
+
...(env("ASL_API_KEY") ? { authorization: `Bearer ${env("ASL_API_KEY")}` } : {})
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseResponseText(text) {
|
|
102
|
+
if (!text) return {};
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(text);
|
|
105
|
+
} catch {
|
|
106
|
+
return { raw: text };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function buildCloudPayload(input = {}) {
|
|
111
|
+
return {
|
|
112
|
+
schema_version: "0.1.0",
|
|
113
|
+
client: clientProfile(),
|
|
114
|
+
component: sanitize({
|
|
115
|
+
name: input.component_name || null,
|
|
116
|
+
type: input.component_type || "unknown",
|
|
117
|
+
source_url: input.source_url || null,
|
|
118
|
+
install_command: input.install_command || null,
|
|
119
|
+
manifest: input.manifest || null,
|
|
120
|
+
package_name: input.package_name || null,
|
|
121
|
+
version: input.version || null,
|
|
122
|
+
registry: input.registry || null,
|
|
123
|
+
source_type: input.source_type || null,
|
|
124
|
+
stars: input.stars || input.github_stars || null,
|
|
125
|
+
community_signals: input.community_signals || null,
|
|
126
|
+
trust_signals: input.trust_signals || null,
|
|
127
|
+
planned_use: input.planned_use || null,
|
|
128
|
+
requested_permissions: input.requested_permissions || null,
|
|
129
|
+
agent_context: input.agent_context || null
|
|
130
|
+
}),
|
|
131
|
+
privacy_policy: {
|
|
132
|
+
send_private_file_contents: false,
|
|
133
|
+
send_secrets: false,
|
|
134
|
+
redaction: "client-side-best-effort"
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function requestJson({ method, path, payload = null }) {
|
|
140
|
+
const endpoints = apiEndpoints();
|
|
141
|
+
if (!cloudIntelligenceEnabled()) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
reason: "cloud_disabled",
|
|
145
|
+
api_url: apiUrl(),
|
|
146
|
+
attempted_api_urls: endpoints
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const attempts = [];
|
|
151
|
+
const timeoutMs = Number(env("ASL_API_TIMEOUT_MS") || DEFAULT_TIMEOUT_MS);
|
|
152
|
+
|
|
153
|
+
for (const [index, endpoint] of endpoints.entries()) {
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetch(`${endpoint}${path}`, {
|
|
158
|
+
method,
|
|
159
|
+
headers: cloudHeaders(method !== "GET"),
|
|
160
|
+
...(method === "GET" ? {} : { body: JSON.stringify(payload) }),
|
|
161
|
+
signal: controller.signal
|
|
162
|
+
});
|
|
163
|
+
const text = await response.text();
|
|
164
|
+
const data = parseResponseText(text);
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
attempts.push({
|
|
167
|
+
api_url: endpoint,
|
|
168
|
+
reason: `http_${response.status}`,
|
|
169
|
+
error: data?.error || data?.message || data?.raw || text
|
|
170
|
+
});
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
api_url: endpoint,
|
|
176
|
+
attempted_api_urls: endpoints.slice(0, index + 1),
|
|
177
|
+
fallback_used: index > 0,
|
|
178
|
+
data
|
|
179
|
+
};
|
|
180
|
+
} catch (error) {
|
|
181
|
+
attempts.push({
|
|
182
|
+
api_url: endpoint,
|
|
183
|
+
reason: error?.name === "AbortError" ? "timeout" : "request_failed",
|
|
184
|
+
error: error?.message || String(error)
|
|
185
|
+
});
|
|
186
|
+
} finally {
|
|
187
|
+
clearTimeout(timeout);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const last = attempts.at(-1);
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
reason: last?.reason || "all_endpoints_failed",
|
|
195
|
+
api_url: endpoints[0] || DEFAULT_API_URL,
|
|
196
|
+
attempted_api_urls: attempts.map((attempt) => attempt.api_url),
|
|
197
|
+
attempts,
|
|
198
|
+
error: last?.error || "No ASL API endpoint responded successfully."
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function postJson(path, payload) {
|
|
203
|
+
return requestJson({ method: "POST", path, payload });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function getJson(path) {
|
|
207
|
+
return requestJson({ method: "GET", path });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function queryCloudReview(input = {}) {
|
|
211
|
+
return postJson("/v1/review-before-install", buildCloudPayload(input));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function queryCloudStatus() {
|
|
215
|
+
return getJson("/v1/status");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function submitUnknownToCloud(input = {}, localReview = null) {
|
|
219
|
+
return postJson("/v1/unknown-components", {
|
|
220
|
+
...buildCloudPayload(input),
|
|
221
|
+
local_review: localReview
|
|
222
|
+
? {
|
|
223
|
+
decision: localReview.decision,
|
|
224
|
+
risk_level: localReview.risk_level,
|
|
225
|
+
risk_signals: localReview.risk_signals,
|
|
226
|
+
trust_score: localReview.trust_score
|
|
227
|
+
}
|
|
228
|
+
: null
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function submitUsageEventToCloud(event = {}) {
|
|
233
|
+
return postJson("/v1/usage-events", {
|
|
234
|
+
schema_version: "0.1.0",
|
|
235
|
+
client: clientProfile(),
|
|
236
|
+
event: sanitize(event),
|
|
237
|
+
privacy_policy: {
|
|
238
|
+
send_private_file_contents: false,
|
|
239
|
+
send_secrets: false,
|
|
240
|
+
redaction: "client-side-best-effort"
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function submitFeedbackToCloud(feedback = {}) {
|
|
246
|
+
return postJson("/v1/decision-feedback", {
|
|
247
|
+
schema_version: "0.1.0",
|
|
248
|
+
client: clientProfile(),
|
|
249
|
+
feedback: sanitize(feedback),
|
|
250
|
+
privacy_policy: {
|
|
251
|
+
send_private_file_contents: false,
|
|
252
|
+
send_secrets: false,
|
|
253
|
+
redaction: "client-side-best-effort"
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function queryResearchStatus(submissionId) {
|
|
259
|
+
return getJson(`/v1/research-status?id=${encodeURIComponent(submissionId || "")}`);
|
|
260
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
cloudIntelligenceEnabled,
|
|
7
|
+
queryCloudReview,
|
|
8
|
+
queryResearchStatus,
|
|
9
|
+
submitFeedbackToCloud,
|
|
10
|
+
submitUnknownToCloud,
|
|
11
|
+
submitUsageEventToCloud
|
|
12
|
+
} from "./cloud-client.mjs";
|
|
13
|
+
import {
|
|
14
|
+
buildInstallDecision,
|
|
15
|
+
findKnownComponent as findKnownComponentRecord,
|
|
16
|
+
riskLevel,
|
|
17
|
+
nextActionFor
|
|
18
|
+
} from "./decision-engine.mjs";
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const ROOT = join(__dirname, "..", "..");
|
|
22
|
+
const COMPONENT_DB_PATH = join(ROOT, "data", "intelligence", "components.json");
|
|
23
|
+
const CANDIDATE_CATALOG_PATH = join(ROOT, "data", "intelligence", "candidates", "2026-06-07-component-catalog.json");
|
|
24
|
+
const OPS_SNAPSHOT_PATH = join(ROOT, "data", "operations", "ops-snapshot.json");
|
|
25
|
+
const LOCAL_QUEUE_ROOT = process.env.ASL_LOCAL_QUEUE_DIR || join(process.cwd(), ".agentsecuritylens");
|
|
26
|
+
const UNKNOWN_QUEUE_DIR = join(LOCAL_QUEUE_ROOT, "unknown-components");
|
|
27
|
+
const USAGE_QUEUE_DIR = join(LOCAL_QUEUE_ROOT, "usage-events");
|
|
28
|
+
const FEEDBACK_QUEUE_DIR = join(LOCAL_QUEUE_ROOT, "decision-feedback");
|
|
29
|
+
|
|
30
|
+
export async function loadComponentDatabase() {
|
|
31
|
+
const content = await readFile(COMPONENT_DB_PATH, "utf8");
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function loadCandidateCatalog() {
|
|
36
|
+
try {
|
|
37
|
+
const content = await readFile(CANDIDATE_CATALOG_PATH, "utf8");
|
|
38
|
+
const catalog = JSON.parse(content);
|
|
39
|
+
return Array.isArray(catalog.candidates) ? catalog.candidates : [];
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function findKnownComponent(input) {
|
|
46
|
+
const database = await loadComponentDatabase();
|
|
47
|
+
return findKnownComponentRecord(input, database.components);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function withResolution(review, resolution) {
|
|
51
|
+
return {
|
|
52
|
+
...review,
|
|
53
|
+
resolution
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeCloudReview(cloudData, input, cloudResult) {
|
|
58
|
+
const review = cloudData?.result || cloudData?.decision || cloudData;
|
|
59
|
+
if (!review || typeof review !== "object") return null;
|
|
60
|
+
if (!review.decision || !review.trust_score || !Array.isArray(review.risk_signals)) return null;
|
|
61
|
+
return withResolution(
|
|
62
|
+
{
|
|
63
|
+
schema_version: review.schema_version || "0.1.0",
|
|
64
|
+
schema_id: review.schema_id || "https://agentsecuritylens.dev/schemas/agent-install-decision.schema.json",
|
|
65
|
+
service: "AgentSecurityLens",
|
|
66
|
+
result_type: "agent_install_decision",
|
|
67
|
+
component: review.component || {
|
|
68
|
+
known: Boolean(review.component_id),
|
|
69
|
+
id: review.component_id || null,
|
|
70
|
+
name: input.component_name || null,
|
|
71
|
+
type: input.component_type || "unknown",
|
|
72
|
+
source_url: input.source_url || null
|
|
73
|
+
},
|
|
74
|
+
decision: review.decision,
|
|
75
|
+
trust_score: review.trust_score,
|
|
76
|
+
score_breakdown: review.score_breakdown || null,
|
|
77
|
+
risk_level: review.risk_level || riskLevel(review.trust_score),
|
|
78
|
+
risk_signals: review.risk_signals,
|
|
79
|
+
required_user_confirmation:
|
|
80
|
+
typeof review.required_user_confirmation === "boolean"
|
|
81
|
+
? review.required_user_confirmation
|
|
82
|
+
: ["ask_user", "avoid"].includes(review.decision),
|
|
83
|
+
safe_install_plan: review.safe_install_plan || [],
|
|
84
|
+
alternatives: review.alternatives || [],
|
|
85
|
+
recommended_alternatives: review.recommended_alternatives || [],
|
|
86
|
+
next_action: review.next_action || nextActionFor(review.decision),
|
|
87
|
+
one_step_action: review.one_step_action || null,
|
|
88
|
+
agent_decision_contract: review.agent_decision_contract || null,
|
|
89
|
+
agent_actions: review.agent_actions || [],
|
|
90
|
+
agent_instruction: review.agent_instruction || "Follow the returned decision before installing this component.",
|
|
91
|
+
intelligence_coverage: review.intelligence_coverage || {
|
|
92
|
+
state: review.component?.intelligence_state || (review.component?.known ? "reviewed" : "unknown"),
|
|
93
|
+
reviewed: Boolean(review.component?.reviewed || review.component?.known),
|
|
94
|
+
cataloged: Boolean(review.component?.cataloged),
|
|
95
|
+
source: review.component?.known ? "reviewed_intelligence_database" : "submitted_metadata_inference",
|
|
96
|
+
confidence: review.component?.known ? "high" : "low",
|
|
97
|
+
catalog: null
|
|
98
|
+
},
|
|
99
|
+
unknown_component: review.unknown_component || null
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
source: "asl_cloud",
|
|
103
|
+
api_url: cloudResult.api_url,
|
|
104
|
+
checked_at: new Date().toISOString(),
|
|
105
|
+
local_fallback_used: false
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function buildLocalReview(input = {}, fallbackReason = null) {
|
|
111
|
+
const database = await loadComponentDatabase();
|
|
112
|
+
const candidates = await loadCandidateCatalog();
|
|
113
|
+
return buildInstallDecision({
|
|
114
|
+
input,
|
|
115
|
+
components: database.components,
|
|
116
|
+
candidates,
|
|
117
|
+
resolution: {
|
|
118
|
+
source: cloudIntelligenceEnabled() ? "local_fallback" : "local_only",
|
|
119
|
+
checked_at: new Date().toISOString(),
|
|
120
|
+
local_fallback_used: cloudIntelligenceEnabled(),
|
|
121
|
+
fallback_reason: fallbackReason
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function reviewBeforeInstall(input = {}) {
|
|
127
|
+
if (cloudIntelligenceEnabled()) {
|
|
128
|
+
const cloudResult = await queryCloudReview(input);
|
|
129
|
+
if (cloudResult.ok) {
|
|
130
|
+
const normalized = normalizeCloudReview(cloudResult.data, input, cloudResult);
|
|
131
|
+
if (normalized) return normalized;
|
|
132
|
+
}
|
|
133
|
+
return buildLocalReview(input, cloudResult.reason || "cloud_result_unusable");
|
|
134
|
+
}
|
|
135
|
+
return buildLocalReview(input);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function submitUnknownComponent(input = {}) {
|
|
139
|
+
const localReview = await buildLocalReview(input);
|
|
140
|
+
const cloudResult = await submitUnknownToCloud(input, localReview);
|
|
141
|
+
if (cloudResult.ok) {
|
|
142
|
+
return {
|
|
143
|
+
queued: true,
|
|
144
|
+
source: "asl_cloud",
|
|
145
|
+
api_url: cloudResult.api_url,
|
|
146
|
+
id: cloudResult.data?.id || cloudResult.data?.submission_id || null,
|
|
147
|
+
status: cloudResult.data?.status || "queued",
|
|
148
|
+
intelligence_state: cloudResult.data?.intelligence_state || null,
|
|
149
|
+
decision_at_submission: cloudResult.data?.decision_at_submission || null,
|
|
150
|
+
research_task: cloudResult.data?.research_task || null
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await mkdir(UNKNOWN_QUEUE_DIR, { recursive: true });
|
|
155
|
+
const submittedAt = new Date().toISOString();
|
|
156
|
+
const id = createHash("sha256")
|
|
157
|
+
.update(JSON.stringify({ input, submittedAt }))
|
|
158
|
+
.digest("hex")
|
|
159
|
+
.slice(0, 16);
|
|
160
|
+
const record = {
|
|
161
|
+
schema_version: "0.1.0",
|
|
162
|
+
id,
|
|
163
|
+
status: "queued",
|
|
164
|
+
submitted_at: submittedAt,
|
|
165
|
+
source: "agent-security-mcp",
|
|
166
|
+
intelligence_state: localReview.component?.intelligence_state || localReview.intelligence_coverage?.state || "unknown",
|
|
167
|
+
component: {
|
|
168
|
+
name: input.component_name || null,
|
|
169
|
+
type: input.component_type || "unknown",
|
|
170
|
+
source_url: input.source_url || null,
|
|
171
|
+
install_command: input.install_command || null,
|
|
172
|
+
manifest: input.manifest || null,
|
|
173
|
+
planned_use: input.planned_use || null,
|
|
174
|
+
requested_permissions: input.requested_permissions || null
|
|
175
|
+
},
|
|
176
|
+
review_summary: {
|
|
177
|
+
component: localReview.component || null,
|
|
178
|
+
decision: localReview.decision || null,
|
|
179
|
+
trust_score: localReview.trust_score ?? null,
|
|
180
|
+
risk_level: localReview.risk_level || null,
|
|
181
|
+
risk_signals: localReview.risk_signals || [],
|
|
182
|
+
recommended_alternatives: localReview.recommended_alternatives || localReview.alternatives || [],
|
|
183
|
+
intelligence_coverage: localReview.intelligence_coverage || null
|
|
184
|
+
},
|
|
185
|
+
research_task: {
|
|
186
|
+
submission_id: id,
|
|
187
|
+
status: "queued",
|
|
188
|
+
owner_team:
|
|
189
|
+
input.component_type === "mcp"
|
|
190
|
+
? "mcp-research-team"
|
|
191
|
+
: input.component_type === "skill"
|
|
192
|
+
? "skills-research-team"
|
|
193
|
+
: input.component_type === "agent-framework"
|
|
194
|
+
? "agent-framework-team"
|
|
195
|
+
: "chief-security-agent",
|
|
196
|
+
next_agent_action: {
|
|
197
|
+
action_type: "wait_for_research_or_ask_user",
|
|
198
|
+
automatic_install_retry_allowed: false,
|
|
199
|
+
user_confirmation_required: true,
|
|
200
|
+
recommended_behavior:
|
|
201
|
+
"Do not install automatically. Use a reviewed alternative or ask the user for explicit restricted-install approval.",
|
|
202
|
+
allowed_next_calls: ["get_research_status", "recommend_alternatives"]
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
privacy_note: "Do not include private file contents, secrets, tokens or personal data in unknown component submissions."
|
|
206
|
+
};
|
|
207
|
+
const path = join(UNKNOWN_QUEUE_DIR, `${submittedAt.replaceAll(":", "-")}__${id}.json`);
|
|
208
|
+
await writeFile(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
209
|
+
return {
|
|
210
|
+
queued: true,
|
|
211
|
+
source: "local_fallback",
|
|
212
|
+
id,
|
|
213
|
+
path,
|
|
214
|
+
intelligence_state: record.intelligence_state,
|
|
215
|
+
decision_at_submission: localReview.decision,
|
|
216
|
+
research_task: record.research_task,
|
|
217
|
+
cloud_submission: {
|
|
218
|
+
attempted: cloudIntelligenceEnabled(),
|
|
219
|
+
reason: cloudResult.reason
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function recommendAlternatives(input = {}) {
|
|
225
|
+
const review = await reviewBeforeInstall(input);
|
|
226
|
+
return {
|
|
227
|
+
component: review.component,
|
|
228
|
+
alternatives: review.alternatives,
|
|
229
|
+
recommended_alternatives: review.recommended_alternatives,
|
|
230
|
+
safe_install_plan: review.safe_install_plan,
|
|
231
|
+
one_step_action: review.one_step_action,
|
|
232
|
+
agent_actions: review.agent_actions?.filter((action) => action.id === "prefer-reviewed-alternative" || action.id === "apply-safe-install-plan"),
|
|
233
|
+
agent_instruction: review.agent_instruction
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function writeLocalQueue(dir, prefix, payload) {
|
|
238
|
+
await mkdir(dir, { recursive: true });
|
|
239
|
+
const submittedAt = new Date().toISOString();
|
|
240
|
+
const id = createHash("sha256")
|
|
241
|
+
.update(JSON.stringify({ payload, submittedAt }))
|
|
242
|
+
.digest("hex")
|
|
243
|
+
.slice(0, 16);
|
|
244
|
+
const record = {
|
|
245
|
+
schema_version: "0.1.0",
|
|
246
|
+
id,
|
|
247
|
+
status: "queued",
|
|
248
|
+
submitted_at: submittedAt,
|
|
249
|
+
source: "agent-security-mcp",
|
|
250
|
+
...payload
|
|
251
|
+
};
|
|
252
|
+
const path = join(dir, `${submittedAt.replaceAll(":", "-")}__${prefix}__${id}.json`);
|
|
253
|
+
await writeFile(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
254
|
+
return { queued: true, source: "local_fallback", id, path };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function recordUsageEvent(event = {}) {
|
|
258
|
+
const cloudResult = await submitUsageEventToCloud(event);
|
|
259
|
+
if (cloudResult.ok) {
|
|
260
|
+
return {
|
|
261
|
+
recorded: true,
|
|
262
|
+
source: "asl_cloud",
|
|
263
|
+
api_url: cloudResult.api_url,
|
|
264
|
+
id: cloudResult.data?.id || null,
|
|
265
|
+
status: cloudResult.data?.status || "recorded"
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return writeLocalQueue(USAGE_QUEUE_DIR, "usage", {
|
|
269
|
+
event,
|
|
270
|
+
cloud_submission: {
|
|
271
|
+
attempted: cloudIntelligenceEnabled(),
|
|
272
|
+
reason: cloudResult.reason
|
|
273
|
+
},
|
|
274
|
+
privacy_note: "Usage events must not include private code, secrets, tokens, cookies or personal data."
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function submitDecisionFeedback(feedback = {}) {
|
|
279
|
+
const cloudResult = await submitFeedbackToCloud(feedback);
|
|
280
|
+
if (cloudResult.ok) {
|
|
281
|
+
return {
|
|
282
|
+
recorded: true,
|
|
283
|
+
source: "asl_cloud",
|
|
284
|
+
api_url: cloudResult.api_url,
|
|
285
|
+
id: cloudResult.data?.id || null,
|
|
286
|
+
status: cloudResult.data?.status || "recorded"
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return writeLocalQueue(FEEDBACK_QUEUE_DIR, "feedback", {
|
|
290
|
+
feedback,
|
|
291
|
+
cloud_submission: {
|
|
292
|
+
attempted: cloudIntelligenceEnabled(),
|
|
293
|
+
reason: cloudResult.reason
|
|
294
|
+
},
|
|
295
|
+
privacy_note: "Decision feedback must not include private code, secrets, tokens, cookies or personal data."
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function getResearchStatus(submissionId) {
|
|
300
|
+
const cloudResult = await queryResearchStatus(submissionId);
|
|
301
|
+
if (cloudResult.ok) return cloudResult.data;
|
|
302
|
+
try {
|
|
303
|
+
const snapshot = JSON.parse(await readFile(OPS_SNAPSHOT_PATH, "utf8"));
|
|
304
|
+
const item = (snapshot.pipeline || []).find((entry) => entry.id === submissionId);
|
|
305
|
+
if (item) {
|
|
306
|
+
return {
|
|
307
|
+
schema_version: "0.1.0",
|
|
308
|
+
service: "AgentSecurityLens",
|
|
309
|
+
result_type: "research_status",
|
|
310
|
+
source: cloudIntelligenceEnabled() ? "local_fallback" : "local_only",
|
|
311
|
+
submission_id: submissionId,
|
|
312
|
+
status: item.status,
|
|
313
|
+
owner_team: item.owner_team,
|
|
314
|
+
submitted_at: item.submitted_at,
|
|
315
|
+
sla_due_at: item.sla_due_at,
|
|
316
|
+
risk_hint: item.risk_hint,
|
|
317
|
+
evidence_count: item.evidence_count,
|
|
318
|
+
install_retry_allowed: item.status === "published",
|
|
319
|
+
next_agent_action: {
|
|
320
|
+
action_type: item.status === "published" ? "rerun_review_before_install" : "wait_for_research_or_ask_user",
|
|
321
|
+
automatic_install_retry_allowed: item.status === "published",
|
|
322
|
+
user_confirmation_required: item.status !== "published",
|
|
323
|
+
recommended_behavior:
|
|
324
|
+
item.status === "published"
|
|
325
|
+
? "Re-run ASL review_before_install and follow the returned decision before installing."
|
|
326
|
+
: "Do not install automatically. Use a reviewed alternative or ask the user for explicit restricted-install approval.",
|
|
327
|
+
allowed_next_calls:
|
|
328
|
+
item.status === "published" ? ["review_before_install", "check_component"] : ["get_research_status", "recommend_alternatives"]
|
|
329
|
+
},
|
|
330
|
+
agent_instruction:
|
|
331
|
+
item.status === "published"
|
|
332
|
+
? "Re-run ASL review_before_install and follow the returned decision before installing."
|
|
333
|
+
: "Do not install automatically. Use a reviewed alternative or ask the user for explicit restricted-install approval."
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
// Fall through to generic status when no local operations snapshot is available.
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
schema_version: "0.1.0",
|
|
341
|
+
service: "AgentSecurityLens",
|
|
342
|
+
result_type: "research_status",
|
|
343
|
+
source: cloudIntelligenceEnabled() ? "local_fallback" : "local_only",
|
|
344
|
+
submission_id: submissionId || null,
|
|
345
|
+
status: "unknown",
|
|
346
|
+
reason: cloudResult.reason || "cloud_unavailable",
|
|
347
|
+
install_retry_allowed: false,
|
|
348
|
+
next_agent_action: {
|
|
349
|
+
action_type: "wait_for_research_or_ask_user",
|
|
350
|
+
automatic_install_retry_allowed: false,
|
|
351
|
+
user_confirmation_required: true,
|
|
352
|
+
recommended_behavior: "If this component is still unknown, do not install automatically. Ask the user or use a reviewed alternative.",
|
|
353
|
+
allowed_next_calls: ["get_research_status", "recommend_alternatives"]
|
|
354
|
+
},
|
|
355
|
+
agent_instruction:
|
|
356
|
+
"If this component is still unknown, do not install automatically. Ask the user or use a reviewed alternative."
|
|
357
|
+
};
|
|
358
|
+
}
|