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,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
|
+
}
|