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.
Files changed (81) hide show
  1. package/.env.example +10 -0
  2. package/.mcp/server.json +42 -0
  3. package/CHANGELOG.md +17 -0
  4. package/LICENSE +17 -0
  5. package/PRIVACY.md +37 -0
  6. package/README.md +150 -0
  7. package/RELEASE-MANIFEST.json +449 -0
  8. package/SECURITY.md +24 -0
  9. package/apps/mcp-server/agent-security-lens-mcp.mjs +441 -0
  10. package/bin/agent-security-lens.mjs +117 -0
  11. package/data/ecosystems/agent-candidates.json +230 -0
  12. package/data/intelligence/components.json +22989 -0
  13. package/data/intelligence/security-evaluation-standard.json +221 -0
  14. package/data/recommendations/core/recommendations.json +256 -0
  15. package/data/trust/signal-taxonomy.json +107 -0
  16. package/docs/asl-agent-component-safety-standard-v0.2.md +56 -0
  17. package/examples/dot-hermes/.hermes/config.json +17 -0
  18. package/examples/dot-openclaw/.openclaw/openclaw.json +17 -0
  19. package/examples/hermes-like/.env.example +2 -0
  20. package/examples/hermes-like/config.json +37 -0
  21. package/examples/hermes-like/optional-mcps/github-tools.json +8 -0
  22. package/examples/hermes-like/skills/openclaw-imports/browser-skill/SKILL.md +8 -0
  23. package/examples/openclaw-like/.env.example +2 -0
  24. package/examples/openclaw-like/AGENTS.md +7 -0
  25. package/examples/openclaw-like/openclaw.json +28 -0
  26. package/examples/openclaw-like/workspace/skills/browser-control/SKILL.md +8 -0
  27. package/llms.txt +25 -0
  28. package/package.json +50 -0
  29. package/profiles/generic-agent/profile.json +19 -0
  30. package/profiles/hermes-like/profile.json +23 -0
  31. package/profiles/mcp-server/profile.json +18 -0
  32. package/profiles/openclaw-like/profile.json +22 -0
  33. package/profiles/skill-runtime/profile.json +19 -0
  34. package/rule-packs/core/rules.json +82 -0
  35. package/rule-packs/hermes/rules.json +44 -0
  36. package/rule-packs/mcp/rules.json +65 -0
  37. package/rule-packs/openclaw/rules.json +46 -0
  38. package/rule-packs/skills/rules.json +45 -0
  39. package/schemas/agent-install-decision.schema.json +432 -0
  40. package/schemas/agent-usage-event.schema.json +45 -0
  41. package/schemas/assessment-result.schema.json +361 -0
  42. package/schemas/comparison-result.schema.json +113 -0
  43. package/schemas/component-alternative-graph.schema.json +187 -0
  44. package/schemas/component-intelligence.schema.json +93 -0
  45. package/schemas/decision-feedback.schema.json +49 -0
  46. package/schemas/ecosystem-candidate-registry.schema.json +98 -0
  47. package/schemas/profile.schema.json +65 -0
  48. package/schemas/recommendation-pack.schema.json +114 -0
  49. package/schemas/rule-pack.schema.json +113 -0
  50. package/schemas/trust-signal-taxonomy.schema.json +68 -0
  51. package/scripts/verify-examples.mjs +121 -0
  52. package/scripts/verify-mcp-server.mjs +278 -0
  53. package/scripts/verify-registry.mjs +264 -0
  54. package/server.json +42 -0
  55. package/src/assessment/assess.mjs +108 -0
  56. package/src/assessment/discover-targets.mjs +127 -0
  57. package/src/assessment/risk-domains.mjs +83 -0
  58. package/src/assessment/summarize.mjs +57 -0
  59. package/src/core/files.mjs +74 -0
  60. package/src/intelligence/cloud-client.mjs +260 -0
  61. package/src/intelligence/component-intelligence.mjs +358 -0
  62. package/src/intelligence/decision-engine.mjs +772 -0
  63. package/src/intelligence/finding-context.mjs +180 -0
  64. package/src/intelligence/safety-score-v0.2.mjs +294 -0
  65. package/src/observations/json-observations.mjs +211 -0
  66. package/src/observations/observation-rules.mjs +157 -0
  67. package/src/profiles/load-profiles.mjs +130 -0
  68. package/src/recommendations/component-alternative-graph.mjs +94 -0
  69. package/src/recommendations/load-recommendations.mjs +17 -0
  70. package/src/recommendations/match-recommendations.mjs +79 -0
  71. package/src/report/comparison-console.mjs +71 -0
  72. package/src/report/console.mjs +103 -0
  73. package/src/report/markdown.mjs +145 -0
  74. package/src/results/compare-results.mjs +106 -0
  75. package/src/results/save-result.mjs +29 -0
  76. package/src/rules/load-rules.mjs +22 -0
  77. package/src/rules/match-rules.mjs +99 -0
  78. package/src/rules/supersedes.mjs +39 -0
  79. package/src/store/assessment-store.mjs +78 -0
  80. package/src/trust/derive-trust-signals.mjs +73 -0
  81. package/src/trust/load-trust-signals.mjs +17 -0
