@opscompass/skills 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/manifest.json +2 -2
- package/package.json +1 -1
- package/skills/opscompass-oracle-licensing/.opscompass-runtime/tools/opsc-cli-context.js +34 -0
- package/skills/opscompass-oracle-licensing/.opscompass-runtime/tools/opsc-cli.js +102 -0
- package/skills/opscompass-oracle-licensing/SKILL.md +11 -5
- package/skills/opscompass-oracle-licensing/references/evidence-model.md +6 -2
- package/skills/opscompass-oracle-licensing/references/report-structure.md +7 -3
- package/skills/opscompass-oracle-licensing/scripts/oracle-assessment.js +284 -0
- package/skills/opscompass-oracle-licensing/scripts/oracle-assessment.tool.js +32 -0
- package/skills/opscompass-oracle-licensing/scripts/tools.js +3 -2
- package/skills/opscompass-resources/.opscompass-runtime/tools/opsc-cli-context.js +10 -0
- package/skills/opscompass-resources/.opscompass-runtime/tools/opsc-cli.js +24 -12
- package/skills/opscompass-resources/SKILL.md +37 -11
- package/skills/opscompass-resources/references/resources-commands.md +15 -1
- package/skills/opscompass-resources/scripts/build-resources-command.tool.js +6 -1
- package/skills/opscompass-resources/scripts/build_resources_command.js +43 -5
- package/skills/opscompass-resources/scripts/list_and_fetch_latest_configs.js +4 -4
- package/skills/opscompass-resources/scripts/opscompass-resources.tool.js +1 -0
- package/skills/opscompass-resources/scripts/tools.js +1 -0
- package/skills/opscompass-oracle-licensing/api/entitlement-compliance/entitlement-compliance.json +0 -189
- package/skills/opscompass-oracle-licensing/api/entitlement-deployment/entitlement-deployment +0 -26
- package/skills/opscompass-oracle-licensing/api/entitlement-usage/entitlement-usage.json +0 -114
- package/skills/opscompass-oracle-licensing/assets/mock-entitlements/deployment-details.json +0 -61
- package/skills/opscompass-oracle-licensing/assets/mock-entitlements/license-usage-map.json +0 -70
- package/skills/opscompass-oracle-licensing/assets/mock-entitlements/overall-compliance.json +0 -36
- package/skills/opscompass-oracle-licensing/scripts/fake-entitlements-api.tool.js +0 -31
- package/skills/opscompass-oracle-licensing/scripts/fake_entitlements_api.js +0 -72
package/README.md
CHANGED
|
@@ -75,4 +75,12 @@ Report query errors as errors, not as an empty inventory. Authentication failure
|
|
|
75
75
|
|
|
76
76
|
## Oracle licensing evidence
|
|
77
77
|
|
|
78
|
-
The Oracle Licensing Skill
|
|
78
|
+
The Oracle Licensing Skill reads the latest persisted OpsCompass periodic Oracle assessment through `oracle-assessment.js`; it does not recalculate an assessment or use synthetic fixture payloads. The selected OpsCompass CLI must be installed, authenticated, and able to read a saved assessment. The helper returns a complete JSON page for one section (`entitlements`, `summary`, `license-usage-summary`, or `deployment-details`), mapping each section to the selected CLI's `get-oracle-assessment-{section}` operation.
|
|
79
|
+
|
|
80
|
+
For standalone use, request every page for every needed section. Pin the local `assessmentId` returned by the first response on all later pages and sections, replacing any initial MLSS assessment selector. Advance `--offset` by `--limit` until the section's returned array is shorter than the limit; request one more page when a page is exactly full. The default limit is 100. Preserve the returned assessment metadata and nested entitlement details, and identify missing customer/licensing evidence explicitly. This saved periodic assessment is distinct from `get-oracle-current-entitlements`.
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
node node_modules/@opscompass/skills/skills/opscompass-oracle-licensing/scripts/oracle-assessment.js entitlements --cli opscompass --company YOUR_COMPANY --account YOUR_ACCOUNT --limit 100 --offset 0
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
In a host integration, the `opscompass_oracle_licensing_assessment` tool uses the host-selected CLI context rather than a model-selected executable. Follow the [Oracle Licensing Skill](./skills/opscompass-oracle-licensing/SKILL.md) for evidence boundaries and the complete workflow.
|
package/manifest.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"scope": "public",
|
|
4
4
|
"name": "@opscompass/skills",
|
|
5
|
-
"version": "0.1
|
|
5
|
+
"version": "0.2.1",
|
|
6
6
|
"source": {
|
|
7
7
|
"repository": "https://gitlab.com/opscompass-hob/opscompass-ai/opscompassai.git",
|
|
8
|
-
"revision": "
|
|
8
|
+
"revision": "e71529c9bf8952fa65fc291216a07c54e15e29ad",
|
|
9
9
|
"dirty": false
|
|
10
10
|
},
|
|
11
11
|
"skills": [
|
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const CLI_COMMANDS = [
|
|
4
|
+
"opscompass",
|
|
5
|
+
"opscompassdev",
|
|
6
|
+
"opscompassdemo",
|
|
7
|
+
];
|
|
8
|
+
/** Resolve standalone CLI identity; embedded callers supply their trusted context directly. */
|
|
9
|
+
export function resolveCliContext(cliCommand) {
|
|
10
|
+
if (cliCommand !== undefined && !CLI_COMMANDS.includes(cliCommand)) {
|
|
11
|
+
throw new Error(`--cli must be one of: ${CLI_COMMANDS.join(", ")}.`);
|
|
12
|
+
}
|
|
13
|
+
const override = process.env.OPSCOMPASS_CLI_ENTRYPOINT;
|
|
14
|
+
const recognizedOverride = override !== undefined && CLI_COMMANDS.includes(override);
|
|
15
|
+
const selectedCommand = cliCommand ?? (recognizedOverride ? override : "opscompass");
|
|
16
|
+
// An explicit identity wins over an ambient command, while a local executable
|
|
17
|
+
// override may run that identity. Never infer an environment from a file path.
|
|
18
|
+
const cliEntryPoint = override && !recognizedOverride ? override : selectedCommand;
|
|
19
|
+
return {
|
|
20
|
+
cliEntryPoint,
|
|
21
|
+
cliCommand: selectedCommand,
|
|
22
|
+
opscompassHome: path.join(os.homedir(), `.${selectedCommand}`),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function resolveCliInvocation(context, args) {
|
|
26
|
+
return {
|
|
27
|
+
executable: context.cliInvocation?.executable ?? context.cliEntryPoint,
|
|
28
|
+
args: [...(context.cliInvocation?.args ?? []), ...args],
|
|
29
|
+
env: {
|
|
30
|
+
...(context.cliInvocation?.env ?? process.env),
|
|
31
|
+
OPSCOMPASS_CLI_ENTRYPOINT: context.cliEntryPoint,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { resolveCliInvocation } from "./opsc-cli-context.js";
|
|
3
|
+
const MAX_TIMEOUT_SECONDS = 120;
|
|
4
|
+
const MAX_OUTPUT_BYTES = 100_000;
|
|
5
|
+
export async function runCli(context, args) {
|
|
6
|
+
if (context.signal.aborted)
|
|
7
|
+
throw new Error("Opscompass command aborted.");
|
|
8
|
+
const invocation = resolveCliInvocation(context, args);
|
|
9
|
+
const child = spawn(invocation.executable, invocation.args, {
|
|
10
|
+
shell: false,
|
|
11
|
+
cwd: context.workspaceRoot,
|
|
12
|
+
// The host-selected executable must win over ambient standalone overrides.
|
|
13
|
+
env: {
|
|
14
|
+
...invocation.env,
|
|
15
|
+
ELECTRON_RUN_AS_NODE: "1",
|
|
16
|
+
OPSCOMPASS_STUDIO: "1",
|
|
17
|
+
},
|
|
18
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
19
|
+
detached: process.platform !== "win32",
|
|
20
|
+
});
|
|
21
|
+
const stdout = [];
|
|
22
|
+
const stderr = [];
|
|
23
|
+
let stdoutBytes = 0;
|
|
24
|
+
let stderrBytes = 0;
|
|
25
|
+
let timedOut = false;
|
|
26
|
+
const collect = (chunks, chunk, outputBytes) => {
|
|
27
|
+
if (outputBytes >= MAX_OUTPUT_BYTES)
|
|
28
|
+
return outputBytes;
|
|
29
|
+
const bounded = chunk.subarray(0, MAX_OUTPUT_BYTES - outputBytes);
|
|
30
|
+
chunks.push(bounded);
|
|
31
|
+
return outputBytes + bounded.length;
|
|
32
|
+
};
|
|
33
|
+
child.stdout?.on("data", (chunk) => {
|
|
34
|
+
stdoutBytes = collect(stdout, chunk, stdoutBytes);
|
|
35
|
+
});
|
|
36
|
+
child.stderr?.on("data", (chunk) => {
|
|
37
|
+
stderrBytes = collect(stderr, chunk, stderrBytes);
|
|
38
|
+
});
|
|
39
|
+
let graceTimer;
|
|
40
|
+
const forceKill = () => {
|
|
41
|
+
try {
|
|
42
|
+
if (process.platform !== "win32" && child.pid)
|
|
43
|
+
process.kill(-child.pid, "SIGKILL");
|
|
44
|
+
else
|
|
45
|
+
child.kill("SIGKILL");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
try {
|
|
49
|
+
child.kill("SIGKILL");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// The child may have exited between termination attempts.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const kill = () => {
|
|
57
|
+
try {
|
|
58
|
+
const sent = process.platform !== "win32" && child.pid
|
|
59
|
+
? process.kill(-child.pid, "SIGTERM")
|
|
60
|
+
: child.kill("SIGTERM");
|
|
61
|
+
if (process.platform !== "win32" && sent && graceTimer === undefined) {
|
|
62
|
+
graceTimer = setTimeout(forceKill, 5_000);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
forceKill();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const result = new Promise((resolve, reject) => {
|
|
70
|
+
child.once("error", reject);
|
|
71
|
+
child.once("close", (code) => {
|
|
72
|
+
const output = Buffer.concat(stdout).toString("utf8");
|
|
73
|
+
const diagnostic = Buffer.concat(stderr).toString("utf8");
|
|
74
|
+
if (context.signal.aborted)
|
|
75
|
+
reject(new Error("Opscompass command aborted."));
|
|
76
|
+
else if (timedOut)
|
|
77
|
+
reject(new Error(`Opscompass command timed out after ${MAX_TIMEOUT_SECONDS} seconds.`));
|
|
78
|
+
else if (code !== 0)
|
|
79
|
+
reject(new Error(diagnostic.trim() ||
|
|
80
|
+
output.trim() ||
|
|
81
|
+
`Opscompass command failed with exit code ${code}.`));
|
|
82
|
+
else
|
|
83
|
+
resolve(output);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
const timeout = setTimeout(() => {
|
|
87
|
+
timedOut = true;
|
|
88
|
+
kill();
|
|
89
|
+
}, MAX_TIMEOUT_SECONDS * 1_000);
|
|
90
|
+
if (context.signal.aborted)
|
|
91
|
+
kill();
|
|
92
|
+
else
|
|
93
|
+
context.signal.addEventListener("abort", kill, { once: true });
|
|
94
|
+
try {
|
|
95
|
+
return await result;
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
clearTimeout(graceTimer);
|
|
100
|
+
context.signal.removeEventListener("abort", kill);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -97,12 +97,15 @@ Review available entitlement evidence for:
|
|
|
97
97
|
- applicability to workloads in scope
|
|
98
98
|
- missing or ambiguous entitlement records
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
- `node scripts/fake_entitlements_api.js deployment-details`
|
|
102
|
-
- `node scripts/fake_entitlements_api.js license-usage-map`
|
|
103
|
-
- `node scripts/fake_entitlements_api.js overall-compliance`
|
|
100
|
+
Use `opscompass_oracle_licensing_assessment` for actual licensing-assessment evidence. It reads persisted Opscompass assessment data through the selected CLI; it does not calculate an assessment or fetch live-current entitlements. The four `section` values are `entitlements`, `summary`, `license-usage-summary`, and `deployment-details`. Each call returns one complete CLI JSON page, including the assessment metadata, pagination, section array, and nested details. Preserve the returned envelope and provenance; do not summarize away entitlement details or evidence sources before analysis.
|
|
104
101
|
|
|
105
|
-
|
|
102
|
+
Fetch every page of all four sections. Start at `offset: 0`, using the default `limit: 100` unless a different page size is useful. Continue by adding the returned `pagination.limit` to the offset until that section's array contains fewer rows than the limit. If a page is exactly full, request the next page even if it may be empty; an empty section array is a valid final page. Use the returned local `assessmentId` on every subsequent page and section. `mlssAssessmentId` is a different identifier namespace and may select the first page only; replace it with the returned local `assessmentId` immediately. `account` is an opaque string, including when it looks numeric.
|
|
103
|
+
|
|
104
|
+
The assessment tool accepts optional `company` and `account` scope, and at most one of `assessmentId` or `mlssAssessmentId`. In embedded use, the trusted host context selects the executable and environment; never accept a model-selected `--cli`. Standalone `scripts/oracle-assessment.js` accepts the section and matching kebab-case flags, plus `--cli opscompass|opscompassdev|opscompassdemo`. Do not substitute `get-oracle-current-entitlements`: it is a separate command and does not represent this persisted assessment.
|
|
105
|
+
|
|
106
|
+
If company or account is omitted, the selected CLI may use its own saved defaults; explicitly provide scope when needed to establish the assessment boundary. Blank or malformed explicit values must fail rather than silently fall back to a saved default.
|
|
107
|
+
|
|
108
|
+
Treat this source as the latest persisted periodic assessment, not a recalculation or proof of present-day entitlements. Record the selected CLI identity, company/account scope, local `assessmentId`, assessment name, and any source/as-of date actually returned. Distinguish an assessment's as-of date from the retrieval date; if freshness is unknown, say so. Entitlement rows and `entitlementDetails` are evidence with provenance, not independently verified proof of purchased rights.
|
|
106
109
|
|
|
107
110
|
6. Analyze current-state exposure.
|
|
108
111
|
Assess:
|
|
@@ -159,6 +162,9 @@ For Oracle on VMware or Nutanix, workload-to-infrastructure correlation is manda
|
|
|
159
162
|
|
|
160
163
|
Lead with licensing risk and business-impacting recommendations, not raw inventory.
|
|
161
164
|
|
|
165
|
+
- Record the selected Opscompass CLI identity, company/account scope, assessment name, local `assessmentId`, and source/as-of date when returned. Distinguish the latest persisted periodic assessment from recalculation or live-current entitlement data, and distinguish its as-of date from retrieval time.
|
|
166
|
+
- Save final user-facing assessment artifacts under `Documents/` with the existing Documents write tool. Claim a saved artifact only after the write succeeds.
|
|
167
|
+
|
|
162
168
|
Structure the final answer in this order when possible:
|
|
163
169
|
|
|
164
170
|
1. Assessment summary
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use this file when you need to reason about what evidence is required for an Oracle Optimized License Assessment focused on AWS Oracle target-state optimization.
|
|
4
4
|
|
|
5
|
-
Record the selected OpsCompass environment, company scope, and evidence provenance.
|
|
5
|
+
Record the selected OpsCompass CLI environment, company/account scope, and evidence provenance. For persisted assessment evidence, retain the returned local `assessmentId`, assessment name, relevant returned date, section, and page offsets. Keep the source/as-of date separate from retrieval time; if freshness cannot be established, say so. Never combine another environment's defaults or credentials into the assessment. Command previews (`execute: false`) are not collected evidence, and failed retrievals must be recorded as gaps with their actual cause rather than assumed to be expired login.
|
|
6
6
|
|
|
7
7
|
## Workload Identity
|
|
8
8
|
|
|
@@ -56,7 +56,7 @@ Try to establish:
|
|
|
56
56
|
|
|
57
57
|
If entitlement evidence is incomplete, the assessment must say so explicitly.
|
|
58
58
|
|
|
59
|
-
Use
|
|
59
|
+
Use `opscompass_oracle_licensing_assessment` or its standalone CLI to retrieve the actual persisted assessment pages for `entitlements`, `summary`, `license-usage-summary`, and `deployment-details`. Each call returns one complete JSON page. Fetch every page, pin the returned local `assessmentId` on all pages and sections after the first response, and continue until a section array is shorter than the returned `pagination.limit`; fetch an empty page if the prior page was exactly full. `mlssAssessmentId` selects an initial assessment in a separate identifier namespace; replace it with the returned local `assessmentId`. The data is the latest persisted periodic assessment, not a recalculation or live-current entitlement query. `get-oracle-current-entitlements` is separate and must not be substituted. Preserve the full metadata, `entReviewName`, nested `entitlementDetails`, and any provenance rather than extracting only conclusions. User-authorized assessment data is evidence with provenance, not by itself proof of purchased rights.
|
|
60
60
|
|
|
61
61
|
## AWS Target-State Evidence
|
|
62
62
|
|
|
@@ -88,3 +88,7 @@ For each conclusion, distinguish:
|
|
|
88
88
|
- directly established evidence
|
|
89
89
|
- strong inference
|
|
90
90
|
- partial conclusion because key evidence is missing
|
|
91
|
+
|
|
92
|
+
## Provenance, Freshness, And Artifacts
|
|
93
|
+
|
|
94
|
+
Record the selected CLI identity, actual company/account scope, local assessment ID, assessment name, source/as-of date when returned, and retrieved sections/pages. Distinguish the assessment's as-of date from retrieval time and state when freshness is unknown. Save final user-facing assessment artifacts under `Documents/` with the existing Documents write tool, and report them as saved only after the write succeeds.
|
|
@@ -34,9 +34,13 @@ Use this file when the user wants a written OLA-style assessment focused on opti
|
|
|
34
34
|
- Include current host hardware/core evidence when available, because current Oracle environments are often licensed by CPU core.
|
|
35
35
|
- If compute, host hardware, or hosting evidence is missing, say the Oracle licensing conclusion is partial.
|
|
36
36
|
- If entitlement evidence is missing, say the Oracle licensing conclusion is partial.
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
37
|
+
- Record the selected Opscompass CLI identity, company/account scope, returned local `assessmentId`, assessment name, and source/as-of date when present. Keep retrieval time distinct from the assessment's as-of date and state when freshness is unknown.
|
|
38
|
+
- Identify the report as based on the latest persisted periodic assessment, not a recalculation or live-current entitlements. Do not substitute `get-oracle-current-entitlements`.
|
|
39
|
+
- Retrieve all pages of `entitlements`, `summary`, `license-usage-summary`, and `deployment-details`; after the first response, use its local `assessmentId` for every section/page, even if selection began with `mlssAssessmentId`.
|
|
40
|
+
- Continue pagination by the returned `pagination.limit` until the section array is shorter than the limit; request an empty final page when the preceding page is exactly full.
|
|
41
|
+
- Preserve the complete page metadata, entitlement `entitlementDetails`, and provenance. Usage/compliance evidence does not establish purchased rights.
|
|
42
|
+
- Base entitlement comparisons on explicit user-provided or user-authorized records, and name missing customer evidence rather than treating saved assessment output as proof of entitlement.
|
|
43
|
+
- Save the final user-facing report under `Documents/` with the existing Documents write tool; claim it is saved only after the write succeeds.
|
|
40
44
|
|
|
41
45
|
## Recommended Finding Format
|
|
42
46
|
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { isMainModule } from "../.opscompass-runtime/tools/cli-entrypoint.js";
|
|
2
|
+
import { resolveCliContext } from "../.opscompass-runtime/tools/opsc-cli-context.js";
|
|
3
|
+
import { runCli } from "../.opscompass-runtime/tools/opsc-cli.js";
|
|
4
|
+
export const ASSESSMENT_SECTIONS = [
|
|
5
|
+
"entitlements",
|
|
6
|
+
"summary",
|
|
7
|
+
"license-usage-summary",
|
|
8
|
+
"deployment-details",
|
|
9
|
+
];
|
|
10
|
+
const SECTION_FIELDS = {
|
|
11
|
+
entitlements: "assessmentEntitlements",
|
|
12
|
+
summary: "assessmentSummary",
|
|
13
|
+
"license-usage-summary": "licenseUsageSummary",
|
|
14
|
+
"deployment-details": "deploymentDetails",
|
|
15
|
+
};
|
|
16
|
+
const SECTION_FIELD_NAMES = Object.values(SECTION_FIELDS);
|
|
17
|
+
const ASSESSMENT_METADATA = [
|
|
18
|
+
"assessmentId",
|
|
19
|
+
"assessmentName",
|
|
20
|
+
"currencyCode",
|
|
21
|
+
"expectedDiscount",
|
|
22
|
+
"deployMapId",
|
|
23
|
+
"entReviewId",
|
|
24
|
+
];
|
|
25
|
+
const PRODUCT_FIELDS = ["masterProductId", "productString", "licenseMetric", "licenseLevel", "limitation"];
|
|
26
|
+
// WHY: A successful CLI exit is not proof that quantities or nested licensing evidence were returned completely.
|
|
27
|
+
const ROW_FIELDS = {
|
|
28
|
+
entitlements: [...PRODUCT_FIELDS, "entReviewProductId", "entitlementId", "quantity", "notes", "entitlementDetails"],
|
|
29
|
+
summary: [...PRODUCT_FIELDS, "assessmentProductId", "deployedQuantity", "entitledQuantity", "availableQuantity", "lineListPrice", "lineDiscountPrice", "lineTotalPrice", "comments"],
|
|
30
|
+
"license-usage-summary": [...PRODUCT_FIELDS, "deployMapProductId", "quantity", "comments", "deployMapProductDetails"],
|
|
31
|
+
"deployment-details": ["deployMapId", "deploymentInstanceId", "physicalMachineName", "virtualMachineName", "instanceName", "productEdition", "productVersion", "dbCreationDate", "deploymentDetails"],
|
|
32
|
+
};
|
|
33
|
+
const NESTED_DETAILS = {
|
|
34
|
+
entitlements: { field: "entitlementDetails", fields: ["orderNumber", "csi", "supportRenewalNumber", "quantity", "supportStartDate", "supportExpirationDate", "supportPrice", "currencyCode", "comments"] },
|
|
35
|
+
"license-usage-summary": { field: "deployMapProductDetails", fields: ["datacenterName", "clusterName", "physicalMachineId", "physicalMachineName", "totalSocketsInUse", "totalCoresInUse", "coreFactor", "minQuantity", "quantity"] },
|
|
36
|
+
"deployment-details": { field: "deploymentDetails", fields: ["masterProductId", "productString", "featureId", "featureString", "detectedUsages", "firstUsageDate", "lastUsageDate", "isExcluded", "notes"] },
|
|
37
|
+
};
|
|
38
|
+
function requireFields(value, fields) {
|
|
39
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
40
|
+
throw new Error("Oracle assessment contains an invalid evidence record.");
|
|
41
|
+
for (const field of fields) {
|
|
42
|
+
if (!Object.hasOwn(value, field))
|
|
43
|
+
throw new Error(`Oracle assessment evidence is missing ${field}.`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const INPUT_FIELDS = {
|
|
47
|
+
section: true,
|
|
48
|
+
company: true,
|
|
49
|
+
account: true,
|
|
50
|
+
assessmentId: true,
|
|
51
|
+
mlssAssessmentId: true,
|
|
52
|
+
limit: true,
|
|
53
|
+
offset: true,
|
|
54
|
+
};
|
|
55
|
+
function optionalString(input, name) {
|
|
56
|
+
if (!Object.hasOwn(input, name))
|
|
57
|
+
return undefined;
|
|
58
|
+
const value = input[name];
|
|
59
|
+
if (typeof value !== "string" ||
|
|
60
|
+
value.trim() === "" ||
|
|
61
|
+
/^\s*--/.test(value) ||
|
|
62
|
+
value.includes("\0")) {
|
|
63
|
+
throw new Error(`${name} must be a non-empty string value.`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function optionalPositiveInteger(input, name) {
|
|
68
|
+
if (!Object.hasOwn(input, name))
|
|
69
|
+
return undefined;
|
|
70
|
+
const value = input[name];
|
|
71
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
|
|
72
|
+
throw new Error(`${name} must be a positive safe integer.`);
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
export function parseAssessmentInput(input) {
|
|
76
|
+
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
77
|
+
throw new Error("Assessment input must be an object.");
|
|
78
|
+
const values = input;
|
|
79
|
+
for (const key of Object.keys(values)) {
|
|
80
|
+
if (!Object.hasOwn(INPUT_FIELDS, key))
|
|
81
|
+
throw new Error(`Unknown assessment input: ${key}.`);
|
|
82
|
+
}
|
|
83
|
+
if (!Object.hasOwn(values, "section"))
|
|
84
|
+
throw new Error("section is required.");
|
|
85
|
+
if (typeof values.section !== "string" ||
|
|
86
|
+
!ASSESSMENT_SECTIONS.includes(values.section)) {
|
|
87
|
+
throw new Error(`section must be one of: ${ASSESSMENT_SECTIONS.join(", ")}.`);
|
|
88
|
+
}
|
|
89
|
+
const company = optionalString(values, "company");
|
|
90
|
+
const account = optionalString(values, "account");
|
|
91
|
+
const assessmentId = optionalPositiveInteger(values, "assessmentId");
|
|
92
|
+
const mlssAssessmentId = optionalPositiveInteger(values, "mlssAssessmentId");
|
|
93
|
+
if (assessmentId !== undefined && mlssAssessmentId !== undefined)
|
|
94
|
+
throw new Error("assessmentId and mlssAssessmentId are mutually exclusive.");
|
|
95
|
+
const limit = Object.hasOwn(values, "limit") ? values.limit : 100;
|
|
96
|
+
if (typeof limit !== "number" ||
|
|
97
|
+
!Number.isSafeInteger(limit) ||
|
|
98
|
+
limit < 1 ||
|
|
99
|
+
limit > 500) {
|
|
100
|
+
throw new Error("limit must be an integer from 1 through 500.");
|
|
101
|
+
}
|
|
102
|
+
const offset = Object.hasOwn(values, "offset") ? values.offset : 0;
|
|
103
|
+
if (typeof offset !== "number" ||
|
|
104
|
+
!Number.isSafeInteger(offset) ||
|
|
105
|
+
offset < 0) {
|
|
106
|
+
throw new Error("offset must be a nonnegative safe integer.");
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
section: values.section,
|
|
110
|
+
...(company === undefined ? {} : { company }),
|
|
111
|
+
...(account === undefined ? {} : { account }),
|
|
112
|
+
...(assessmentId === undefined ? {} : { assessmentId }),
|
|
113
|
+
...(mlssAssessmentId === undefined ? {} : { mlssAssessmentId }),
|
|
114
|
+
limit,
|
|
115
|
+
offset,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function buildCommand(request) {
|
|
119
|
+
const args = [
|
|
120
|
+
"license-manager",
|
|
121
|
+
`get-oracle-assessment-${request.section}`,
|
|
122
|
+
];
|
|
123
|
+
for (const [name, value] of [
|
|
124
|
+
["company", request.company],
|
|
125
|
+
["account", request.account],
|
|
126
|
+
["assessment-id", request.assessmentId],
|
|
127
|
+
["mlss-assessment-id", request.mlssAssessmentId],
|
|
128
|
+
["limit", request.limit],
|
|
129
|
+
["offset", request.offset],
|
|
130
|
+
]) {
|
|
131
|
+
if (value !== undefined)
|
|
132
|
+
args.push(`--${name}`, String(value));
|
|
133
|
+
}
|
|
134
|
+
return args;
|
|
135
|
+
}
|
|
136
|
+
function validatePage(text, request) {
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(text);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
throw new Error("Oracle assessment CLI returned invalid or truncated JSON.");
|
|
143
|
+
}
|
|
144
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
145
|
+
throw new Error("Oracle assessment CLI returned an incomplete assessment page.");
|
|
146
|
+
const page = parsed;
|
|
147
|
+
if (Object.hasOwn(page, "error") || page.success === false)
|
|
148
|
+
throw new Error("Oracle assessment CLI returned an error response.");
|
|
149
|
+
for (const field of ASSESSMENT_METADATA) {
|
|
150
|
+
if (!Object.hasOwn(page, field))
|
|
151
|
+
throw new Error(`Oracle assessment page is missing ${field}.`);
|
|
152
|
+
}
|
|
153
|
+
if (typeof page.assessmentId !== "number" ||
|
|
154
|
+
!Number.isSafeInteger(page.assessmentId) ||
|
|
155
|
+
page.assessmentId < 1) {
|
|
156
|
+
throw new Error("Oracle assessment page has an invalid local assessmentId.");
|
|
157
|
+
}
|
|
158
|
+
if (request.assessmentId !== undefined &&
|
|
159
|
+
page.assessmentId !== request.assessmentId) {
|
|
160
|
+
throw new Error("Oracle assessment CLI returned a different assessmentId.");
|
|
161
|
+
}
|
|
162
|
+
if (typeof page.assessmentName !== "string")
|
|
163
|
+
throw new Error("Oracle assessment page has an invalid assessmentName.");
|
|
164
|
+
const sectionField = SECTION_FIELDS[request.section];
|
|
165
|
+
for (const field of SECTION_FIELD_NAMES) {
|
|
166
|
+
if (field !== sectionField && Object.hasOwn(page, field))
|
|
167
|
+
throw new Error(`Oracle assessment CLI returned the wrong section instead of ${sectionField}.`);
|
|
168
|
+
}
|
|
169
|
+
const rows = page[sectionField];
|
|
170
|
+
if (!Array.isArray(rows) || rows.length > request.limit)
|
|
171
|
+
throw new Error(`Oracle assessment page has an invalid ${sectionField} array.`);
|
|
172
|
+
if (request.section === "entitlements" &&
|
|
173
|
+
typeof page.entReviewName !== "string") {
|
|
174
|
+
throw new Error("Oracle assessment entitlement page is missing entReviewName.");
|
|
175
|
+
}
|
|
176
|
+
if ((request.section === "license-usage-summary" || request.section === "deployment-details") && typeof page.deployMapName !== "string")
|
|
177
|
+
throw new Error("Oracle assessment page is missing deployMapName.");
|
|
178
|
+
const nested = NESTED_DETAILS[request.section];
|
|
179
|
+
for (const row of rows) {
|
|
180
|
+
requireFields(row, ROW_FIELDS[request.section]);
|
|
181
|
+
if (nested) {
|
|
182
|
+
const details = row[nested.field];
|
|
183
|
+
if (!Array.isArray(details))
|
|
184
|
+
throw new Error(`Oracle assessment evidence has invalid ${nested.field}.`);
|
|
185
|
+
for (const detail of details)
|
|
186
|
+
requireFields(detail, nested.fields);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (page.pagination === null ||
|
|
190
|
+
typeof page.pagination !== "object" ||
|
|
191
|
+
Array.isArray(page.pagination)) {
|
|
192
|
+
throw new Error("Oracle assessment page is missing pagination metadata.");
|
|
193
|
+
}
|
|
194
|
+
const pagination = page.pagination;
|
|
195
|
+
if (pagination.limit !== request.limit ||
|
|
196
|
+
pagination.offset !== request.offset) {
|
|
197
|
+
throw new Error("Oracle assessment page pagination does not match the request.");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export async function readAssessmentPage(request, context) {
|
|
201
|
+
const output = await runCli(context, buildCommand(request));
|
|
202
|
+
validatePage(output, request);
|
|
203
|
+
return output;
|
|
204
|
+
}
|
|
205
|
+
function parseCliInteger(value, name) {
|
|
206
|
+
if (!/^\d+$/.test(value))
|
|
207
|
+
throw new Error(`${name} must be an integer.`);
|
|
208
|
+
const number = Number(value);
|
|
209
|
+
if (!Number.isSafeInteger(number))
|
|
210
|
+
throw new Error(`${name} must be a safe integer.`);
|
|
211
|
+
return number;
|
|
212
|
+
}
|
|
213
|
+
function parseCliArgs(argv) {
|
|
214
|
+
const input = {};
|
|
215
|
+
const seen = new Set();
|
|
216
|
+
let section;
|
|
217
|
+
let cliCommand;
|
|
218
|
+
const flags = {
|
|
219
|
+
"--company": "company",
|
|
220
|
+
"--account": "account",
|
|
221
|
+
"--assessment-id": "assessmentId",
|
|
222
|
+
"--mlss-assessment-id": "mlssAssessmentId",
|
|
223
|
+
"--limit": "limit",
|
|
224
|
+
"--offset": "offset",
|
|
225
|
+
"--cli": "cli",
|
|
226
|
+
};
|
|
227
|
+
for (let index = 0; index < argv.length; index++) {
|
|
228
|
+
const arg = argv[index];
|
|
229
|
+
if (arg.startsWith("--")) {
|
|
230
|
+
const key = Object.hasOwn(flags, arg) ? flags[arg] : undefined;
|
|
231
|
+
if (key === undefined)
|
|
232
|
+
throw new Error(`Unknown option: ${arg}.`);
|
|
233
|
+
if (seen.has(arg))
|
|
234
|
+
throw new Error(`Option ${arg} may be specified only once.`);
|
|
235
|
+
seen.add(arg);
|
|
236
|
+
const value = argv[++index];
|
|
237
|
+
if (value === undefined || value.startsWith("--"))
|
|
238
|
+
throw new Error(`Option ${arg} requires a value.`);
|
|
239
|
+
if (key === "cli")
|
|
240
|
+
cliCommand = value;
|
|
241
|
+
else if (key === "assessmentId" ||
|
|
242
|
+
key === "mlssAssessmentId" ||
|
|
243
|
+
key === "limit" ||
|
|
244
|
+
key === "offset") {
|
|
245
|
+
input[key] = parseCliInteger(value, arg);
|
|
246
|
+
}
|
|
247
|
+
else
|
|
248
|
+
input[key] = value;
|
|
249
|
+
}
|
|
250
|
+
else if (section === undefined)
|
|
251
|
+
section = arg;
|
|
252
|
+
else
|
|
253
|
+
throw new Error(`Unexpected argument: ${arg}.`);
|
|
254
|
+
}
|
|
255
|
+
if (section === undefined)
|
|
256
|
+
throw new Error("An assessment section is required.");
|
|
257
|
+
input.section = section;
|
|
258
|
+
return {
|
|
259
|
+
request: parseAssessmentInput(input),
|
|
260
|
+
...(cliCommand === undefined ? {} : { cliCommand }),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
264
|
+
if (argv.length === 1 && argv[0] === "--help") {
|
|
265
|
+
process.stdout.write("Usage: oracle-assessment.js <section> [--company <name>] [--account <value>] [--assessment-id <id> | --mlss-assessment-id <id>] [--limit <1-500>] [--offset <n>] [--cli opscompass|opscompassdev|opscompassdemo]\n");
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const { request, cliCommand } = parseCliArgs(argv);
|
|
270
|
+
const context = {
|
|
271
|
+
...resolveCliContext(cliCommand),
|
|
272
|
+
workspaceRoot: process.cwd(),
|
|
273
|
+
signal: new AbortController().signal,
|
|
274
|
+
};
|
|
275
|
+
process.stdout.write(await readAssessmentPage(request, context));
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
280
|
+
return 1;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (isMainModule(import.meta.url))
|
|
284
|
+
process.exitCode = await main();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ASSESSMENT_SECTIONS, parseAssessmentInput, readAssessmentPage, } from "./oracle-assessment.js";
|
|
2
|
+
export const oracleAssessmentTool = {
|
|
3
|
+
name: "opscompass_oracle_licensing_assessment",
|
|
4
|
+
label: "Oracle licensing assessment evidence",
|
|
5
|
+
description: "Read one page from a persisted Opscompass Oracle license assessment section.",
|
|
6
|
+
promptSnippet: "Fetch persisted Opscompass Oracle licensing assessment evidence",
|
|
7
|
+
promptGuidelines: [
|
|
8
|
+
"This reads the latest persisted periodic assessment; it does not recalculate an assessment or return live current entitlements.",
|
|
9
|
+
"Fetch every page for all four sections. After the first response, pin its local assessmentId for all pages and sections, including when the first selector was mlssAssessmentId.",
|
|
10
|
+
"Advance offset by pagination.limit until the returned section array is shorter than the limit; request an empty page when the last page is exactly full.",
|
|
11
|
+
"Preserve the returned assessment metadata, nested entitlementDetails, and provenance; usage signals do not establish purchased rights.",
|
|
12
|
+
],
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
section: { type: "string", enum: [...ASSESSMENT_SECTIONS] },
|
|
17
|
+
company: { type: "string" },
|
|
18
|
+
account: { type: "string" },
|
|
19
|
+
assessmentId: { type: "integer", minimum: 1 },
|
|
20
|
+
mlssAssessmentId: { type: "integer", minimum: 1 },
|
|
21
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
22
|
+
offset: { type: "integer", minimum: 0 },
|
|
23
|
+
},
|
|
24
|
+
required: ["section"],
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
},
|
|
27
|
+
async execute(input, context) {
|
|
28
|
+
const request = parseAssessmentInput(input);
|
|
29
|
+
const output = await readAssessmentPage(request, context);
|
|
30
|
+
return { content: [{ type: "text", text: output }] };
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export const
|
|
1
|
+
import { oracleAssessmentTool } from "./oracle-assessment.tool.js";
|
|
2
|
+
export const supportsCliInvocation = true;
|
|
3
|
+
export const tools = [oracleAssessmentTool];
|
|
@@ -22,3 +22,13 @@ export function resolveCliContext(cliCommand) {
|
|
|
22
22
|
opscompassHome: path.join(os.homedir(), `.${selectedCommand}`),
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
+
export function resolveCliInvocation(context, args) {
|
|
26
|
+
return {
|
|
27
|
+
executable: context.cliInvocation?.executable ?? context.cliEntryPoint,
|
|
28
|
+
args: [...(context.cliInvocation?.args ?? []), ...args],
|
|
29
|
+
env: {
|
|
30
|
+
...(context.cliInvocation?.env ?? process.env),
|
|
31
|
+
OPSCOMPASS_CLI_ENTRYPOINT: context.cliEntryPoint,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|