@hasna/machines 0.0.34 → 0.0.36
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 +33 -0
- package/dist/cli/index.js +358 -25
- package/dist/commands/doctor.d.ts +23 -2
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/consumer.js +188 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +370 -25
- package/dist/manifests.d.ts +22 -1
- package/dist/manifests.d.ts.map +1 -1
- package/dist/mcp/index.js +351 -20
- package/dist/redaction.d.ts +11 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/topology.d.ts.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/consumer-conformance.mjs +3 -2
package/README.md
CHANGED
|
@@ -36,9 +36,18 @@ machines manifest validate
|
|
|
36
36
|
machines manifest list
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
Public packages should keep private fleet state behind an opaque source/ref
|
|
40
|
+
boundary. `HASNA_MACHINES_PRIVATE_MANIFEST_REF` (or
|
|
41
|
+
`MACHINES_PRIVATE_MANIFEST_REF`) may point at a private backend, but
|
|
42
|
+
open-machines only reports the redacted ref and falls back to the local
|
|
43
|
+
`machines.json` unless a caller supplies a manifest adapter. The adapter
|
|
44
|
+
contract is backend-agnostic and lives in the package root exports; it does not
|
|
45
|
+
pull in secrets managers, storage SDKs, or org-specific fleet internals.
|
|
46
|
+
|
|
39
47
|
## Provision and reconcile
|
|
40
48
|
|
|
41
49
|
```bash
|
|
50
|
+
machines setup --machine linux-dev-01
|
|
42
51
|
machines setup --machine linux-dev-01 --json
|
|
43
52
|
machines setup --machine linux-dev-01 --apply --yes
|
|
44
53
|
machines sync --machine linux-dev-01 --json
|
|
@@ -47,6 +56,25 @@ machines doctor --machine linux-dev-01
|
|
|
47
56
|
machines self-test
|
|
48
57
|
```
|
|
49
58
|
|
|
59
|
+
`machines setup` is a dry-run plan by default. The generated playbook favors
|
|
60
|
+
idempotent operations (`mkdir -p`, command-existence guards, package-manager
|
|
61
|
+
installs) and only executes when both `--apply` and `--yes` are provided.
|
|
62
|
+
The default plan also adds update-check/download settings without enabling
|
|
63
|
+
automatic OS installation: Linux uses apt periodic download-only settings, and
|
|
64
|
+
macOS uses `softwareupdate`/`defaults` with `AutomaticallyInstallMacOSUpdates`
|
|
65
|
+
left disabled.
|
|
66
|
+
`doctor --json` includes public-safe source/ref diagnostics plus optional
|
|
67
|
+
adapter hook results for secrets, configs, monitors, repos, MCPs, and shield
|
|
68
|
+
checks. When no adapter is configured, those checks report a skipped fallback
|
|
69
|
+
instead of importing private dependencies.
|
|
70
|
+
It also reports noninteractive sudo readiness, SSH certificate support, and
|
|
71
|
+
GitHub App secret-reference readiness without printing credentials or private
|
|
72
|
+
keys.
|
|
73
|
+
|
|
74
|
+
Apple device management belongs in the private deployment layer. The public
|
|
75
|
+
setup plan can report enrollment status with `profiles status -type enrollment`,
|
|
76
|
+
but it does not enroll devices, install profiles, or publish team identifiers.
|
|
77
|
+
|
|
50
78
|
## Topology SDK
|
|
51
79
|
|
|
52
80
|
`@hasna/machines` exposes a compact consumer SDK for other open-core packages
|
|
@@ -151,6 +179,11 @@ machines defaults to
|
|
|
151
179
|
`machines/screen-sharing/screen-<machine>-vnc-password`, or the namespace set in
|
|
152
180
|
`HASNA_MACHINES_SCREEN_SECRET_NAMESPACE`. The user comes from the manifest
|
|
153
181
|
(`metadata.user`) when present, or `--user`.
|
|
182
|
+
|
|
183
|
+
For GitHub automation, prefer GitHub App installation tokens over personal user
|
|
184
|
+
tokens. Public manifests and docs should store only opaque secret references
|
|
185
|
+
for the app id/private key material; private adapters or `open-secrets` should
|
|
186
|
+
resolve those references at runtime.
|
|
154
187
|
`screen-credentials` verifies the resolved user and secret key for a machine or
|
|
155
188
|
the full fleet without printing secret values.
|
|
156
189
|
|
package/dist/cli/index.js
CHANGED
|
@@ -7092,6 +7092,104 @@ var coerce = {
|
|
|
7092
7092
|
var NEVER = INVALID;
|
|
7093
7093
|
// src/manifests.ts
|
|
7094
7094
|
init_paths();
|
|
7095
|
+
|
|
7096
|
+
// src/redaction.ts
|
|
7097
|
+
var REDACTED_VALUE = "[redacted]";
|
|
7098
|
+
var SENSITIVE_KEY_PATTERN = /(password|passwd|token|credential|private[_-]?key|privateKey|api[_-]?key|github.*key|pem|secret)/i;
|
|
7099
|
+
var SECRET_REFERENCE_KEY_PATTERN = /(secret(ref(erence)?|key)?|secretRef|secretKey)$/i;
|
|
7100
|
+
var SENSITIVE_VALUE_PATTERNS = [
|
|
7101
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/,
|
|
7102
|
+
/\bghp_[A-Za-z0-9_]{20,}\b/,
|
|
7103
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/,
|
|
7104
|
+
/\bxox[baprs]-[A-Za-z0-9-]{20,}\b/,
|
|
7105
|
+
/\bAKIA[0-9A-Z]{16}\b/,
|
|
7106
|
+
/\bsk-[A-Za-z0-9_-]{20,}\b/
|
|
7107
|
+
];
|
|
7108
|
+
function isSensitiveKey(key) {
|
|
7109
|
+
return SENSITIVE_KEY_PATTERN.test(key);
|
|
7110
|
+
}
|
|
7111
|
+
function isSecretReferenceKey(key) {
|
|
7112
|
+
return SECRET_REFERENCE_KEY_PATTERN.test(key);
|
|
7113
|
+
}
|
|
7114
|
+
function looksSensitiveString(value) {
|
|
7115
|
+
return SENSITIVE_VALUE_PATTERNS.some((pattern) => pattern.test(value));
|
|
7116
|
+
}
|
|
7117
|
+
function isRecord(value) {
|
|
7118
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7119
|
+
}
|
|
7120
|
+
function redactPath(value) {
|
|
7121
|
+
return value.replace(/\/home\/[^/\s]+/g, "/home/<user>").replace(/\/Users\/[^/\s]+/g, "/Users/<user>").replace(/[A-Za-z]:\\Users\\[^\\\s]+/g, "C:\\Users\\<user>");
|
|
7122
|
+
}
|
|
7123
|
+
function redactPrivateRef(value) {
|
|
7124
|
+
const trimmed = value.trim();
|
|
7125
|
+
const scheme = trimmed.match(/^([a-z][a-z0-9+.-]*:\/\/)/i);
|
|
7126
|
+
if (scheme)
|
|
7127
|
+
return `${scheme[1]}<redacted>`;
|
|
7128
|
+
const colon = trimmed.match(/^([a-z][a-z0-9+.-]*):/i);
|
|
7129
|
+
if (colon)
|
|
7130
|
+
return `${colon[1]}:<redacted>`;
|
|
7131
|
+
return "<private-manifest-ref:redacted>";
|
|
7132
|
+
}
|
|
7133
|
+
function redactIdentifier(value) {
|
|
7134
|
+
return value.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 80) || "adapter";
|
|
7135
|
+
}
|
|
7136
|
+
function redactSensitiveValue(value, key = "") {
|
|
7137
|
+
if (typeof value === "string") {
|
|
7138
|
+
if (isSensitiveKey(key) && !(isSecretReferenceKey(key) && !looksSensitiveString(value))) {
|
|
7139
|
+
return REDACTED_VALUE;
|
|
7140
|
+
}
|
|
7141
|
+
if (looksSensitiveString(value))
|
|
7142
|
+
return REDACTED_VALUE;
|
|
7143
|
+
return redactPath(value);
|
|
7144
|
+
}
|
|
7145
|
+
if (Array.isArray(value)) {
|
|
7146
|
+
return value.map((entry) => redactSensitiveValue(entry, key));
|
|
7147
|
+
}
|
|
7148
|
+
if (isRecord(value)) {
|
|
7149
|
+
const redacted = {};
|
|
7150
|
+
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
7151
|
+
redacted[entryKey] = redactSensitiveValue(entryValue, entryKey);
|
|
7152
|
+
}
|
|
7153
|
+
return redacted;
|
|
7154
|
+
}
|
|
7155
|
+
return value;
|
|
7156
|
+
}
|
|
7157
|
+
function publicMetadataKeys(metadata) {
|
|
7158
|
+
return Object.keys(metadata ?? {}).filter((key) => !isSensitiveKey(key)).sort();
|
|
7159
|
+
}
|
|
7160
|
+
function redactMetadata(metadata) {
|
|
7161
|
+
return redactSensitiveValue(metadata ?? {});
|
|
7162
|
+
}
|
|
7163
|
+
function redactManifestForDiagnostics(machine) {
|
|
7164
|
+
const metadata = redactMetadata(machine.metadata);
|
|
7165
|
+
for (const key of ["user", "username", "login"]) {
|
|
7166
|
+
if (typeof metadata[key] === "string")
|
|
7167
|
+
metadata[key] = REDACTED_VALUE;
|
|
7168
|
+
}
|
|
7169
|
+
return {
|
|
7170
|
+
id: machine.id,
|
|
7171
|
+
hostname: machine.hostname ? REDACTED_VALUE : undefined,
|
|
7172
|
+
sshAddress: machine.sshAddress ? REDACTED_VALUE : undefined,
|
|
7173
|
+
tailscaleName: machine.tailscaleName ? REDACTED_VALUE : undefined,
|
|
7174
|
+
platform: machine.platform,
|
|
7175
|
+
connection: machine.connection,
|
|
7176
|
+
workspacePath: redactPath(machine.workspacePath),
|
|
7177
|
+
bunPath: machine.bunPath ? redactPath(machine.bunPath) : undefined,
|
|
7178
|
+
tags: machine.tags ?? [],
|
|
7179
|
+
metadata,
|
|
7180
|
+
packages: machine.packages?.map((pkg) => ({ ...pkg })),
|
|
7181
|
+
apps: machine.apps?.map((app) => ({ ...app })),
|
|
7182
|
+
files: machine.files?.map((file) => ({
|
|
7183
|
+
...file,
|
|
7184
|
+
source: redactPath(file.source),
|
|
7185
|
+
target: redactPath(file.target)
|
|
7186
|
+
}))
|
|
7187
|
+
};
|
|
7188
|
+
}
|
|
7189
|
+
|
|
7190
|
+
// src/manifests.ts
|
|
7191
|
+
var PRIVATE_MANIFEST_REF_ENV = "HASNA_MACHINES_PRIVATE_MANIFEST_REF";
|
|
7192
|
+
var PRIVATE_MANIFEST_BACKEND_ENV = "HASNA_MACHINES_PRIVATE_MANIFEST_BACKEND";
|
|
7095
7193
|
var packageSchema = exports_external.object({
|
|
7096
7194
|
name: exports_external.string(),
|
|
7097
7195
|
manager: exports_external.enum(["bun", "brew", "apt", "custom"]).optional(),
|
|
@@ -7144,6 +7242,42 @@ function normalizePlatform() {
|
|
|
7144
7242
|
function normalizeMachines(machines) {
|
|
7145
7243
|
return [...machines].sort((left, right) => left.id.localeCompare(right.id));
|
|
7146
7244
|
}
|
|
7245
|
+
function inferPrivateBackend(rawRef, explicitBackend) {
|
|
7246
|
+
if (explicitBackend?.trim())
|
|
7247
|
+
return explicitBackend.trim();
|
|
7248
|
+
const scheme = rawRef.trim().match(/^([a-z][a-z0-9+.-]*)(?::\/\/|:)/i);
|
|
7249
|
+
return scheme?.[1] ?? null;
|
|
7250
|
+
}
|
|
7251
|
+
function fileSourceRef(path) {
|
|
7252
|
+
return {
|
|
7253
|
+
kind: "file",
|
|
7254
|
+
ref: redactPath(path),
|
|
7255
|
+
backend: "file",
|
|
7256
|
+
private: false,
|
|
7257
|
+
publicSafe: true
|
|
7258
|
+
};
|
|
7259
|
+
}
|
|
7260
|
+
function privateSourceRef(rawRef, backend) {
|
|
7261
|
+
return {
|
|
7262
|
+
kind: "private-ref",
|
|
7263
|
+
ref: redactPrivateRef(rawRef),
|
|
7264
|
+
backend: inferPrivateBackend(rawRef, backend),
|
|
7265
|
+
private: true,
|
|
7266
|
+
publicSafe: true
|
|
7267
|
+
};
|
|
7268
|
+
}
|
|
7269
|
+
function privateRefFromOptions(options) {
|
|
7270
|
+
const env2 = options.env ?? process.env;
|
|
7271
|
+
return options.privateRef?.trim() || env2[PRIVATE_MANIFEST_REF_ENV]?.trim() || env2["MACHINES_PRIVATE_MANIFEST_REF"]?.trim() || null;
|
|
7272
|
+
}
|
|
7273
|
+
function getManifestSourceRef(options = {}) {
|
|
7274
|
+
const rawPrivateRef = privateRefFromOptions(options);
|
|
7275
|
+
if (rawPrivateRef) {
|
|
7276
|
+
const env2 = options.env ?? process.env;
|
|
7277
|
+
return privateSourceRef(rawPrivateRef, options.privateBackend ?? env2[PRIVATE_MANIFEST_BACKEND_ENV]);
|
|
7278
|
+
}
|
|
7279
|
+
return fileSourceRef(options.path ?? getManifestPath());
|
|
7280
|
+
}
|
|
7147
7281
|
function getDefaultManifest() {
|
|
7148
7282
|
return {
|
|
7149
7283
|
version: 1,
|
|
@@ -7158,6 +7292,53 @@ function readManifest(path = getManifestPath()) {
|
|
|
7158
7292
|
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
7159
7293
|
return fleetSchema.parse(raw);
|
|
7160
7294
|
}
|
|
7295
|
+
function readManifestWithSource(options = {}) {
|
|
7296
|
+
const path = options.path ?? getManifestPath();
|
|
7297
|
+
const source = getManifestSourceRef(options);
|
|
7298
|
+
const warnings = [];
|
|
7299
|
+
if (source.kind === "private-ref") {
|
|
7300
|
+
const rawRef = privateRefFromOptions(options);
|
|
7301
|
+
if (rawRef && options.adapter) {
|
|
7302
|
+
try {
|
|
7303
|
+
const manifest2 = options.adapter.readManifest({ source, rawRef });
|
|
7304
|
+
if (manifest2) {
|
|
7305
|
+
return {
|
|
7306
|
+
manifest: fleetSchema.parse(manifest2),
|
|
7307
|
+
info: {
|
|
7308
|
+
source,
|
|
7309
|
+
loadedFrom: "private-ref",
|
|
7310
|
+
warnings
|
|
7311
|
+
}
|
|
7312
|
+
};
|
|
7313
|
+
}
|
|
7314
|
+
warnings.push(`private_manifest_adapter_empty:${redactIdentifier(options.adapter.id)}`);
|
|
7315
|
+
} catch (error) {
|
|
7316
|
+
warnings.push(`private_manifest_adapter_failed:${redactIdentifier(options.adapter.id)}`);
|
|
7317
|
+
}
|
|
7318
|
+
} else {
|
|
7319
|
+
warnings.push("private_manifest_ref_without_adapter");
|
|
7320
|
+
}
|
|
7321
|
+
const fallbackSource = fileSourceRef(path);
|
|
7322
|
+
const manifest = readManifest(path);
|
|
7323
|
+
return {
|
|
7324
|
+
manifest,
|
|
7325
|
+
info: {
|
|
7326
|
+
source,
|
|
7327
|
+
loadedFrom: existsSync3(path) ? "fallback" : "default",
|
|
7328
|
+
fallbackSource,
|
|
7329
|
+
warnings
|
|
7330
|
+
}
|
|
7331
|
+
};
|
|
7332
|
+
}
|
|
7333
|
+
return {
|
|
7334
|
+
manifest: readManifest(path),
|
|
7335
|
+
info: {
|
|
7336
|
+
source,
|
|
7337
|
+
loadedFrom: existsSync3(path) ? "file" : "default",
|
|
7338
|
+
warnings
|
|
7339
|
+
}
|
|
7340
|
+
};
|
|
7341
|
+
}
|
|
7161
7342
|
function validateManifest(path = getManifestPath()) {
|
|
7162
7343
|
return readManifest(path);
|
|
7163
7344
|
}
|
|
@@ -7444,7 +7625,7 @@ function buildEntry(input) {
|
|
|
7444
7625
|
},
|
|
7445
7626
|
route_hints: hints,
|
|
7446
7627
|
tags: manifest?.tags ?? [],
|
|
7447
|
-
metadata: manifest?.metadata
|
|
7628
|
+
metadata: redactMetadata(manifest?.metadata)
|
|
7448
7629
|
};
|
|
7449
7630
|
}
|
|
7450
7631
|
function discoverMachineTopology(options = {}) {
|
|
@@ -7682,7 +7863,7 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
7682
7863
|
warnings
|
|
7683
7864
|
};
|
|
7684
7865
|
}
|
|
7685
|
-
function
|
|
7866
|
+
function isRecord2(value) {
|
|
7686
7867
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7687
7868
|
}
|
|
7688
7869
|
function metadataString(metadata, keys) {
|
|
@@ -7712,13 +7893,13 @@ function metadataStringArray(metadata, keys) {
|
|
|
7712
7893
|
function readMappedPath(input) {
|
|
7713
7894
|
for (const containerName of input.containers) {
|
|
7714
7895
|
const container = input.metadata[containerName];
|
|
7715
|
-
if (!
|
|
7896
|
+
if (!isRecord2(container))
|
|
7716
7897
|
continue;
|
|
7717
7898
|
for (const key of input.keys) {
|
|
7718
7899
|
const value = container[key];
|
|
7719
7900
|
if (typeof value === "string" && value.trim())
|
|
7720
7901
|
return value.trim();
|
|
7721
|
-
if (
|
|
7902
|
+
if (isRecord2(value)) {
|
|
7722
7903
|
const path = metadataString(value, ["path", "root", "workspacePath", "workspace_path"]);
|
|
7723
7904
|
if (path)
|
|
7724
7905
|
return path;
|
|
@@ -7972,7 +8153,7 @@ function primaryMachine(machine, projectId, primaryMachineId) {
|
|
|
7972
8153
|
return machine.tags.includes("primary");
|
|
7973
8154
|
}
|
|
7974
8155
|
function metadataKeysForDiagnostics(metadata) {
|
|
7975
|
-
return
|
|
8156
|
+
return publicMetadataKeys(metadata);
|
|
7976
8157
|
}
|
|
7977
8158
|
function resolveMachineWorkspace(options) {
|
|
7978
8159
|
const now = options.now ?? new Date;
|
|
@@ -8205,6 +8386,13 @@ function buildBaseSteps(machine) {
|
|
|
8205
8386
|
manager: "apt",
|
|
8206
8387
|
privileged: true
|
|
8207
8388
|
});
|
|
8389
|
+
steps.push({
|
|
8390
|
+
id: "linux-update-downloads",
|
|
8391
|
+
title: "Enable Linux package list refresh and download-only upgrades",
|
|
8392
|
+
command: `printf '%s\\n' 'APT::Periodic::Update-Package-Lists "1";' 'APT::Periodic::Download-Upgradeable-Packages "1";' 'APT::Periodic::Unattended-Upgrade "0";' | sudo tee /etc/apt/apt.conf.d/20auto-upgrades >/dev/null`,
|
|
8393
|
+
manager: "apt",
|
|
8394
|
+
privileged: true
|
|
8395
|
+
});
|
|
8208
8396
|
} else if (machine.platform === "macos") {
|
|
8209
8397
|
steps.push({
|
|
8210
8398
|
id: "brew-base",
|
|
@@ -8218,7 +8406,26 @@ function buildBaseSteps(machine) {
|
|
|
8218
8406
|
command: "brew install git coreutils",
|
|
8219
8407
|
manager: "brew"
|
|
8220
8408
|
});
|
|
8409
|
+
steps.push({
|
|
8410
|
+
id: "macos-update-downloads",
|
|
8411
|
+
title: "Enable macOS update checks and downloads without automatic install",
|
|
8412
|
+
command: "sudo softwareupdate --schedule on && sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled -bool true && sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticDownload -int 1 && sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticallyInstallMacOSUpdates -int 0",
|
|
8413
|
+
manager: "custom",
|
|
8414
|
+
privileged: true
|
|
8415
|
+
});
|
|
8416
|
+
steps.push({
|
|
8417
|
+
id: "macos-management-readiness",
|
|
8418
|
+
title: "Report Apple management readiness without enrolling devices",
|
|
8419
|
+
command: "profiles status -type enrollment 2>/dev/null || true",
|
|
8420
|
+
manager: "custom"
|
|
8421
|
+
});
|
|
8221
8422
|
}
|
|
8423
|
+
steps.push({
|
|
8424
|
+
id: "github-app-auth-readiness",
|
|
8425
|
+
title: "Check GitHub CLI/App auth readiness without printing credentials",
|
|
8426
|
+
command: "command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1 || true",
|
|
8427
|
+
manager: "custom"
|
|
8428
|
+
});
|
|
8222
8429
|
return steps;
|
|
8223
8430
|
}
|
|
8224
8431
|
function buildPackageSteps(machine) {
|
|
@@ -9578,20 +9785,20 @@ function getStatus() {
|
|
|
9578
9785
|
|
|
9579
9786
|
// src/commands/workspace.ts
|
|
9580
9787
|
init_paths();
|
|
9581
|
-
function
|
|
9788
|
+
function isRecord3(value) {
|
|
9582
9789
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9583
9790
|
}
|
|
9584
9791
|
function cloneMetadata(metadata) {
|
|
9585
|
-
return
|
|
9792
|
+
return isRecord3(metadata) ? { ...metadata } : {};
|
|
9586
9793
|
}
|
|
9587
9794
|
function mappedPath(metadata, field, key) {
|
|
9588
9795
|
const container = metadata[field];
|
|
9589
|
-
if (!
|
|
9796
|
+
if (!isRecord3(container))
|
|
9590
9797
|
return null;
|
|
9591
9798
|
const value = container[key];
|
|
9592
9799
|
if (typeof value === "string" && value.trim())
|
|
9593
9800
|
return value.trim();
|
|
9594
|
-
if (
|
|
9801
|
+
if (isRecord3(value)) {
|
|
9595
9802
|
const nested = value["path"] ?? value["root"] ?? value["workspacePath"] ?? value["workspace_path"];
|
|
9596
9803
|
if (typeof nested === "string" && nested.trim())
|
|
9597
9804
|
return nested.trim();
|
|
@@ -9600,7 +9807,7 @@ function mappedPath(metadata, field, key) {
|
|
|
9600
9807
|
}
|
|
9601
9808
|
function writeMappedPath(metadata, field, key, path) {
|
|
9602
9809
|
const existing = metadata[field];
|
|
9603
|
-
const container =
|
|
9810
|
+
const container = isRecord3(existing) ? { ...existing } : {};
|
|
9604
9811
|
container[key] = path;
|
|
9605
9812
|
metadata[field] = container;
|
|
9606
9813
|
}
|
|
@@ -9970,12 +10177,28 @@ function checkMachineCompatibility(options = {}) {
|
|
|
9970
10177
|
|
|
9971
10178
|
// src/commands/doctor.ts
|
|
9972
10179
|
init_db();
|
|
9973
|
-
|
|
9974
|
-
|
|
10180
|
+
var DOCTOR_OPTIONAL_ADAPTER_DOMAINS = ["secrets", "configs", "monitor", "repos", "mcps", "shield"];
|
|
10181
|
+
function makeCheck2(id, status, summary, detail, extra = {}) {
|
|
10182
|
+
const { data, ...rest } = extra;
|
|
10183
|
+
return {
|
|
10184
|
+
...rest,
|
|
10185
|
+
id,
|
|
10186
|
+
status,
|
|
10187
|
+
summary,
|
|
10188
|
+
detail,
|
|
10189
|
+
data: data ? redactSensitiveValue(data) : undefined
|
|
10190
|
+
};
|
|
9975
10191
|
}
|
|
9976
10192
|
function parseKeyValueOutput(stdout) {
|
|
9977
|
-
|
|
9978
|
-
|
|
10193
|
+
const result = {};
|
|
10194
|
+
for (const line of stdout.trim().split(`
|
|
10195
|
+
`)) {
|
|
10196
|
+
const index = line.indexOf("=");
|
|
10197
|
+
if (index <= 0)
|
|
10198
|
+
continue;
|
|
10199
|
+
result[line.slice(0, index)] = line.slice(index + 1);
|
|
10200
|
+
}
|
|
10201
|
+
return result;
|
|
9979
10202
|
}
|
|
9980
10203
|
function buildDoctorCommand() {
|
|
9981
10204
|
return [
|
|
@@ -9983,9 +10206,11 @@ function buildDoctorCommand() {
|
|
|
9983
10206
|
'manifest_path="${HASNA_MACHINES_MANIFEST_PATH:-$data_dir/machines.json}"',
|
|
9984
10207
|
'db_path="${HASNA_MACHINES_DB_PATH:-$data_dir/machines.db}"',
|
|
9985
10208
|
'notifications_path="${HASNA_MACHINES_NOTIFICATIONS_PATH:-$data_dir/notifications.json}"',
|
|
10209
|
+
`printf 'data_dir=%s\\n' "$data_dir"`,
|
|
9986
10210
|
`printf 'manifest_path=%s\\n' "$manifest_path"`,
|
|
9987
10211
|
`printf 'db_path=%s\\n' "$db_path"`,
|
|
9988
10212
|
`printf 'notifications_path=%s\\n' "$notifications_path"`,
|
|
10213
|
+
`printf 'data_dir_exists=%s\\n' "$(test -d "$data_dir" && printf yes || printf no)"`,
|
|
9989
10214
|
`printf 'manifest_exists=%s\\n' "$(test -e "$manifest_path" && printf yes || printf no)"`,
|
|
9990
10215
|
`printf 'db_exists=%s\\n' "$(test -e "$db_path" && printf yes || printf no)"`,
|
|
9991
10216
|
`printf 'notifications_exists=%s\\n' "$(test -e "$notifications_path" && printf yes || printf no)"`,
|
|
@@ -9993,31 +10218,139 @@ function buildDoctorCommand() {
|
|
|
9993
10218
|
`printf 'ssh=%s\\n' "$(command -v ssh >/dev/null 2>&1 && printf ok || printf missing)"`,
|
|
9994
10219
|
`printf 'machines=%s\\n' "$(command -v machines 2>/dev/null || printf missing)"`,
|
|
9995
10220
|
`printf 'machines_agent=%s\\n' "$(command -v machines-agent 2>/dev/null || printf missing)"`,
|
|
9996
|
-
`printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"
|
|
10221
|
+
`printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`,
|
|
10222
|
+
`printf 'sudo_noninteractive=%s\\n' "$(sudo -n true >/dev/null 2>&1 && printf ok || printf unavailable)"`,
|
|
10223
|
+
`printf 'ssh_cert_support=%s\\n' "$(ssh -Q key-cert 2>/dev/null | grep -q 'ssh-ed25519-cert-v01@openssh.com' && printf ok || printf unavailable)"`,
|
|
10224
|
+
`printf 'gh_cli=%s\\n' "$(command -v gh 2>/dev/null || printf missing)"`,
|
|
10225
|
+
`printf 'gh_auth=%s\\n' "$(gh auth status >/dev/null 2>&1 && printf ok || printf unavailable)"`,
|
|
10226
|
+
"printf 'github_app_ref=%s\\n' \"$(test -n \\\"${HASNA_GITHUB_APP_ID:-}\\\" -a -n \\\"${HASNA_GITHUB_APP_PRIVATE_KEY_REF:-}\\\" && printf configured || printf missing)\""
|
|
9997
10227
|
].join("; ");
|
|
9998
10228
|
}
|
|
9999
|
-
function
|
|
10000
|
-
|
|
10229
|
+
function fallbackAdapterCheck(domain) {
|
|
10230
|
+
return makeCheck2(`${domain}-adapter`, "ok", `Optional ${domain} adapter`, `No ${domain} adapter configured; skipped optional private integration check.`, {
|
|
10231
|
+
optional: true,
|
|
10232
|
+
source: "open-machines",
|
|
10233
|
+
data: { configured: false, fallback: true }
|
|
10234
|
+
});
|
|
10235
|
+
}
|
|
10236
|
+
function sanitizeAdapterCheck(check, domain, adapterId) {
|
|
10237
|
+
const safeAdapterId = redactIdentifier(adapterId);
|
|
10238
|
+
return makeCheck2(check.id.startsWith(`${domain}-`) || check.id.startsWith(`${domain}:`) ? check.id : `${domain}:${check.id}`, check.status, check.summary, String(redactSensitiveValue(check.detail)), {
|
|
10239
|
+
...check,
|
|
10240
|
+
optional: check.optional ?? true,
|
|
10241
|
+
source: check.source ? String(redactSensitiveValue(check.source)) : `adapter:${safeAdapterId}`,
|
|
10242
|
+
data: check.data ? redactSensitiveValue(check.data) : undefined
|
|
10243
|
+
});
|
|
10244
|
+
}
|
|
10245
|
+
function runOptionalAdapterChecks(context, adapters) {
|
|
10246
|
+
const checks = [];
|
|
10247
|
+
for (const domain of DOCTOR_OPTIONAL_ADAPTER_DOMAINS) {
|
|
10248
|
+
const adapter2 = adapters.find((candidate) => candidate.checks?.[domain]);
|
|
10249
|
+
const hook = adapter2?.checks?.[domain];
|
|
10250
|
+
if (!adapter2 || !hook) {
|
|
10251
|
+
checks.push(fallbackAdapterCheck(domain));
|
|
10252
|
+
continue;
|
|
10253
|
+
}
|
|
10254
|
+
try {
|
|
10255
|
+
const result = hook(context);
|
|
10256
|
+
const domainChecks = Array.isArray(result) ? result : result ? [result] : [fallbackAdapterCheck(domain)];
|
|
10257
|
+
checks.push(...domainChecks.map((check) => sanitizeAdapterCheck(check, domain, adapter2.id)));
|
|
10258
|
+
} catch {
|
|
10259
|
+
const safeAdapterId = redactIdentifier(adapter2.id);
|
|
10260
|
+
checks.push(makeCheck2(`${domain}-adapter`, "warn", `Optional ${domain} adapter failed`, "Adapter failed; details are intentionally hidden to avoid leaking private refs or credentials.", {
|
|
10261
|
+
optional: true,
|
|
10262
|
+
source: `adapter:${safeAdapterId}`,
|
|
10263
|
+
data: { adapter: safeAdapterId, fallback: true }
|
|
10264
|
+
}));
|
|
10265
|
+
}
|
|
10266
|
+
}
|
|
10267
|
+
return checks;
|
|
10268
|
+
}
|
|
10269
|
+
function runDoctor(machineId = getLocalMachineId(), options = {}) {
|
|
10270
|
+
const now = options.now ?? new Date;
|
|
10271
|
+
const { manifest, info: manifestSource } = readManifestWithSource({ adapter: options.manifestAdapter ?? null });
|
|
10001
10272
|
const commandChecks = runMachineCommand(machineId, buildDoctorCommand());
|
|
10002
10273
|
const details = parseKeyValueOutput(commandChecks.stdout);
|
|
10003
10274
|
const machineInManifest = manifest.machines.find((machine) => machine.id === machineId);
|
|
10275
|
+
const optionalAdapterChecks = options.includeOptionalAdapters === false ? [] : runOptionalAdapterChecks({
|
|
10276
|
+
machineId,
|
|
10277
|
+
manifest,
|
|
10278
|
+
manifestSource,
|
|
10279
|
+
commandDetails: details,
|
|
10280
|
+
now
|
|
10281
|
+
}, options.adapters ?? []);
|
|
10004
10282
|
const checks = [
|
|
10005
|
-
makeCheck2("manifest-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10283
|
+
makeCheck2("manifest-source", manifestSource.warnings.length > 0 ? "warn" : "ok", "Manifest source boundary", `${manifestSource.source.kind}:${manifestSource.source.ref} loaded from ${manifestSource.loadedFrom}`, {
|
|
10284
|
+
data: {
|
|
10285
|
+
source: manifestSource.source,
|
|
10286
|
+
loadedFrom: manifestSource.loadedFrom,
|
|
10287
|
+
fallbackSource: manifestSource.fallbackSource,
|
|
10288
|
+
warnings: manifestSource.warnings
|
|
10289
|
+
},
|
|
10290
|
+
remediation: manifestSource.warnings.length > 0 ? ["Provide a private manifest adapter or unset the private manifest ref to use the local manifest only."] : undefined
|
|
10291
|
+
}),
|
|
10292
|
+
makeCheck2("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", machineInManifest ? JSON.stringify(redactManifestForDiagnostics(machineInManifest)) : `No manifest entry for ${machineId}`, {
|
|
10293
|
+
data: {
|
|
10294
|
+
declared: Boolean(machineInManifest),
|
|
10295
|
+
machine: machineInManifest ? redactManifestForDiagnostics(machineInManifest) : null
|
|
10296
|
+
}
|
|
10297
|
+
}),
|
|
10298
|
+
makeCheck2("data-dir", details["data_dir_exists"] === "yes" ? "ok" : "warn", "Data directory check", `${redactPath(details["data_dir"] || "unknown")} ${details["data_dir_exists"] === "yes" ? "exists" : "missing"}`, {
|
|
10299
|
+
data: {
|
|
10300
|
+
path: redactPath(details["data_dir"] || "unknown"),
|
|
10301
|
+
exists: details["data_dir_exists"] === "yes"
|
|
10302
|
+
}
|
|
10303
|
+
}),
|
|
10304
|
+
makeCheck2("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${redactPath(details["manifest_path"] || "unknown")} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`, {
|
|
10305
|
+
data: {
|
|
10306
|
+
path: redactPath(details["manifest_path"] || "unknown"),
|
|
10307
|
+
exists: details["manifest_exists"] === "yes"
|
|
10308
|
+
}
|
|
10309
|
+
}),
|
|
10310
|
+
makeCheck2("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${redactPath(details["db_path"] || "unknown")} ${details["db_exists"] === "yes" ? "exists" : "missing"}`, {
|
|
10311
|
+
data: {
|
|
10312
|
+
path: redactPath(details["db_path"] || "unknown"),
|
|
10313
|
+
exists: details["db_exists"] === "yes"
|
|
10314
|
+
}
|
|
10315
|
+
}),
|
|
10316
|
+
makeCheck2("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${redactPath(details["notifications_path"] || "unknown")} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`, {
|
|
10317
|
+
data: {
|
|
10318
|
+
path: redactPath(details["notifications_path"] || "unknown"),
|
|
10319
|
+
exists: details["notifications_exists"] === "yes"
|
|
10320
|
+
}
|
|
10321
|
+
}),
|
|
10009
10322
|
makeCheck2("bun", details["bun"] && details["bun"] !== "missing" ? "ok" : "fail", "Bun availability", details["bun"] || "missing"),
|
|
10010
10323
|
makeCheck2("machines-cli", details["machines"] && details["machines"] !== "missing" ? "ok" : "warn", "machines CLI availability", details["machines"] || "missing"),
|
|
10011
10324
|
makeCheck2("machines-agent-cli", details["machines_agent"] && details["machines_agent"] !== "missing" ? "ok" : "warn", "machines-agent availability", details["machines_agent"] || "missing"),
|
|
10012
10325
|
makeCheck2("machines-mcp-cli", details["machines_mcp"] && details["machines_mcp"] !== "missing" ? "ok" : "warn", "machines-mcp availability", details["machines_mcp"] || "missing"),
|
|
10013
|
-
makeCheck2("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing")
|
|
10326
|
+
makeCheck2("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing"),
|
|
10327
|
+
makeCheck2("sudo-noninteractive", details["sudo_noninteractive"] === "ok" ? "ok" : "warn", "Noninteractive sudo availability", details["sudo_noninteractive"] === "ok" ? "sudo -n is available" : "sudo -n unavailable; setup may require user-provided approval or password handling.", {
|
|
10328
|
+
data: { available: details["sudo_noninteractive"] === "ok" },
|
|
10329
|
+
remediation: details["sudo_noninteractive"] === "ok" ? undefined : ["Configure explicit sudo policy or run setup commands manually; do not store sudo passwords in public manifests."]
|
|
10330
|
+
}),
|
|
10331
|
+
makeCheck2("ssh-cert-support", details["ssh_cert_support"] === "ok" ? "ok" : "warn", "SSH certificate support", details["ssh_cert_support"] === "ok" ? "OpenSSH reports ed25519 certificate support" : "OpenSSH certificate support not detected.", {
|
|
10332
|
+
data: { supported: details["ssh_cert_support"] === "ok" },
|
|
10333
|
+
remediation: details["ssh_cert_support"] === "ok" ? undefined : ["Install or update OpenSSH before adopting SSH certificate auth for this machine."]
|
|
10334
|
+
}),
|
|
10335
|
+
makeCheck2("github-app-auth", details["github_app_ref"] === "configured" ? "ok" : "warn", "GitHub App auth references", details["github_app_ref"] === "configured" ? "GitHub App id and private-key reference are configured" : "GitHub App id/private-key reference missing; use secret references, not user tokens or raw private keys.", {
|
|
10336
|
+
data: {
|
|
10337
|
+
gh_cli: details["gh_cli"] && details["gh_cli"] !== "missing",
|
|
10338
|
+
gh_auth: details["gh_auth"] === "ok",
|
|
10339
|
+
app_ref_configured: details["github_app_ref"] === "configured"
|
|
10340
|
+
},
|
|
10341
|
+
remediation: details["github_app_ref"] === "configured" ? undefined : ["Set HASNA_GITHUB_APP_ID plus HASNA_GITHUB_APP_PRIVATE_KEY_REF or provide an equivalent open-secrets adapter."]
|
|
10342
|
+
}),
|
|
10343
|
+
...optionalAdapterChecks
|
|
10014
10344
|
];
|
|
10015
10345
|
return {
|
|
10016
10346
|
machineId,
|
|
10017
10347
|
source: commandChecks.source,
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10348
|
+
schemaVersion: 1,
|
|
10349
|
+
generatedAt: now.toISOString(),
|
|
10350
|
+
manifestSource,
|
|
10351
|
+
manifestPath: details["manifest_path"] ? redactPath(details["manifest_path"]) : undefined,
|
|
10352
|
+
dbPath: details["db_path"] ? redactPath(details["db_path"]) : undefined,
|
|
10353
|
+
notificationsPath: details["notifications_path"] ? redactPath(details["notifications_path"]) : undefined,
|
|
10021
10354
|
checks
|
|
10022
10355
|
};
|
|
10023
10356
|
}
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
1
|
+
import { type ManifestSourceAdapter } from "../manifests.js";
|
|
2
|
+
import type { DoctorCheck, DoctorReport, FleetManifest, ManifestLoadInfo } from "../types.js";
|
|
3
|
+
export declare const DOCTOR_OPTIONAL_ADAPTER_DOMAINS: readonly ["secrets", "configs", "monitor", "repos", "mcps", "shield"];
|
|
4
|
+
export type DoctorOptionalAdapterDomain = typeof DOCTOR_OPTIONAL_ADAPTER_DOMAINS[number];
|
|
5
|
+
export interface DoctorAdapterContext {
|
|
6
|
+
machineId: string;
|
|
7
|
+
manifest: FleetManifest;
|
|
8
|
+
manifestSource: ManifestLoadInfo;
|
|
9
|
+
commandDetails: Record<string, string>;
|
|
10
|
+
now: Date;
|
|
11
|
+
}
|
|
12
|
+
export type DoctorAdapterHook = (context: DoctorAdapterContext) => DoctorCheck | DoctorCheck[] | null | undefined;
|
|
13
|
+
export interface DoctorAdapter {
|
|
14
|
+
id: string;
|
|
15
|
+
checks?: Partial<Record<DoctorOptionalAdapterDomain, DoctorAdapterHook>>;
|
|
16
|
+
}
|
|
17
|
+
export interface DoctorOptions {
|
|
18
|
+
now?: Date;
|
|
19
|
+
manifestAdapter?: ManifestSourceAdapter | null;
|
|
20
|
+
adapters?: DoctorAdapter[];
|
|
21
|
+
includeOptionalAdapters?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function runDoctor(machineId?: string, options?: DoctorOptions): DoctorReport;
|
|
3
24
|
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAGrF,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE9F,eAAO,MAAM,+BAA+B,uEAAwE,CAAC;AAErH,MAAM,MAAM,2BAA2B,GAAG,OAAO,+BAA+B,CAAC,MAAM,CAAC,CAAC;AAEzF,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,aAAa,CAAC;IACxB,cAAc,EAAE,gBAAgB,CAAC;IACjC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,GAAG,EAAE,IAAI,CAAC;CACX;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,oBAAoB,KAAK,WAAW,GAAG,WAAW,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;AAElH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,2BAA2B,EAAE,iBAAiB,CAAC,CAAC,CAAC;CAC1E;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,eAAe,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;IAC3B,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAuHD,wBAAgB,SAAS,CAAC,SAAS,SAAsB,EAAE,OAAO,GAAE,aAAkB,GAAG,YAAY,CAwLpG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoD,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAC3G,OAAO,KAAK,EAAmB,WAAW,EAAa,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoD,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAC3G,OAAO,KAAK,EAAmB,WAAW,EAAa,MAAM,aAAa,CAAC;AAiG3E,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,WAAW,CAyB9D;AAED,wBAAgB,QAAQ,CACtB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,EAChD,MAAM,GAAE,oBAAwC,GAC/C,WAAW,CAmCb"}
|