@@ -0,0 +1,772 @@
1
+ import { evaluateComponentSafety } from "./safety-score-v0.2.mjs";
2
+ import {
3
+ alternativeCoverageFor,
4
+ findComponentAlternatives
5
+ } from "../recommendations/component-alternative-graph.mjs";
6
+
7
+ export function normalizeText(value) {
8
+ if (value == null) return "";
9
+ if (typeof value === "string") return value.toLowerCase();
10
+ return JSON.stringify(value).toLowerCase();
11
+ }
12
+
13
+ export function unique(items) {
14
+ return [...new Set(items.filter(Boolean))];
15
+ }
16
+
17
+ function aliasMatchesInput(alias, input = {}) {
18
+ const normalizedAlias = normalizeText(alias).trim();
19
+ if (!normalizedAlias) return false;
20
+ const exactFields = [
21
+ input.component_name,
22
+ input.name,
23
+ input.package_name,
24
+ input.registry
25
+ ].map((item) => normalizeText(item).trim());
26
+ if (exactFields.includes(normalizedAlias)) return true;
27
+ if (normalizedAlias.length < 5) return false;
28
+
29
+ const looseMatchAllowed = /[\/@._-]/.test(normalizedAlias) || normalizedAlias.length >= 8;
30
+ if (!looseMatchAllowed) return false;
31
+
32
+ const searchableText = normalizeText([
33
+ input.component_name,
34
+ input.component_type,
35
+ input.source_url,
36
+ input.install_command,
37
+ input.manifest,
38
+ input.name,
39
+ input.type,
40
+ input.package_name,
41
+ input.registry
42
+ ]);
43
+ return searchableText.includes(normalizedAlias);
44
+ }
45
+
46
+ const EVALUATION_MODEL_VERSION = "asl-safety-standard@0.2.0";
47
+
48
+ const RISK_SIGNAL_WEIGHTS = {
49
+ "remote-code-install": 30,
50
+ "shell-execution": 25,
51
+ "subprocess-spawn": 25,
52
+ "docker-runtime": 25,
53
+ "credential-access": 22,
54
+ "filesystem-read": 18,
55
+ "filesystem-write": 22,
56
+ "browser-access": 22,
57
+ "database-access": 22,
58
+ "data-retention": 18,
59
+ "network-access": 16,
60
+ "external-api": 16,
61
+ webhook: 16,
62
+ "remote-mcp-endpoint": 16,
63
+ "multi-agent-delegation": 14,
64
+ "tool-chaining": 14,
65
+ "third-party-integration": 14,
66
+ "workflow-automation": 18,
67
+ "background-execution": 18,
68
+ "scheduled-trigger": 18,
69
+ "hidden-instruction": 20,
70
+ "override-rules": 20,
71
+ "ignore-safety": 20,
72
+ "prompt-injection-pattern": 20,
73
+ "repository-write": 18,
74
+ "message-write": 14,
75
+ "unknown-source": 16
76
+ };
77
+
78
+ const TRUST_SIGNAL_WEIGHTS = {
79
+ "source-official-or-known-org": 12,
80
+ "active-maintenance": 8,
81
+ "transparent-permissions": 10,
82
+ "signed-release-or-pinned-version": 8,
83
+ "high-community-adoption": 6,
84
+ "negative-community-reports": -18,
85
+ "unresolved-security-issues": -16,
86
+ "unknown-maintainer": -10
87
+ };
88
+
89
+ const DECISION_THRESHOLDS = {
90
+ allow: { min_score: 80 },
91
+ allow_with_restrictions: { min_score: 60, max_score: 79 },
92
+ ask_user: { min_score: 40, max_score: 59 },
93
+ avoid: { max_score: 39 }
94
+ };
95
+
96
+ const MONITORED_POPULARITY_SCORE = 100;
97
+
98
+ function catalogCoverageState(candidate = {}) {
99
+ const source = normalizeText(candidate.source_url || candidate.full_name || "");
100
+ const popularity = Number(candidate.popularity_score || 0);
101
+ if (candidate.review_state === "monitored" || candidate.catalog_state === "monitored") return "monitored";
102
+ if (popularity >= MONITORED_POPULARITY_SCORE) return "monitored";
103
+ if (source.includes("github.com/modelcontextprotocol") || source.includes("github.com/microsoft")) return "monitored";
104
+ return "candidate";
105
+ }
106
+
107
+ export function findKnownComponent(input, components = []) {
108
+ return (
109
+ components.find((component) => {
110
+ const aliases = [component.id, component.name, ...(component.aliases || []), ...(component.source_patterns || [])];
111
+ return aliases.some((alias) => aliasMatchesInput(alias, input));
112
+ }) || null
113
+ );
114
+ }
115
+
116
+ export function findCandidateComponent(input, candidates = []) {
117
+ return (
118
+ candidates.find((candidate) => {
119
+ const aliases = unique([
120
+ candidate.id,
121
+ candidate.name,
122
+ candidate.full_name,
123
+ candidate.source_url,
124
+ ...(candidate.aliases || []),
125
+ ...(candidate.source_patterns || [])
126
+ ]);
127
+ return aliases.some((alias) => aliasMatchesInput(alias, input));
128
+ }) || null
129
+ );
130
+ }
131
+
132
+ function candidateAsIntelligenceRecord(candidate = null) {
133
+ if (!candidate) return null;
134
+ const coverageState = catalogCoverageState(candidate);
135
+ return {
136
+ id: candidate.id,
137
+ name: candidate.name,
138
+ type: candidate.type || candidate.category || "unknown",
139
+ aliases: unique([candidate.name, candidate.full_name]),
140
+ source_patterns: unique([candidate.full_name, candidate.source_url]),
141
+ source_url: candidate.source_url || null,
142
+ source_type: candidate.source_type || "candidate_catalog",
143
+ stars: candidate.stars || 0,
144
+ trust_score: Number.isInteger(candidate.trust_score) ? candidate.trust_score : undefined,
145
+ risk_level: candidate.risk_level || undefined,
146
+ risk_signals: candidate.risk_signals || candidate.risk_hints || [],
147
+ safe_install_plan: candidate.safe_install_plan || [],
148
+ alternatives: candidate.alternatives || [],
149
+ decision: candidate.decision || "ask_user",
150
+ assessment_type: candidate.assessment_type || "automatic",
151
+ confidence: candidate.confidence ?? null,
152
+ model_version: candidate.model_version || null,
153
+ community_signals: {
154
+ positive_count: Number(candidate.stars || 0) >= 1000 ? 1 : 0,
155
+ unresolved_security_issues: 0,
156
+ negative_count: 0
157
+ },
158
+ catalog: {
159
+ coverage_state: coverageState,
160
+ state: candidate.catalog_state || "cataloged",
161
+ review_state: candidate.review_state || "unreviewed",
162
+ popularity_score: candidate.popularity_score || 0,
163
+ forks: candidate.forks || 0,
164
+ open_issues: candidate.open_issues || 0,
165
+ language: candidate.language || null,
166
+ topics: candidate.topics || [],
167
+ evidence: candidate.evidence || null,
168
+ next_action: candidate.next_action || "collect_metadata",
169
+ description: candidate.description || ""
170
+ }
171
+ };
172
+ }
173
+
174
+ function clampScore(score) {
175
+ return Math.max(0, Math.min(100, Math.round(score)));
176
+ }
177
+
178
+ function sourceReliability(input = {}, known = null) {
179
+ const sourceType = normalizeText(input.source_type || input.registry || known?.source_type);
180
+ const sourceUrl = normalizeText(input.source_url || known?.source_url || known?.source_patterns || "");
181
+ if (sourceType.includes("official_registry")) return 0.95;
182
+ if (sourceType.includes("official_github_org")) return 0.9;
183
+ if (sourceType.includes("package_registry") || sourceType.includes("npm") || sourceType.includes("pypi")) return 0.75;
184
+ if (sourceUrl.includes("github.com/modelcontextprotocol") || sourceUrl.includes("github.com/microsoft")) return 0.9;
185
+ if (sourceUrl.includes("github.com") || sourceType.includes("github")) return 0.75;
186
+ if (sourceType.includes("community")) return 0.55;
187
+ if (sourceType.includes("social")) return 0.45;
188
+ return known ? 0.75 : 0.35;
189
+ }
190
+
191
+ function inferTrustSignals(input = {}, known = null) {
192
+ const signals = [...(known?.trust_signals || []), ...(input.trust_signals || [])];
193
+ const sourceUrl = normalizeText(input.source_url || known?.source_url || known?.source_patterns || "");
194
+ const text = normalizeText(input);
195
+ const stars = Number(input.stars || input.github_stars || known?.stars || 0);
196
+ const negativeCount = Number(input.community_signals?.negative_count || known?.community_signals?.negative_count || 0);
197
+ const positiveCount = Number(input.community_signals?.positive_count || known?.community_signals?.positive_count || 0);
198
+ const unresolvedSecurityIssues = Number(
199
+ input.community_signals?.unresolved_security_issues || known?.community_signals?.unresolved_security_issues || 0
200
+ );
201
+
202
+ if (sourceUrl.includes("github.com/modelcontextprotocol") || sourceUrl.includes("github.com/microsoft")) {
203
+ signals.push("source-official-or-known-org");
204
+ }
205
+ if (stars >= 1000 || positiveCount >= 5) signals.push("high-community-adoption");
206
+ if (/\b(read-only|scoped token|sandbox|signed release|permission documented)\b/.test(text)) {
207
+ signals.push("transparent-permissions");
208
+ }
209
+ if (/\b(pin|pinned|version|sha|digest|signed)\b/.test(text)) {
210
+ signals.push("signed-release-or-pinned-version");
211
+ }
212
+ if (negativeCount > 0) signals.push("negative-community-reports");
213
+ if (unresolvedSecurityIssues > 0) signals.push("unresolved-security-issues");
214
+ if (!sourceUrl && !known) signals.push("unknown-maintainer");
215
+
216
+ return unique(signals);
217
+ }
218
+
219
+ export function inferRisks(input) {
220
+ const text = normalizeText(input);
221
+ const risks = [];
222
+ if (/\b(npx|uvx|pipx|python|node|bash|sh|powershell|pwsh|cmd\.exe|subprocess)\b/.test(text)) {
223
+ risks.push("shell-execution");
224
+ }
225
+ if (/\b(curl|wget)\b/.test(text) || /\|\s*(bash|sh|powershell|pwsh)\b/.test(text)) {
226
+ risks.push("remote-code-install");
227
+ }
228
+ if (/\b(docker|podman)\b/.test(text)) {
229
+ risks.push("docker-runtime");
230
+ }
231
+ if (/\b(file|filesystem|write|workspace|home|\/users|c:\\\\|\.ssh)\b/.test(text)) {
232
+ risks.push("filesystem-write");
233
+ }
234
+ if (/\b(token|secret|api[_-]?key|credential|env|\.env|ssh|cookie)\b/.test(text)) {
235
+ risks.push("credential-access");
236
+ }
237
+ if (/\b(http|https|sse|webhook|remote|api|network)\b/.test(text)) {
238
+ risks.push("network-access");
239
+ }
240
+ if (/\b(browser|chrome|playwright|cookies?|session)\b/.test(text)) {
241
+ risks.push("browser-access");
242
+ }
243
+ return unique(risks);
244
+ }
245
+
246
+ export function scoreAssessment({ risks, known = null, input = {} }) {
247
+ const safetyAssessment = evaluateComponentSafety({ risks, known, input });
248
+ const trustSignals = inferTrustSignals(input, known);
249
+ const appliedRiskWeights = risks.map((signal) => ({
250
+ signal,
251
+ weight: -(RISK_SIGNAL_WEIGHTS[signal] || 8)
252
+ }));
253
+ const appliedTrustSignals = trustSignals.map((signal) => ({
254
+ signal,
255
+ weight: TRUST_SIGNAL_WEIGHTS[signal] || 0
256
+ }));
257
+ const riskPenalty = appliedRiskWeights.reduce((total, item) => total + Math.abs(item.weight), 0);
258
+ const trustAdjustment = appliedTrustSignals.reduce((total, item) => total + item.weight, 0);
259
+ const reliability = sourceReliability(input, known);
260
+ const reliabilityAdjustment = Math.round((reliability - 0.5) * 12);
261
+ const baseScore = safetyAssessment.context_safety_score;
262
+ return {
263
+ score: safetyAssessment.context_safety_score,
264
+ decision: safetyAssessment.decision,
265
+ safety_assessment: safetyAssessment,
266
+ breakdown: {
267
+ model_version: EVALUATION_MODEL_VERSION,
268
+ mode:
269
+ known?.trust_score != null
270
+ ? known?.intelligence_state === "strict_reviewed"
271
+ ? "strict_reviewed_intelligence_record"
272
+ : "curated_baseline_record"
273
+ : known?.catalog?.coverage_state === "monitored"
274
+ ? "monitored_catalog_record"
275
+ : known?.catalog
276
+ ? "candidate_catalog_record"
277
+ : "inferred_from_submitted_metadata",
278
+ base_score: baseScore,
279
+ risk_penalty: -riskPenalty,
280
+ trust_adjustment: trustAdjustment,
281
+ source_reliability: reliability,
282
+ source_reliability_adjustment: reliabilityAdjustment,
283
+ applied_risk_weights: appliedRiskWeights,
284
+ applied_trust_signals: appliedTrustSignals,
285
+ sentiment: {
286
+ auxiliary_only: true,
287
+ negative_count: Number(input.community_signals?.negative_count || known?.community_signals?.negative_count || 0),
288
+ positive_count: Number(input.community_signals?.positive_count || known?.community_signals?.positive_count || 0),
289
+ rule: "Community sentiment can adjust confidence, but it does not alone prove malicious behavior."
290
+ },
291
+ dimensions: safetyAssessment.dimensions,
292
+ residual_risk: safetyAssessment.residual_risk,
293
+ hard_failures: safetyAssessment.hard_failures,
294
+ required_controls: safetyAssessment.required_controls,
295
+ missing_controls: safetyAssessment.missing_controls,
296
+ decision_thresholds: DECISION_THRESHOLDS
297
+ }
298
+ };
299
+ }
300
+
301
+ export function scoreFromRisks(risks, knownScore = null) {
302
+ return scoreAssessment({ risks, known: knownScore == null ? null : { trust_score: knownScore } }).score;
303
+ }
304
+
305
+ export function riskLevel(score) {
306
+ if (score < 40) return "high";
307
+ if (score < 80) return "medium";
308
+ return "low";
309
+ }
310
+
311
+ export function decisionFor({ score, risks, knownDecision }) {
312
+ if (knownDecision) return knownDecision;
313
+ if (risks.includes("catalog-unreviewed")) return "ask_user";
314
+ if (risks.includes("remote-code-install")) return "ask_user";
315
+ if (risks.includes("credential-access") && risks.includes("shell-execution")) return "ask_user";
316
+ if (score < 40) return "avoid";
317
+ if (score < 60) return "ask_user";
318
+ if (score < 80) return "allow_with_restrictions";
319
+ return "allow";
320
+ }
321
+
322
+ function reviewedAlternativeNames(known = null) {
323
+ const recommendation = (known?.evidence || [])
324
+ .find((item) => item.kind === "recommendation_review")
325
+ ?.recommendations;
326
+ return unique(
327
+ (recommendation?.reviewed_alternatives || [])
328
+ .filter((item) => item?.kind === "reviewed_component" && item?.name)
329
+ .map((item) => item.name)
330
+ );
331
+ }
332
+
333
+ export function nextActionFor(decision) {
334
+ if (decision === "allow") return "install";
335
+ if (decision === "allow_with_restrictions") return "apply_restrictions_then_install";
336
+ if (decision === "ask_user") return "ask_user_before_install";
337
+ return "do_not_install";
338
+ }
339
+
340
+ function blocksAutomaticInstall(decision) {
341
+ return ["ask_user", "avoid"].includes(decision);
342
+ }
343
+
344
+ function blockingReasons({ decision, risks, known }) {
345
+ const reasons = [];
346
+ if (!known) reasons.push("component-not-in-asl-intelligence");
347
+ if (known?.catalog?.coverage_state === "monitored") reasons.push("component-monitored-but-not-reviewed");
348
+ else if (known?.catalog) reasons.push("component-cataloged-but-not-reviewed");
349
+ if (decision === "avoid") reasons.push("decision-avoid");
350
+ if (decision === "ask_user") reasons.push("user-confirmation-required");
351
+ if (risks.includes("remote-code-install")) reasons.push("remote-code-install-pattern");
352
+ if (risks.includes("credential-access")) reasons.push("credential-access-risk");
353
+ if (risks.includes("shell-execution")) reasons.push("shell-execution-risk");
354
+ return unique(reasons);
355
+ }
356
+
357
+ function actionStatusFor(decision) {
358
+ if (decision === "allow") return "allowed";
359
+ if (decision === "allow_with_restrictions") return "conditional";
360
+ if (decision === "ask_user") return "requires_user";
361
+ return "blocked";
362
+ }
363
+
364
+ function buildAgentActions({ decision, risks, safeInstallPlan, alternatives, known, input }) {
365
+ const status = actionStatusFor(decision);
366
+ const actions = [
367
+ {
368
+ id: "record-component-source",
369
+ stage: "before_install",
370
+ action_type: "record",
371
+ required: true,
372
+ status: "required",
373
+ instruction: "Record component name, source URL, package/version and install command before any installation attempt.",
374
+ tool: null
375
+ }
376
+ ];
377
+
378
+ if (!known) {
379
+ actions.push({
380
+ id: "submit-unknown-component",
381
+ stage: "before_install",
382
+ action_type: "call_asl_tool",
383
+ required: true,
384
+ status: "required",
385
+ tool: "submit_unknown_component",
386
+ arguments: {
387
+ component_name: input.component_name || input.name || null,
388
+ component_type: input.component_type || input.type || "unknown",
389
+ source_url: input.source_url || null,
390
+ install_command: input.install_command || null
391
+ },
392
+ instruction: "Submit public component metadata to ASL. Do not send private source code, secrets, cookies, tokens or personal data."
393
+ });
394
+ }
395
+
396
+ if (known?.catalog) {
397
+ actions.push({
398
+ id: "check-catalog-research-status",
399
+ stage: "before_install",
400
+ action_type: "call_asl_tool",
401
+ required: true,
402
+ status: "required",
403
+ tool: "get_research_status",
404
+ arguments: {
405
+ submission_id: known.id
406
+ },
407
+ instruction: "This component is cataloged but not reviewed. Check ASL research status before retrying automatic installation."
408
+ });
409
+ }
410
+
411
+ if (decision === "allow_with_restrictions") {
412
+ actions.push({
413
+ id: "apply-safe-install-plan",
414
+ stage: "before_install",
415
+ action_type: "apply_restrictions",
416
+ required: true,
417
+ status,
418
+ instruction: "Apply every safe_install_plan step before installation.",
419
+ checklist: safeInstallPlan
420
+ });
421
+ }
422
+
423
+ if (blocksAutomaticInstall(decision)) {
424
+ actions.push({
425
+ id: "request-user-confirmation",
426
+ stage: "before_install",
427
+ action_type: "ask_user",
428
+ required: true,
429
+ status,
430
+ instruction:
431
+ decision === "avoid"
432
+ ? "Do not install automatically. Ask the user only if they explicitly want to override ASL guidance."
433
+ : "Ask the user to approve or reject installation before continuing."
434
+ });
435
+ }
436
+
437
+ if (alternatives.length) {
438
+ actions.push({
439
+ id: "prefer-reviewed-alternative",
440
+ stage: "before_install",
441
+ action_type: "recommend_alternative",
442
+ required: decision === "avoid",
443
+ status: decision === "avoid" ? "required" : "recommended",
444
+ instruction: "Prefer a reviewed alternative when it satisfies the same task.",
445
+ alternatives
446
+ });
447
+ }
448
+
449
+ actions.push({
450
+ id: "report-install-outcome",
451
+ stage: "after_decision",
452
+ action_type: "call_asl_tool",
453
+ required: true,
454
+ status: "required",
455
+ tool: "report_install_outcome",
456
+ arguments_template: {
457
+ component_name: input.component_name || input.name || null,
458
+ component_type: input.component_type || input.type || "unknown",
459
+ source_url: input.source_url || null,
460
+ decision,
461
+ outcome: "installed|blocked|user_approved|user_rejected|restriction_applied|failed|skipped",
462
+ restriction_applied: risks.some((risk) => ["filesystem-write", "credential-access", "browser-access", "network-access"].includes(risk))
463
+ },
464
+ instruction: "Report the final install outcome to ASL after acting on this decision."
465
+ });
466
+
467
+ return actions;
468
+ }
469
+
470
+ function buildRecommendedAlternatives(alternatives = [], { type = "unknown" } = {}) {
471
+ return alternatives.map((alternative, index) => {
472
+ if (typeof alternative === "string") {
473
+ return {
474
+ id: `legacy-alternative-${index + 1}`,
475
+ name: alternative,
476
+ reason: "Legacy reviewed recommendation. Re-check the target with ASL before installation.",
477
+ component_type: type,
478
+ action: "review_before_install_before_use"
479
+ };
480
+ }
481
+ return {
482
+ ...alternative,
483
+ id: alternative.id || `alternative-${index + 1}`,
484
+ component_type: alternative.component_type || type,
485
+ action:
486
+ alternative.relationship_type === "conditional_alternative"
487
+ ? "confirm_conditions_then_review_target"
488
+ : "review_before_install_before_use"
489
+ };
490
+ });
491
+ }
492
+
493
+ function buildOneStepAction({ decision, safeInstallPlan, alternatives, known }) {
494
+ if (decision === "avoid") {
495
+ const replacement = alternatives[0] ? ` Prefer reviewed alternative: ${alternatives[0]}.` : "";
496
+ return {
497
+ action_type: "do_not_install",
498
+ command: null,
499
+ agent_instruction: `Do not install this component automatically. Submit it to ASL research and ask the user before any override.${replacement}`,
500
+ requires_user_confirmation: true,
501
+ blocks_install: true
502
+ };
503
+ }
504
+ if (decision === "ask_user") {
505
+ const replacement = alternatives[0] ? ` Prefer reviewed alternative first: ${alternatives[0]}.` : "";
506
+ return {
507
+ action_type: "ask_user_before_install",
508
+ command: null,
509
+ agent_instruction: `Ask the user to approve this install. Show ASL risk signals and safe_install_plan before running any command.${replacement}`,
510
+ requires_user_confirmation: true,
511
+ blocks_install: true
512
+ };
513
+ }
514
+ if (decision === "allow_with_restrictions") {
515
+ return {
516
+ action_type: "apply_restrictions_then_install",
517
+ command: null,
518
+ agent_instruction: `Apply these restrictions first: ${safeInstallPlan.join(" ")} Then install only from the recorded source and version.`,
519
+ requires_user_confirmation: false,
520
+ blocks_install: false
521
+ };
522
+ }
523
+ return {
524
+ action_type: "install",
525
+ command: null,
526
+ agent_instruction: known
527
+ ? "Install from the reviewed source/version and report the outcome to ASL."
528
+ : "Install only after recording exact source/version and submitting unknown metadata to ASL.",
529
+ requires_user_confirmation: false,
530
+ blocks_install: false
531
+ };
532
+ }
533
+
534
+ function buildAgentDecisionContract({ decision, risks, known, input, safeInstallPlan, alternatives }) {
535
+ const cataloged = Boolean(known?.catalog);
536
+ const reasons = blockingReasons({ decision, risks, known });
537
+ return {
538
+ contract_version: "asl-agent-decision-contract@0.2.0",
539
+ install_allowed: decision !== "avoid",
540
+ automatic_install_allowed: !blocksAutomaticInstall(decision),
541
+ user_confirmation_required: blocksAutomaticInstall(decision),
542
+ restrictions_required: decision === "allow_with_restrictions",
543
+ submit_unknown_required: !known,
544
+ research_status_required_before_retry: !known || cataloged,
545
+ outcome_report_required: true,
546
+ feedback_requested: true,
547
+ blocks_install: blocksAutomaticInstall(decision),
548
+ blocking_reasons: reasons,
549
+ required_tools: unique([
550
+ !known ? "submit_unknown_component" : null,
551
+ !known || cataloged ? "get_research_status" : null,
552
+ "report_install_outcome",
553
+ "submit_decision_feedback"
554
+ ]),
555
+ allowed_outcomes: ["installed", "blocked", "user_approved", "user_rejected", "restriction_applied", "failed", "skipped"],
556
+ retry_policy: {
557
+ unknown_component:
558
+ !known || cataloged
559
+ ? "Do not retry automatic installation until get_research_status returns published or the user explicitly approves a restricted install."
560
+ : "Re-run review_before_install when source, package, version, manifest or install command changes.",
561
+ decision_cache_ttl_hours: known?.trust_score != null ? 24 : 1
562
+ },
563
+ component_fingerprint_fields: {
564
+ component_name: input.component_name || input.name || null,
565
+ component_type: input.component_type || input.type || "unknown",
566
+ source_url: input.source_url || null,
567
+ install_command: input.install_command || null
568
+ },
569
+ safe_install_plan_required: decision === "allow_with_restrictions" ? safeInstallPlan : [],
570
+ recommended_alternative_count: alternatives.length
571
+ };
572
+ }
573
+
574
+ export function defaultSafeInstallPlan(risks) {
575
+ const plan = [];
576
+ if (risks.includes("shell-execution") || risks.includes("remote-code-install")) {
577
+ plan.push("Do not run install commands automatically. Ask the user to approve the command first.");
578
+ plan.push("Pin package versions or commit SHAs before installation.");
579
+ }
580
+ if (risks.includes("filesystem-write")) {
581
+ plan.push("Limit filesystem access to the current workspace or a temporary copy.");
582
+ }
583
+ if (risks.includes("credential-access")) {
584
+ plan.push("Use scoped low-privilege credentials instead of personal tokens.");
585
+ }
586
+ if (risks.includes("browser-access")) {
587
+ plan.push("Use a dedicated browser profile without personal cookies.");
588
+ }
589
+ if (risks.includes("network-access")) {
590
+ plan.push("Allowlist remote endpoints before enabling the component.");
591
+ }
592
+ if (!plan.length) plan.push("Install only from a reviewed source and record the exact version.");
593
+ return plan;
594
+ }
595
+
596
+ export function buildAgentInstruction({ decision, risks, safeInstallPlan, alternatives, known }) {
597
+ if (decision === "avoid") {
598
+ return "Do not install this component automatically. Ask the user and submit it for security review.";
599
+ }
600
+ if (decision === "ask_user") {
601
+ const replacement = alternatives.length ? ` Prefer alternatives first: ${alternatives.join(", ")}.` : "";
602
+ return `Ask the user before installing. Key risks: ${risks.join(", ")}. Apply the safe install plan first.${replacement}`;
603
+ }
604
+ if (decision === "allow_with_restrictions") {
605
+ return `Install only with restrictions: ${safeInstallPlan.join(" ")}`;
606
+ }
607
+ if (known && alternatives.length) {
608
+ return `This component is known. Install with the recorded safe plan. Alternatives: ${alternatives.join(", ")}.`;
609
+ }
610
+ return "Component appears low risk from available signals. Record exact source and version before installing.";
611
+ }
612
+
613
+ export function buildInstallDecision({
614
+ input = {},
615
+ components = [],
616
+ candidates = [],
617
+ recommendationGraph = {},
618
+ resolution
619
+ }) {
620
+ const knownRecord = findKnownComponent(input, components);
621
+ const strictReviewed = knownRecord?.intelligence_state === "strict_reviewed";
622
+ const curatedBaseline = knownRecord?.intelligence_state === "curated_baseline";
623
+ const candidate = knownRecord ? null : candidateAsIntelligenceRecord(findCandidateComponent(input, candidates));
624
+ const known = knownRecord || candidate;
625
+ const intelligenceState = strictReviewed
626
+ ? "strict_reviewed"
627
+ : curatedBaseline
628
+ ? "curated_baseline"
629
+ : candidate?.catalog?.coverage_state || "unknown";
630
+ const inferredRisks = inferRisks(input);
631
+ const risks = unique([
632
+ ...(known?.risk_signals || []),
633
+ ...inferredRisks,
634
+ ...(candidate ? ["catalog-unreviewed"] : []),
635
+ ...(known ? [] : ["unknown-source"])
636
+ ]);
637
+ const {
638
+ score: trustScore,
639
+ decision: contextualDecision,
640
+ safety_assessment: safetyAssessment,
641
+ breakdown: scoreBreakdown
642
+ } = scoreAssessment({ risks, known, input });
643
+ const level = safetyAssessment.residual_risk >= 70 ? "high" : safetyAssessment.residual_risk >= 35 ? "medium" : "low";
644
+ const scoredDecision = contextualDecision;
645
+ // Missing intelligence is itself a blocking condition. A favorable metadata-only
646
+ // score may guide restrictions, but it must never authorize autonomous install.
647
+ const decision =
648
+ (!known || curatedBaseline) && scoredDecision !== "avoid"
649
+ ? "ask_user"
650
+ : scoredDecision;
651
+ const safeInstallPlan = known?.safe_install_plan?.length ? known.safe_install_plan : defaultSafeInstallPlan(risks);
652
+ const componentType = input.component_type || input.type || known?.type || "unknown";
653
+ const graphAlternatives = strictReviewed
654
+ ? findComponentAlternatives({
655
+ componentId: known.id,
656
+ graph: recommendationGraph,
657
+ components
658
+ })
659
+ : [];
660
+ const legacyAlternativeNames =
661
+ !["mcp", "skill"].includes(componentType) && !graphAlternatives.length
662
+ ? reviewedAlternativeNames(known)
663
+ : [];
664
+ const recommendedAlternativeRecords = graphAlternatives.length
665
+ ? graphAlternatives
666
+ : legacyAlternativeNames;
667
+ const alternatives = recommendedAlternativeRecords.map((alternative) =>
668
+ typeof alternative === "string" ? alternative : alternative.name
669
+ );
670
+ const alternativeCoverage = strictReviewed
671
+ ? alternativeCoverageFor({
672
+ componentId: known.id,
673
+ graph: recommendationGraph,
674
+ alternatives: graphAlternatives
675
+ })
676
+ : {
677
+ status: "not_applicable",
678
+ reviewed_alternative_count: 0,
679
+ reason: "Alternatives are available only for strictly reviewed intelligence records.",
680
+ graph_version: recommendationGraph.version || null
681
+ };
682
+ const agentDecisionContract = buildAgentDecisionContract({
683
+ decision,
684
+ risks,
685
+ known,
686
+ input,
687
+ safeInstallPlan,
688
+ alternatives
689
+ });
690
+ const agentActions = buildAgentActions({ decision, risks, safeInstallPlan, alternatives, known, input });
691
+
692
+ return {
693
+ schema_version: "0.1.0",
694
+ schema_id: "https://agentsecuritylens.dev/schemas/agent-install-decision.schema.json",
695
+ service: "AgentSecurityLens",
696
+ result_type: "agent_install_decision",
697
+ component: {
698
+ known: Boolean(known),
699
+ reviewed: Boolean(strictReviewed),
700
+ strict_reviewed: Boolean(strictReviewed),
701
+ curated_baseline: Boolean(curatedBaseline),
702
+ cataloged: Boolean(candidate),
703
+ intelligence_state: intelligenceState,
704
+ id: known?.id || null,
705
+ name: input.component_name || input.name || known?.name || null,
706
+ type: input.component_type || input.type || known?.type || "unknown",
707
+ source_url: input.source_url || known?.source_url || null,
708
+ full_name: known?.aliases?.[1] || null,
709
+ stars: known?.stars || null
710
+ },
711
+ decision,
712
+ trust_score: trustScore,
713
+ safety_assessment: safetyAssessment,
714
+ score_breakdown: scoreBreakdown,
715
+ risk_level: level,
716
+ risk_signals: risks,
717
+ required_user_confirmation: ["ask_user", "avoid"].includes(decision),
718
+ safe_install_plan: safeInstallPlan,
719
+ alternatives,
720
+ recommended_alternatives: buildRecommendedAlternatives(recommendedAlternativeRecords, {
721
+ type: componentType
722
+ }),
723
+ alternative_coverage: alternativeCoverage,
724
+ next_action: nextActionFor(decision),
725
+ one_step_action: buildOneStepAction({ decision, safeInstallPlan, alternatives, known }),
726
+ agent_decision_contract: agentDecisionContract,
727
+ agent_actions: agentActions,
728
+ agent_instruction: buildAgentInstruction({ decision, risks, safeInstallPlan, alternatives, known }),
729
+ intelligence_coverage: {
730
+ state: intelligenceState,
731
+ reviewed: Boolean(strictReviewed),
732
+ strict_reviewed: Boolean(strictReviewed),
733
+ curated_baseline: Boolean(curatedBaseline),
734
+ cataloged: Boolean(candidate),
735
+ monitored: intelligenceState === "monitored",
736
+ source: strictReviewed
737
+ ? "strict_reviewed_intelligence_database"
738
+ : curatedBaseline
739
+ ? "curated_baseline_database"
740
+ : intelligenceState === "monitored"
741
+ ? "monitored_catalog"
742
+ : candidate
743
+ ? "candidate_catalog"
744
+ : "submitted_metadata_inference",
745
+ confidence: strictReviewed
746
+ ? "high"
747
+ : curatedBaseline
748
+ ? "medium"
749
+ : intelligenceState === "monitored"
750
+ ? "medium_high"
751
+ : candidate
752
+ ? "medium"
753
+ : "low",
754
+ disclosure: curatedBaseline
755
+ ? "This is a curated launch baseline, not a completed independent ASL evidence review."
756
+ : null,
757
+ catalog: candidate?.catalog || null
758
+ },
759
+ unknown_component: known
760
+ ? null
761
+ : {
762
+ should_submit: true,
763
+ submission_required: true,
764
+ reason: "No matching component intelligence record was found.",
765
+ submit_tool: "submit_unknown_component",
766
+ research_status_tool: "get_research_status",
767
+ retry_policy:
768
+ "Do not retry automatic installation until ASL publishes an intelligence record or the user explicitly approves a restricted install."
769
+ },
770
+ resolution
771
+ };
772
+ }