@namewta/speculo 1.0.17 → 1.0.18
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/package.json +1 -1
- package/template/workflows/ops/D-project-deploy/D-project-deploy.md +11 -9
- package/template/workflows/ops/H-host-manage/H-host-manage.md +11 -10
- package/template/workflows/ops/I-initialize/I-initialize.md +7 -7
- package/template/workflows/ops/INDEX.md +20 -16
- package/template/workflows/ops/README.md +27 -9
- package/template/workflows/ops/S-server-connect/S-server-connect.md +28 -0
- package/template/workflows/ops/V-inventory-view/V-inventory-view.md +30 -0
- package/template/workflows/ops/common/CAPABILITIES.md +22 -15
- package/template/workflows/ops/common/WORKSPACE-USAGE.md +78 -0
- package/template/workflows/ops/common/examples/task-host.example.json +64 -0
- package/template/workflows/ops/common/rules/activation-and-memory.md +5 -3
- package/template/workflows/ops/common/rules/inventory-and-records.md +55 -0
- package/template/workflows/ops/common/rules/research-and-decisions.md +18 -0
- package/template/workflows/ops/common/rules/server-onboarding.md +21 -0
- package/template/workflows/ops/common/rules/workspace-and-authorization.md +39 -0
- package/template/workflows/ops/common/schemas/fleet-view.schema.json +487 -0
- package/template/workflows/ops/common/schemas/server-check.schema.json +95 -0
- package/template/workflows/ops/common/schemas/task-request.schema.json +138 -0
- package/template/workflows/ops/common/templates/FLEET.html +61 -0
- package/template/workflows/ops/common/tests/test_ops_workspace.mjs +246 -0
- package/template/workflows/ops/common/tools/ops.mjs +5 -3
- package/template/workflows/ops/common/tools/opslib/fleet.mjs +155 -0
- package/template/workflows/ops/common/tools/opslib/server_checks.mjs +149 -0
- package/template/workflows/ops/common/tools/opslib/tasks.mjs +313 -0
- package/template/workflows/ops/common/tools/opslib/workspace.mjs +107 -0
- package/template/workflows/ops/common/tools/opslib/workspace_cli.mjs +80 -0
- package/template/workflows/ops/common/tools/validate-ops.mjs +11 -8
- package/template/workflows/ops/manifest.json +43 -7
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Explicit controller/server/project routing and bounded workspace paths. */
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, relative as pathRelative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { digest, exact, identifier, noSymlinks, OpsError, readJson } from "./core.mjs";
|
|
6
|
+
|
|
7
|
+
export const WORKS = Object.freeze({
|
|
8
|
+
controller: "I-initialize", access: "S-server-connect", server: "H-host-manage",
|
|
9
|
+
project: "D-project-deploy", inventory: "V-inventory-view",
|
|
10
|
+
});
|
|
11
|
+
const WORKFLOW = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
12
|
+
export const MAX_JSON_BYTES = 16 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
export function statePath(value) {
|
|
15
|
+
if (typeof value !== "string" || !isAbsolute(value) || value.includes("\0") || value.split(/[\\/]/).includes("..")) {
|
|
16
|
+
throw new OpsError("--state must be an explicit absolute path without traversal; never inferred from cwd");
|
|
17
|
+
}
|
|
18
|
+
const state = resolve(value);
|
|
19
|
+
const rel = pathRelative(WORKFLOW, state);
|
|
20
|
+
if (dirname(state) === state || rel === "" || (rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute(rel))) {
|
|
21
|
+
throw new OpsError("state must be a dedicated runtime directory outside static workflow assets");
|
|
22
|
+
}
|
|
23
|
+
noSymlinks(state, { allowMissing: false });
|
|
24
|
+
if (!statSync(state).isDirectory()) throw new OpsError("state is not a directory");
|
|
25
|
+
return state;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readBoundedJson(path) {
|
|
29
|
+
noSymlinks(path, { allowMissing: false });
|
|
30
|
+
const st = statSync(path);
|
|
31
|
+
if (!st.isFile() || st.size > MAX_JSON_BYTES) throw new OpsError("JSON file is not regular or exceeds 16 MiB");
|
|
32
|
+
return readJson(path);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readRegistry(state) {
|
|
36
|
+
statePath(state);
|
|
37
|
+
const value = readBoundedJson(join(state, "status.json"));
|
|
38
|
+
if (value.schema_version !== 3 || value.workflow !== "ops" || !Number.isSafeInteger(value.revision) || value.revision < 0) {
|
|
39
|
+
throw new OpsError("workspace requires OPS status schema v3; legacy state must be imported, not overwritten");
|
|
40
|
+
}
|
|
41
|
+
if (!value.controller || !value.controller.controller_id) throw new OpsError("initialize the controller first");
|
|
42
|
+
identifier(value.controller.controller_id, "controller_id");
|
|
43
|
+
if (typeof value.controller.state_root !== "string" || resolve(value.controller.state_root) !== resolve(state)) throw new OpsError("controller state_root mismatch; do not silently adopt another workspace");
|
|
44
|
+
for (const key of ["hosts", "projects", "deployments", "bindings", "allocations", "releases"]) {
|
|
45
|
+
if (!value[key] || typeof value[key] !== "object" || Array.isArray(value[key])) throw new OpsError(`invalid registry map: ${key}`);
|
|
46
|
+
if (Object.keys(value[key]).length > 10000) throw new OpsError(`registry map exceeds workspace bound: ${key}`);
|
|
47
|
+
for (const id of Object.keys(value[key])) identifier(id, key);
|
|
48
|
+
}
|
|
49
|
+
for (const [id, h] of Object.entries(value.hosts)) {
|
|
50
|
+
if (!h || h.host_id !== id || !["local", "ssh"].includes(h.transport)) throw new OpsError(`invalid host record: ${id}`);
|
|
51
|
+
}
|
|
52
|
+
for (const [id, p] of Object.entries(value.projects)) {
|
|
53
|
+
if (!p || p.project_id !== id) throw new OpsError(`invalid project record: ${id}`);
|
|
54
|
+
}
|
|
55
|
+
for (const [id, d] of Object.entries(value.deployments)) {
|
|
56
|
+
if (!d || d.deployment_id !== id || !Object.hasOwn(value.hosts, d.host_id) || !Object.hasOwn(value.projects, d.project_id)) {
|
|
57
|
+
throw new OpsError(`orphan or invalid deployment: ${id}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ids(value, label) {
|
|
64
|
+
if (!Array.isArray(value) || new Set(value).size !== value.length) throw new OpsError(`${label} must be an array of distinct IDs`);
|
|
65
|
+
return value.map((id) => identifier(id, label));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function route(registry, context) {
|
|
69
|
+
exact(context, new Set(["scope", "server_ids", "project_ids"]), new Set(["scope", "server_ids", "project_ids"]), "context");
|
|
70
|
+
if (!Object.hasOwn(WORKS, context.scope)) throw new OpsError("scope must be controller, access, server, project or inventory");
|
|
71
|
+
const servers = ids(context.server_ids, "server_ids");
|
|
72
|
+
const projects = ids(context.project_ids, "project_ids");
|
|
73
|
+
if (context.scope === "controller" && (servers.length || projects.length)) throw new OpsError("controller scope cannot target a server or project; use S/H/D");
|
|
74
|
+
if (["access", "server"].includes(context.scope) && (servers.length !== 1 || projects.length)) throw new OpsError("S/H require exactly one explicit server and no project");
|
|
75
|
+
if (context.scope === "project" && (!servers.length || projects.length !== 1)) throw new OpsError("D requires explicit server(s) and exactly one project; no last-used/cwd fallback");
|
|
76
|
+
for (const id of servers) if (context.scope !== "access" && !Object.hasOwn(registry.hosts, id)) throw new OpsError(`unknown server: ${id}; enroll via S first`);
|
|
77
|
+
for (const id of projects) if (!Object.hasOwn(registry.projects, id)) throw new OpsError(`unknown project: ${id}; explicitly register the project first`);
|
|
78
|
+
return { scope: context.scope, work: WORKS[context.scope], controller_id: registry.controller.controller_id,
|
|
79
|
+
server_ids: servers, project_ids: projects, host_ids: servers, registration_required: servers.filter((id) => !Object.hasOwn(registry.hosts, id)), note: "server maps to Host.host_id; controller is never an implicit local server" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function catalogUnlocked(state) {
|
|
83
|
+
noSymlinks(join(state, ".locks", "catalog"));
|
|
84
|
+
if (existsSync(join(state, ".locks", "catalog"))) throw new OpsError("catalog lock held; inspect the owner before generating a new snapshot");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function fileDigest(path) {
|
|
88
|
+
noSymlinks(path, { allowMissing: false });
|
|
89
|
+
if (!statSync(path).isFile() || statSync(path).size > MAX_JSON_BYTES) throw new OpsError("file exceeds snapshot bound");
|
|
90
|
+
return digest(readFileSync(path));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Pin identity and transport, not changeable host_services annotations. */
|
|
94
|
+
export function targetDigest(host) {
|
|
95
|
+
if (!host) throw new OpsError("missing target host");
|
|
96
|
+
const { host_id, root, identity, platform, transport, connection } = host;
|
|
97
|
+
const known_hosts_digest = transport === "ssh" ? fileDigest(connection.known_hosts) : null;
|
|
98
|
+
return digest({ host_id, root, identity, platform, transport, connection, known_hosts_digest });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function containedPath(root, path) {
|
|
102
|
+
if (typeof path !== "string" || !isAbsolute(path) || path.split(/[\\/]/).includes("..")) throw new OpsError("expected an absolute contained artifact path");
|
|
103
|
+
const rel = pathRelative(resolve(root), resolve(path));
|
|
104
|
+
if (!rel || rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) throw new OpsError("artifact path outside workspace");
|
|
105
|
+
noSymlinks(path, { allowMissing: false });
|
|
106
|
+
return resolve(path);
|
|
107
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Additive workspace commands. Legacy CLI semantics remain owned by cli.mjs. */
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { OpsError } from "./core.mjs";
|
|
4
|
+
import { readBoundedJson, readRegistry, route, statePath } from "./workspace.mjs";
|
|
5
|
+
import { checkServer } from "./server_checks.mjs";
|
|
6
|
+
import { generateFleet } from "./fleet.mjs";
|
|
7
|
+
import { authorizeTask, prepareTask, runTask } from "./tasks.mjs";
|
|
8
|
+
|
|
9
|
+
export const COMMANDS = Object.freeze(["workspace-help", "route", "server-check", "fleet", "task-plan", "task-authorize", "task-run"]);
|
|
10
|
+
const options = {
|
|
11
|
+
route: ["scope", "server", "project"],
|
|
12
|
+
"server-check": ["server", "profile", "min-free-mib", "min-memory-mib"],
|
|
13
|
+
fleet: ["stale-hours"], "task-plan": ["file"],
|
|
14
|
+
"task-authorize": ["task", "digest", "by", "statement", "ack-destructive"], "task-run": ["task"], "workspace-help": [],
|
|
15
|
+
};
|
|
16
|
+
export function workspaceCommand(argv) {
|
|
17
|
+
const rest = [...argv];
|
|
18
|
+
for (let i = rest.length - 1; i >= 0; i--) if (rest[i] === "--state") rest.splice(i, 2);
|
|
19
|
+
return COMMANDS.includes(rest[0]) ? rest[0] : null;
|
|
20
|
+
}
|
|
21
|
+
export function parseWorkspaceArgs(argv) {
|
|
22
|
+
const command = workspaceCommand(argv);
|
|
23
|
+
if (!command) throw new OpsError("unknown workspace command");
|
|
24
|
+
const args = [...argv], out = { command };
|
|
25
|
+
args.splice(args.indexOf(command), 1);
|
|
26
|
+
const allowed = new Set(["state", ...options[command]]);
|
|
27
|
+
while (args.length) {
|
|
28
|
+
const flag = args.shift();
|
|
29
|
+
if (!flag.startsWith("--") || !allowed.has(flag.slice(2))) throw new OpsError(`unknown ${command} argument: ${flag}`);
|
|
30
|
+
const key = flag.slice(2);
|
|
31
|
+
if (Object.hasOwn(out, key)) throw new OpsError(`duplicate argument: ${flag}`);
|
|
32
|
+
const value = args.shift();
|
|
33
|
+
if (!value || value.startsWith("--")) throw new OpsError(`${flag} needs a value`);
|
|
34
|
+
out[key] = value;
|
|
35
|
+
}
|
|
36
|
+
if (command !== "workspace-help" && !out.state) throw new OpsError("--state is required and must be absolute");
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
const required = (args, key) => { if (!args[key]) throw new OpsError(`--${key} is required`); return args[key]; };
|
|
40
|
+
const list = (v) => v ? v.split(",") : [];
|
|
41
|
+
function integer(value, fallback) {
|
|
42
|
+
if (value === undefined) return fallback;
|
|
43
|
+
if (!/^[1-9][0-9]*$/.test(value) || !Number.isSafeInteger(Number(value))) throw new OpsError("threshold must be a positive integer");
|
|
44
|
+
return Number(value);
|
|
45
|
+
}
|
|
46
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
47
|
+
try {
|
|
48
|
+
const args = parseWorkspaceArgs(argv);
|
|
49
|
+
if (args.command === "workspace-help") {
|
|
50
|
+
process.stdout.write(JSON.stringify({ commands: options, usage: "node ops.mjs --state ABSOLUTE_STATE COMMAND [options]",
|
|
51
|
+
workflow: "I controller; S server access; H server baseline; D project deployment; V local inventory",
|
|
52
|
+
task: "task-plan -> record existing explicit user authorization once via task-authorize -> task-run; no per-step reconfirmation; no automatic retry",
|
|
53
|
+
legacy: "All other commands retain the original plan/approve/apply contracts" }, null, 2) + "\n");
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
const state = statePath(args.state);
|
|
57
|
+
// Keep the existing complete schema/semantic validator authoritative in the installed runtime.
|
|
58
|
+
const { load } = await import("./model.mjs");
|
|
59
|
+
load(state);
|
|
60
|
+
let result;
|
|
61
|
+
switch (args.command) {
|
|
62
|
+
case "route": result = route(readRegistry(state), { scope: required(args, "scope"), server_ids: list(args.server), project_ids: list(args.project) }); break;
|
|
63
|
+
case "server-check": result = await checkServer(state, required(args, "server"), { profile: args.profile ?? "base",
|
|
64
|
+
minFreeMiB: integer(args["min-free-mib"], 1024), minMemoryMiB: integer(args["min-memory-mib"], 256) }); break;
|
|
65
|
+
case "fleet": result = generateFleet(state, { staleHours: integer(args["stale-hours"], 24) }); break;
|
|
66
|
+
case "task-plan": {
|
|
67
|
+
const file = required(args, "file");
|
|
68
|
+
if (!isAbsolute(file)) throw new OpsError("--file must be an absolute task request path");
|
|
69
|
+
result = await prepareTask(state, readBoundedJson(file)); break;
|
|
70
|
+
}
|
|
71
|
+
case "task-authorize": result = authorizeTask(state, required(args, "task"), required(args, "digest"), required(args, "by"), required(args, "statement"), args["ack-destructive"] ?? null); break;
|
|
72
|
+
case "task-run": result = await runTask(state, required(args, "task")); break;
|
|
73
|
+
}
|
|
74
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
75
|
+
return ["blocked", "failed", "partial", "unknown", "stale", "docs_pending", "interrupted"].includes(result.status) ? 2 : 0;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
process.stderr.write(JSON.stringify({ status: "blocked", error: error.message || String(error) }) + "\n");
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -5,10 +5,10 @@ import { dirname, join, resolve } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { validateStatus } from "./opslib/model.mjs";
|
|
8
|
+
import { WORKS } from "./opslib/workspace.mjs";
|
|
8
9
|
|
|
9
10
|
const tools = dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const root = resolve(tools, "../..");
|
|
11
|
-
const expected = ["D-project-deploy", "H-host-manage", "I-initialize"];
|
|
12
12
|
const args = process.argv.slice(2);
|
|
13
13
|
|
|
14
14
|
try {
|
|
@@ -16,12 +16,17 @@ try {
|
|
|
16
16
|
const works = readdirSync(root, { withFileTypes: true })
|
|
17
17
|
.filter((x) => x.isDirectory() && !["common", "_state", "__pycache__"].includes(x.name))
|
|
18
18
|
.map((x) => x.name).sort();
|
|
19
|
-
|
|
19
|
+
const expected = Object.values(WORKS).sort();
|
|
20
|
+
if (JSON.stringify(works) !== JSON.stringify(expected)) throw new Error("OPS Work directories differ from explicit scope router");
|
|
21
|
+
const manifest = JSON.parse(readFileSync(join(root, "manifest.json"), "utf8"));
|
|
22
|
+
const stages = manifest.stages.map((s) => s.id).sort();
|
|
23
|
+
if (new Set(stages).size !== stages.length || JSON.stringify(stages) !== JSON.stringify(works.map((w) => w.slice(2)).sort())) throw new Error("manifest stages differ from Work entries");
|
|
24
|
+
for (const stage of manifest.stages) for (const id of stage.after) if (!stages.includes(id)) throw new Error("unknown manifest prerequisite " + id);
|
|
20
25
|
for (const id of works) {
|
|
21
26
|
const text = readFileSync(join(root, id, id + ".md"), "utf8");
|
|
22
|
-
if (!text.includes("type: workflow-entry") || !text.includes("读取范围")) throw new Error("invalid work " + id);
|
|
27
|
+
if (!text.includes("type: workflow-entry") || !text.includes("读取范围") || !text.includes("id: ops/" + id.slice(2))) throw new Error("invalid work " + id);
|
|
23
28
|
}
|
|
24
|
-
for (const file of ["INDEX.md", "README.md", "manifest.json", "runtime-contract.json", "common/USAGE.md", "common/CAPABILITIES.md", "common/tools/opslib/execution.mjs"]) {
|
|
29
|
+
for (const file of ["INDEX.md", "README.md", "manifest.json", "runtime-contract.json", "common/USAGE.md", "common/WORKSPACE-USAGE.md", "common/CAPABILITIES.md", "common/templates/FLEET.html", "common/schemas/fleet-view.schema.json", "common/schemas/task-request.schema.json", "common/schemas/server-check.schema.json", "common/tools/opslib/execution.mjs", "common/tools/opslib/tasks.mjs", "common/tools/opslib/fleet.mjs"]) {
|
|
25
30
|
if (!existsSync(join(root, file))) throw new Error("missing " + file);
|
|
26
31
|
}
|
|
27
32
|
for (const name of readdirSync(join(root, "common/schemas")).filter((x) => x.endsWith(".json"))) {
|
|
@@ -30,10 +35,8 @@ try {
|
|
|
30
35
|
JSON.parse(readFileSync(join(root, "common/toolchains/volta-linux.json"), "utf8"));
|
|
31
36
|
validateStatus(JSON.parse(readFileSync(join(root, "_state/status.json"), "utf8")));
|
|
32
37
|
console.log("resource schema valid");
|
|
33
|
-
console.log(JSON.stringify({
|
|
34
|
-
|
|
35
|
-
scope: "static and empty-resource-schema check; not live target acceptance",
|
|
36
|
-
}, null, 2));
|
|
38
|
+
console.log(JSON.stringify({ valid: true, version: manifest.version, workers: works, schema_version: 3,
|
|
39
|
+
scope: "static and empty-resource-schema check; not live target acceptance" }, null, 2));
|
|
37
40
|
} else if (args.length === 2 && ["--state-root", "--state"].includes(args[0])) {
|
|
38
41
|
const p = spawnSync(process.execPath, [join(tools, "ops.mjs"), "--state", resolve(args[1]), "validate"], { encoding: "utf8", timeout: 30000 });
|
|
39
42
|
if (p.status !== 0) throw new Error(p.stdout + p.stderr);
|
|
@@ -22,31 +22,50 @@
|
|
|
22
22
|
"risk": "local-reversible",
|
|
23
23
|
"context_budget": 16000
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
"id": "server-connect",
|
|
27
|
+
"after": [
|
|
28
|
+
"initialize"
|
|
29
|
+
],
|
|
30
|
+
"inputs": [
|
|
31
|
+
"server",
|
|
32
|
+
"verified-host-key",
|
|
33
|
+
"key-or-agent"
|
|
34
|
+
],
|
|
35
|
+
"outputs": [
|
|
36
|
+
"server-registration",
|
|
37
|
+
"inventory",
|
|
38
|
+
"server-checks"
|
|
39
|
+
],
|
|
40
|
+
"risk": "production-critical",
|
|
41
|
+
"context_budget": 20000
|
|
42
|
+
},
|
|
25
43
|
{
|
|
26
44
|
"id": "host-manage",
|
|
27
45
|
"after": [],
|
|
28
46
|
"inputs": [
|
|
29
|
-
"
|
|
47
|
+
"server",
|
|
30
48
|
"spec",
|
|
31
|
-
"approval"
|
|
49
|
+
"task-authorization-or-plan-approval"
|
|
32
50
|
],
|
|
33
51
|
"outputs": [
|
|
34
52
|
"host-run",
|
|
35
53
|
"verification",
|
|
36
|
-
"dual-documents"
|
|
54
|
+
"dual-documents",
|
|
55
|
+
"server-checks"
|
|
37
56
|
],
|
|
38
57
|
"risk": "production-critical",
|
|
39
|
-
"context_budget":
|
|
58
|
+
"context_budget": 24000
|
|
40
59
|
},
|
|
41
60
|
{
|
|
42
61
|
"id": "project-deploy",
|
|
43
62
|
"after": [],
|
|
44
63
|
"inputs": [
|
|
45
64
|
"project",
|
|
46
|
-
"
|
|
65
|
+
"explicit-servers",
|
|
47
66
|
"bindings",
|
|
48
67
|
"spec",
|
|
49
|
-
"approval"
|
|
68
|
+
"task-authorization-or-plan-approval"
|
|
50
69
|
],
|
|
51
70
|
"outputs": [
|
|
52
71
|
"release",
|
|
@@ -54,7 +73,24 @@
|
|
|
54
73
|
"dual-documents"
|
|
55
74
|
],
|
|
56
75
|
"risk": "production-critical",
|
|
57
|
-
"context_budget":
|
|
76
|
+
"context_budget": 30000
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "inventory-view",
|
|
80
|
+
"after": [],
|
|
81
|
+
"inputs": [
|
|
82
|
+
"resource-state",
|
|
83
|
+
"server-checks",
|
|
84
|
+
"task-summaries"
|
|
85
|
+
],
|
|
86
|
+
"outputs": [
|
|
87
|
+
"fleet-markdown",
|
|
88
|
+
"fleet-json",
|
|
89
|
+
"fleet-html",
|
|
90
|
+
"view-receipt"
|
|
91
|
+
],
|
|
92
|
+
"risk": "local-reversible",
|
|
93
|
+
"context_budget": 16000
|
|
58
94
|
}
|
|
59
95
|
]
|
|
60
96
|
}
|