[taskboard] add dedicated truenas admin agent
This commit is contained in:
@@ -80,10 +80,10 @@
|
|||||||
],
|
],
|
||||||
"configuredAgents": [
|
"configuredAgents": [
|
||||||
"pve-direct",
|
"pve-direct",
|
||||||
"truenas-direct",
|
"truenas-admin",
|
||||||
"panda-direct"
|
"panda-direct"
|
||||||
],
|
],
|
||||||
"diagram": "taskboard direct SSH\n -> pve : built-in Proxmox overview\n -> truenas : built-in storage overview\n -> panda : built-in SSH add-on overview\n\nEach direct task\n -> ssh safe built-in command\n -> capture stdout/stderr\n -> task callback -> completed result\n",
|
"diagram": "taskboard direct SSH\n -> pve : built-in Proxmox overview\n -> truenas : dedicated truenas-admin audit actions\n -> panda : built-in SSH add-on overview\n\nEach direct task\n -> ssh safe built-in command or host-specific builtin audit\n -> capture stdout/stderr plus repo dependency correlation\n -> task callback -> completed/review result\n",
|
||||||
"notes": [
|
"notes": [
|
||||||
"Direct targets are for safe built-in actions, not arbitrary remote shell execution from the UI.",
|
"Direct targets are for safe built-in actions, not arbitrary remote shell execution from the UI.",
|
||||||
"Completion state is written through the same callback pipeline used by remote agent runtimes."
|
"Completion state is written through the same callback pipeline used by remote agent runtimes."
|
||||||
@@ -188,37 +188,45 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "truenas-direct",
|
"slug": "truenas-admin",
|
||||||
"assignmentKey": "truenas-direct",
|
"assignmentKey": "truenas-admin",
|
||||||
"aliases": ["truenas-direct", "TrueNAS Direct", "truenas"],
|
"aliases": ["truenas-admin", "truenas-direct", "TrueNAS Admin", "TrueNAS Direct", "truenas"],
|
||||||
"name": "TrueNAS Direct",
|
"name": "TrueNAS Admin",
|
||||||
"host": "truenas",
|
"host": "truenas",
|
||||||
"role": "Direct storage checks over SSH",
|
"role": "Dedicated storage and dataset audit agent for the TrueNAS host",
|
||||||
"runtimePath": "ssh://christopher@192.168.50.12:22",
|
"runtimePath": "ssh://christopher@192.168.50.12:22",
|
||||||
"configPath": null,
|
"configPath": null,
|
||||||
"emoji": "T",
|
"emoji": "T",
|
||||||
"channels": [
|
"channels": [
|
||||||
{ "label": "SSH", "value": "christopher@192.168.50.12:22" },
|
{ "label": "SSH", "value": "christopher@192.168.50.12:22" },
|
||||||
{ "label": "Actions", "value": "storage-overview" }
|
{ "label": "Actions", "value": "dataset-audit, storage-overview" }
|
||||||
],
|
],
|
||||||
"tools": ["ssh", "zfs", "systemctl", "midclt"],
|
"tools": ["ssh", "zfs", "systemctl", "midclt"],
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
|
"Build dataset dependency matrices from live storage plus repo configuration",
|
||||||
"Verify storage datasets",
|
"Verify storage datasets",
|
||||||
"Check docker service state",
|
"Check docker service state",
|
||||||
"Report host identity and storage status"
|
"Report host identity and storage status"
|
||||||
],
|
],
|
||||||
"files": [],
|
"files": [],
|
||||||
"notes": [
|
"notes": [
|
||||||
"Runs safe read-only storage checks.",
|
"Runs safe read-only storage and dependency audit checks.",
|
||||||
"Does not modify datasets or apps."
|
"Does not delete datasets directly; cleanup stays review-gated."
|
||||||
],
|
],
|
||||||
"dispatch": {
|
"dispatch": {
|
||||||
"method": "direct-ssh",
|
"method": "direct-ssh",
|
||||||
"hostname": "192.168.50.12",
|
"hostname": "192.168.50.12",
|
||||||
"user": "christopher",
|
"user": "christopher",
|
||||||
"port": 22,
|
"port": 22,
|
||||||
"defaultAction": "storage-overview",
|
"defaultAction": "dataset-audit",
|
||||||
"actions": [
|
"actions": [
|
||||||
|
{
|
||||||
|
"key": "dataset-audit",
|
||||||
|
"title": "Dataset dependency audit",
|
||||||
|
"description": "Correlate live ZFS datasets with active homelab dependency signals and produce cleanup review candidates.",
|
||||||
|
"command": "builtin:truenas-dataset-audit",
|
||||||
|
"successSummary": "TrueNAS dataset dependency audit completed"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "storage-overview",
|
"key": "storage-overview",
|
||||||
"title": "Storage overview",
|
"title": "Storage overview",
|
||||||
|
|||||||
@@ -71,10 +71,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "direct-truenas-check",
|
"key": "direct-truenas-check",
|
||||||
"title": "TrueNAS direct verification",
|
"title": "TrueNAS dataset audit",
|
||||||
"summary": "Run the built-in storage overview action through the direct SSH target.",
|
"summary": "Run the dedicated TrueNAS dataset dependency audit through the host-specific admin agent.",
|
||||||
"family": "direct",
|
"family": "direct",
|
||||||
"tags": ["host-ops", "storage-check", "action:storage-overview"],
|
"tags": ["host-ops", "storage-check", "dataset-audit", "action:dataset-audit"],
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"priority": "High",
|
"priority": "High",
|
||||||
"dispatchMethod": "direct-ssh",
|
"dispatchMethod": "direct-ssh",
|
||||||
|
|||||||
@@ -37,13 +37,14 @@
|
|||||||
- typed direct target definitions in `config/fleet.json`
|
- typed direct target definitions in `config/fleet.json`
|
||||||
- safe built-in host actions for `pve`, `truenas`, and `panda`
|
- safe built-in host actions for `pve`, `truenas`, and `panda`
|
||||||
- completion written through the callback pipeline
|
- completion written through the callback pipeline
|
||||||
|
- dedicated `truenas-admin` audit action that correlates live ZFS datasets with repo dependency signals
|
||||||
|
|
||||||
## Verified Live
|
## Verified Live
|
||||||
|
|
||||||
- `grizzley` ZeroClaw webhook dispatch from taskboard
|
- `grizzley` ZeroClaw webhook dispatch from taskboard
|
||||||
- `ice` ZeroClaw webhook dispatch from taskboard
|
- `ice` ZeroClaw webhook dispatch from taskboard
|
||||||
- OpenClaw swarm queue creation and host worktree creation on `ubuntu`
|
- OpenClaw swarm queue creation and host worktree creation on `ubuntu`
|
||||||
- direct SSH host actions can now be dispatched for `pve`, `truenas`, and `panda`
|
- direct SSH host actions can now be dispatched for `pve`, `truenas-admin`, and `panda`
|
||||||
|
|
||||||
## Current Limits
|
## Current Limits
|
||||||
|
|
||||||
@@ -51,3 +52,4 @@
|
|||||||
- ZeroClaw acknowledgements and completions are still operator-driven; remote runtimes do not push completion state back yet.
|
- ZeroClaw acknowledgements and completions are still operator-driven; remote runtimes do not push completion state back yet.
|
||||||
- The board records remote webhook responses, but not structured per-step execution output from the agents.
|
- The board records remote webhook responses, but not structured per-step execution output from the agents.
|
||||||
- Direct targets are intentionally restricted to configured safe actions and do not expose arbitrary shell execution in the UI.
|
- Direct targets are intentionally restricted to configured safe actions and do not expose arbitrary shell execution in the UI.
|
||||||
|
- `truenas-admin` remains review-gated for cleanup decisions; the taskboard produces audit results and candidate datasets, not direct deletion.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
3. Expand direct host operations beyond the first safe action set.
|
3. Expand direct host operations beyond the first safe action set.
|
||||||
- add more read-only Proxmox actions
|
- add more read-only Proxmox actions
|
||||||
- add richer TrueNAS storage and service checks
|
- add richer TrueNAS storage, share, and snapshot checks beyond the dataset audit
|
||||||
- add more Home Assistant supervisor and add-on checks
|
- add more Home Assistant supervisor and add-on checks
|
||||||
|
|
||||||
4. Add operator controls for swarm execution.
|
4. Add operator controls for swarm execution.
|
||||||
|
|||||||
371
lib/dispatch.ts
371
lib/dispatch.ts
@@ -79,6 +79,346 @@ function truncateOutput(output: string, maxLength = 4000) {
|
|||||||
return `${trimmed.slice(0, maxLength - 15)}\n...[truncated]`;
|
return `${trimmed.slice(0, maxLength - 15)}\n...[truncated]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TrueNasDataset = {
|
||||||
|
name: string;
|
||||||
|
used: string;
|
||||||
|
avail: string;
|
||||||
|
mountpoint: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DatasetSignal = {
|
||||||
|
source: string;
|
||||||
|
dataset: string;
|
||||||
|
signalType: "active" | "legacy";
|
||||||
|
matchedText: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TRUENAS_SIGNAL_PATTERNS: Array<{
|
||||||
|
dataset: string;
|
||||||
|
patterns: string[];
|
||||||
|
signalType: "active" | "legacy";
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/NetworkMediaShare",
|
||||||
|
patterns: [
|
||||||
|
"/mnt/truenas/mediadata",
|
||||||
|
"/mnt/TrueNAS/NetworkMediaShare",
|
||||||
|
"/mnt/TrueNAS/NetworkMediaShare/mediadata",
|
||||||
|
],
|
||||||
|
signalType: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "RPiPool/PersonalMediaLibrary",
|
||||||
|
patterns: [
|
||||||
|
"/mnt/PersonalMediaLibrary",
|
||||||
|
"/mnt/RPiPool/PersonalMediaLibrary",
|
||||||
|
],
|
||||||
|
signalType: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/backups",
|
||||||
|
patterns: [
|
||||||
|
"/mnt/truenas-backup",
|
||||||
|
"/mnt/TrueNAS/backups",
|
||||||
|
],
|
||||||
|
signalType: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/container-config",
|
||||||
|
patterns: ["/mnt/TrueNAS/container-config"],
|
||||||
|
signalType: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/databases",
|
||||||
|
patterns: ["/mnt/TrueNAS/databases"],
|
||||||
|
signalType: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/homelab/databases",
|
||||||
|
patterns: ["/mnt/TrueNAS/homelab/databases"],
|
||||||
|
signalType: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/homelab/hosts/ubuntu/docker-data",
|
||||||
|
patterns: ["/mnt/TrueNAS/homelab/hosts/ubuntu/docker-data"],
|
||||||
|
signalType: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/homelab/hosts/grizzley/docker-data",
|
||||||
|
patterns: ["/mnt/TrueNAS/homelab/hosts/grizzley/docker-data"],
|
||||||
|
signalType: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/homelab/hosts/ice/docker-data",
|
||||||
|
patterns: ["/mnt/TrueNAS/homelab/hosts/ice/docker-data"],
|
||||||
|
signalType: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/RPiPool-backup",
|
||||||
|
patterns: ["/mnt/TrueNAS/RPiPool-backup"],
|
||||||
|
signalType: "legacy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/PersonalMediaLibraryBackup",
|
||||||
|
patterns: ["/mnt/TrueNAS/PersonalMediaLibraryBackup"],
|
||||||
|
signalType: "legacy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/TimeMachine",
|
||||||
|
patterns: ["/mnt/TrueNAS/TimeMachine", "TimeMachine"],
|
||||||
|
signalType: "legacy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/UserShares",
|
||||||
|
patterns: ["/mnt/TrueNAS/UserShares", "UserShares"],
|
||||||
|
signalType: "legacy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/UserShares/RedVelvet",
|
||||||
|
patterns: ["/mnt/TrueNAS/UserShares/RedVelvet", "TrueNAS/UserShares/RedVelvet"],
|
||||||
|
signalType: "legacy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/UserShares/Vanilla",
|
||||||
|
patterns: ["/mnt/TrueNAS/UserShares/Vanilla", "TrueNAS/UserShares/Vanilla"],
|
||||||
|
signalType: "legacy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataset: "TrueNAS/traefik-certs",
|
||||||
|
patterns: ["/mnt/truenas/traefik-certs", "/mnt/TrueNAS/traefik-certs"],
|
||||||
|
signalType: "active",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ACTIVE_TRUENAS_SCAN_PATHS = [
|
||||||
|
"homelab/ubuntu",
|
||||||
|
"homelab/grizzley",
|
||||||
|
"homelab/truenas/AGENTS.md",
|
||||||
|
"homelab/AGENTS.md",
|
||||||
|
"homelab/catalog",
|
||||||
|
"ansible/playbooks",
|
||||||
|
];
|
||||||
|
|
||||||
|
const LEGACY_TRUENAS_SCAN_PATHS = [
|
||||||
|
"homelab/inventory/truenas.json",
|
||||||
|
"homelab/proxmox/truenas",
|
||||||
|
"obsidian-vault/homelab",
|
||||||
|
];
|
||||||
|
|
||||||
|
function sshConnectionArgs(host: string, user: string, port: number) {
|
||||||
|
return [
|
||||||
|
"-F",
|
||||||
|
"/dev/null",
|
||||||
|
"-o",
|
||||||
|
"BatchMode=yes",
|
||||||
|
"-o",
|
||||||
|
"StrictHostKeyChecking=accept-new",
|
||||||
|
"-o",
|
||||||
|
"ConnectTimeout=15",
|
||||||
|
"-o",
|
||||||
|
"IdentitiesOnly=yes",
|
||||||
|
"-o",
|
||||||
|
"UserKnownHostsFile=/tmp/taskboard_known_hosts",
|
||||||
|
"-i",
|
||||||
|
DIRECT_SSH_KEY_PATH,
|
||||||
|
"-p",
|
||||||
|
String(port),
|
||||||
|
`${user}@${host}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function existingHomelabRoots() {
|
||||||
|
const candidates = [
|
||||||
|
process.env.HOMELAB_REPO_ROOT,
|
||||||
|
"/home/bear/homelabagentroot",
|
||||||
|
"/home/christopher/opencode-home",
|
||||||
|
].filter((entry): entry is string => Boolean(entry));
|
||||||
|
|
||||||
|
return [...new Set(candidates)].filter((candidate) => fs.existsSync(candidate));
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectTextFiles(targetPath: string, collected: string[]) {
|
||||||
|
if (!fs.existsSync(targetPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = fs.statSync(targetPath);
|
||||||
|
if (stat.isFile()) {
|
||||||
|
if (stat.size <= 1024 * 1024) {
|
||||||
|
collected.push(targetPath);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of fs.readdirSync(targetPath, { withFileTypes: true })) {
|
||||||
|
if (entry.name.startsWith(".git") || entry.name === "node_modules" || entry.name === "code-server-ai") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
collectTextFiles(path.join(targetPath, entry.name), collected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanTrueNasSignals(repoRoot: string, relativePaths: string[], signalType: "active" | "legacy") {
|
||||||
|
const files: string[] = [];
|
||||||
|
for (const relativePath of relativePaths) {
|
||||||
|
collectTextFiles(path.join(repoRoot, relativePath), files);
|
||||||
|
}
|
||||||
|
|
||||||
|
const signals: DatasetSignal[] = [];
|
||||||
|
for (const filePath of files) {
|
||||||
|
let content = "";
|
||||||
|
try {
|
||||||
|
content = fs.readFileSync(filePath, "utf8");
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mapping of TRUENAS_SIGNAL_PATTERNS.filter((entry) => entry.signalType === signalType)) {
|
||||||
|
const matchedPattern = mapping.patterns.find((pattern) => content.includes(pattern));
|
||||||
|
if (matchedPattern) {
|
||||||
|
signals.push({
|
||||||
|
source: filePath,
|
||||||
|
dataset: mapping.dataset,
|
||||||
|
signalType,
|
||||||
|
matchedText: matchedPattern,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return signals;
|
||||||
|
}
|
||||||
|
|
||||||
|
function datasetHierarchyName(datasetName: string) {
|
||||||
|
return datasetName.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeSignals(signals: DatasetSignal[], dataset: string) {
|
||||||
|
return signals.filter((signal) => signal.dataset === dataset);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTrueNasDatasetAudit(taskId: number, host: string, user: string, port: number): Promise<DispatchResult> {
|
||||||
|
const task = await findTask(taskId);
|
||||||
|
if (!task) {
|
||||||
|
throw new Error("task_not_found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const sshArgs = sshConnectionArgs(host, user, port);
|
||||||
|
const { stdout } = await execFileAsync(
|
||||||
|
"ssh",
|
||||||
|
[...sshArgs, "zfs list -H -o name,used,avail,mountpoint"],
|
||||||
|
{
|
||||||
|
timeout: DIRECT_SSH_TIMEOUT_MS,
|
||||||
|
maxBuffer: 1024 * 1024,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const datasets = stdout
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => {
|
||||||
|
const [name, used, avail, mountpoint] = line.split("\t");
|
||||||
|
return { name, used, avail, mountpoint } satisfies TrueNasDataset;
|
||||||
|
});
|
||||||
|
|
||||||
|
const repoRoots = existingHomelabRoots();
|
||||||
|
const activeSignals = repoRoots.flatMap((repoRoot) =>
|
||||||
|
scanTrueNasSignals(repoRoot, ACTIVE_TRUENAS_SCAN_PATHS, "active"),
|
||||||
|
);
|
||||||
|
const legacySignals = repoRoots.flatMap((repoRoot) =>
|
||||||
|
scanTrueNasSignals(repoRoot, LEGACY_TRUENAS_SCAN_PATHS, "legacy"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const childActiveMap = new Map<string, number>();
|
||||||
|
for (const dataset of datasets) {
|
||||||
|
const parentNames = dataset.name.split("/").map((_, index, parts) => parts.slice(0, index + 1).join("/"));
|
||||||
|
for (const parentName of parentNames.slice(0, -1)) {
|
||||||
|
childActiveMap.set(parentName, (childActiveMap.get(parentName) || 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeDatasets = datasets
|
||||||
|
.map((dataset) => ({
|
||||||
|
dataset,
|
||||||
|
activeRefs: summarizeSignals(activeSignals, dataset.name),
|
||||||
|
legacyRefs: summarizeSignals(legacySignals, dataset.name),
|
||||||
|
}))
|
||||||
|
.filter(({ activeRefs }) => activeRefs.length > 0);
|
||||||
|
|
||||||
|
const reviewCandidates = datasets
|
||||||
|
.map((dataset) => ({
|
||||||
|
dataset,
|
||||||
|
activeRefs: summarizeSignals(activeSignals, dataset.name),
|
||||||
|
legacyRefs: summarizeSignals(legacySignals, dataset.name),
|
||||||
|
hasActiveChild:
|
||||||
|
datasets.some(
|
||||||
|
(candidate) =>
|
||||||
|
candidate.name !== dataset.name &&
|
||||||
|
datasetHierarchyName(candidate.name).startsWith(`${datasetHierarchyName(dataset.name)}/`) &&
|
||||||
|
summarizeSignals(activeSignals, candidate.name).length > 0,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter(({ dataset, activeRefs, hasActiveChild }) => {
|
||||||
|
if (dataset.name === "TrueNAS" || dataset.name === "RPiPool" || dataset.name.includes("/.system")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return activeRefs.length === 0 && !hasActiveChild;
|
||||||
|
});
|
||||||
|
|
||||||
|
const detailSections = [
|
||||||
|
`Task: #${task.id} ${task.title}`,
|
||||||
|
"",
|
||||||
|
"Active dependency signals",
|
||||||
|
...(
|
||||||
|
activeDatasets.length > 0
|
||||||
|
? activeDatasets.map(({ dataset, activeRefs }) =>
|
||||||
|
`- ${dataset.name} (${dataset.used}, mount ${dataset.mountpoint}) <- ${activeRefs
|
||||||
|
.slice(0, 4)
|
||||||
|
.map((ref) => path.relative(repoRoots[0] || process.cwd(), ref.source))
|
||||||
|
.join(", ")}`,
|
||||||
|
)
|
||||||
|
: ["- none detected"]
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
"Review candidates with no active dependency signal",
|
||||||
|
...(
|
||||||
|
reviewCandidates.length > 0
|
||||||
|
? reviewCandidates.map(({ dataset, legacyRefs }) =>
|
||||||
|
`- ${dataset.name} (${dataset.used}, mount ${dataset.mountpoint})${
|
||||||
|
legacyRefs.length > 0 ? ` [legacy refs: ${legacyRefs.length}]` : ""
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
: ["- none"]
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
"Legacy-only references",
|
||||||
|
...(
|
||||||
|
legacySignals.length > 0
|
||||||
|
? legacySignals.map((signal) => `- ${signal.dataset} <- ${path.relative(repoRoots[0] || process.cwd(), signal.source)}`)
|
||||||
|
: ["- none"]
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
"Live datasets",
|
||||||
|
...datasets.map((dataset) => `- ${dataset.name} | used ${dataset.used} | mount ${dataset.mountpoint}`),
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: "completed" as const,
|
||||||
|
summary: "TrueNAS dataset dependency audit completed",
|
||||||
|
detail: truncateOutput(detailSections.join("\n"), 12000),
|
||||||
|
callback: {
|
||||||
|
status: "Review",
|
||||||
|
dispatch_state: "completed",
|
||||||
|
summary: "TrueNAS dataset dependency audit completed",
|
||||||
|
detail: truncateOutput(detailSections.join("\n"), 12000),
|
||||||
|
completed_by: "direct-ssh:truenas-audit",
|
||||||
|
last_error: null,
|
||||||
|
last_dispatch_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function dispatchOpenClawTask(taskId: number): Promise<DispatchResult> {
|
async function dispatchOpenClawTask(taskId: number): Promise<DispatchResult> {
|
||||||
const task = await findTask(taskId);
|
const task = await findTask(taskId);
|
||||||
if (!task) {
|
if (!task) {
|
||||||
@@ -236,24 +576,21 @@ async function dispatchDirectTask(taskId: number): Promise<DispatchResult> {
|
|||||||
throw new Error(`unsupported_direct_action:${actionKey}`);
|
throw new Error(`unsupported_direct_action:${actionKey}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.command === "builtin:truenas-dataset-audit") {
|
||||||
|
return runTrueNasDatasetAudit(
|
||||||
|
taskId,
|
||||||
|
directAgent.dispatch.hostname,
|
||||||
|
directAgent.dispatch.user,
|
||||||
|
directAgent.dispatch.port,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const sshArgs = [
|
const sshArgs = [
|
||||||
"-F",
|
...sshConnectionArgs(
|
||||||
"/dev/null",
|
directAgent.dispatch.hostname,
|
||||||
"-o",
|
directAgent.dispatch.user,
|
||||||
"BatchMode=yes",
|
directAgent.dispatch.port,
|
||||||
"-o",
|
),
|
||||||
"StrictHostKeyChecking=accept-new",
|
|
||||||
"-o",
|
|
||||||
"ConnectTimeout=15",
|
|
||||||
"-o",
|
|
||||||
"IdentitiesOnly=yes",
|
|
||||||
"-o",
|
|
||||||
"UserKnownHostsFile=/tmp/taskboard_known_hosts",
|
|
||||||
"-i",
|
|
||||||
DIRECT_SSH_KEY_PATH,
|
|
||||||
"-p",
|
|
||||||
String(directAgent.dispatch.port),
|
|
||||||
`${directAgent.dispatch.user}@${directAgent.dispatch.hostname}`,
|
|
||||||
action.command,
|
action.command,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user