akm-cli 0.9.1 → 0.9.2-alpha.2
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/CHANGELOG.md +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -29,9 +29,14 @@ import { ConfigError } from "../../core/errors.js";
|
|
|
29
29
|
import { getTaskLogDir } from "../../core/paths.js";
|
|
30
30
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
31
31
|
import { parseSchedule, translateToLaunchd } from "../schedule.js";
|
|
32
|
-
import {
|
|
32
|
+
import { assertSchedulerExecutionEvidenceDigest, assertSchedulerExpectationIdentity, assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, assertSchedulerNativeArtifactOwner, assertSchedulerRemovalArtifact, assertSchedulerRollbackArtifactCardinality, schedulerBindingNativeId, schedulerLogicalBindingId, schedulerLogicalBindingOwner, schedulerNativeArtifactKey, } from "../scheduler-binding.js";
|
|
33
|
+
import { buildScheduledBindingInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
|
|
33
34
|
import { escapeXml, nodeExec, nodeFs, runOrThrow } from "./exec-utils.js";
|
|
34
35
|
export const LAUNCHD_LABEL_PREFIX = "com.akm.task.";
|
|
36
|
+
const MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
37
|
+
const MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES = 4096;
|
|
38
|
+
const LAUNCHD_AKM_LABEL_RE = /^com\.akm\.task\.[A-Za-z0-9._-]{1,1024}$/u;
|
|
39
|
+
const LAUNCHD_SNAPSHOT = Symbol("akm-launchd-binding-snapshot");
|
|
35
40
|
export function LAUNCHD_BACKEND(options = {}) {
|
|
36
41
|
const exec = options.exec ?? defaultLaunchdExec();
|
|
37
42
|
const fsLike = options.fs ?? defaultLaunchdFs();
|
|
@@ -39,240 +44,708 @@ export function LAUNCHD_BACKEND(options = {}) {
|
|
|
39
44
|
const logDir = options.logDir ?? getTaskLogDir();
|
|
40
45
|
const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
|
|
41
46
|
const scheduledContext = options.scheduledContext ?? resolveScheduledTaskContext();
|
|
42
|
-
const plistPath = (
|
|
43
|
-
const label = (
|
|
44
|
-
const target = (
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
return undefined;
|
|
48
|
-
if (typeof options.envPath === "string")
|
|
49
|
-
return options.envPath;
|
|
50
|
-
return process.env.PATH ?? "";
|
|
51
|
-
};
|
|
52
|
-
const defaultContextPath = schedulerContextPath(schedulerContextDescriptor(scheduledContext, pathEnv() ?? ""));
|
|
53
|
-
const setEnableState = (id, enabled) => {
|
|
54
|
-
const verb = enabled ? "enable" : "disable";
|
|
55
|
-
runOrThrow(exec, ["launchctl", verb, target(id)], {
|
|
56
|
-
message: (r) => `launchctl ${verb} failed: ${r.stderr || r.stdout || "no output"}.`,
|
|
57
|
-
});
|
|
58
|
-
};
|
|
47
|
+
const plistPath = (nativeId) => path.join(agentsDir, `${LAUNCHD_LABEL_PREFIX}${nativeId}.plist`);
|
|
48
|
+
const label = (nativeId) => `${LAUNCHD_LABEL_PREFIX}${nativeId}`;
|
|
49
|
+
const target = (nativeId) => `gui/${exec.uid()}/${label(nativeId)}`;
|
|
50
|
+
const defaultContextPath = launchdDefaultContextPath(options, scheduledContext);
|
|
51
|
+
const setEnableState = (nativeId, enabled) => setLaunchdEnableState(exec, target(nativeId), enabled);
|
|
59
52
|
return {
|
|
60
53
|
name: "launchd",
|
|
61
|
-
install(task, opts) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
54
|
+
install(task, opts, expected) {
|
|
55
|
+
installLaunchdBinding(task, opts, expected, {
|
|
56
|
+
exec,
|
|
57
|
+
fsLike,
|
|
58
|
+
agentsDir,
|
|
59
|
+
logDir,
|
|
60
|
+
akmArgv,
|
|
61
|
+
defaultContextPath,
|
|
62
|
+
plistPath,
|
|
63
|
+
label,
|
|
64
|
+
target,
|
|
65
|
+
setEnableState,
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
uninstall(nativeId, expected) {
|
|
69
|
+
const inspectionContext = { exec, fsLike, agentsDir, plistPath, target, label };
|
|
70
|
+
const initialNamespace = inspectStableLaunchdNamespace([nativeId], inspectionContext);
|
|
71
|
+
let initialArtifact;
|
|
72
|
+
if (expected) {
|
|
73
|
+
assertSchedulerExpectationIdentity({ ...expected, state: "present" });
|
|
74
|
+
initialArtifact = assertSchedulerNativeArtifactCardinality(initialNamespace.inspection.artifacts, nativeId, 1);
|
|
75
|
+
if (!initialArtifact) {
|
|
76
|
+
throw new ConfigError(`launchd artifact ${JSON.stringify(nativeId)} disappeared during inspection.`, "INVALID_CONFIG_FILE");
|
|
72
77
|
}
|
|
73
|
-
|
|
78
|
+
assertSchedulerRemovalArtifact(initialArtifact.nativeId, expected, initialArtifact.invocation, initialArtifact.fingerprint);
|
|
74
79
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
else {
|
|
81
|
+
initialArtifact = assertLaunchdDirectRemovalOwner(initialNamespace.inspection.artifacts, nativeId);
|
|
82
|
+
}
|
|
83
|
+
let resolvedNativeId = initialArtifact?.nativeId ?? nativeId;
|
|
84
|
+
const file = plistPath(resolvedNativeId);
|
|
85
|
+
const snapshot = snapshotLaunchdBindings([resolvedNativeId], {
|
|
86
|
+
exec,
|
|
87
|
+
fsLike,
|
|
88
|
+
agentsDir,
|
|
89
|
+
plistPath,
|
|
90
|
+
target,
|
|
91
|
+
label,
|
|
92
|
+
});
|
|
93
|
+
const finalArtifact = expected
|
|
94
|
+
? assertSchedulerNativeArtifactCardinality(snapshot.artifacts, nativeId, 1)
|
|
95
|
+
: assertLaunchdDirectRemovalOwner(snapshot.artifacts, nativeId);
|
|
96
|
+
if (expected) {
|
|
97
|
+
if (!finalArtifact) {
|
|
98
|
+
throw new ConfigError(`launchd artifact ${JSON.stringify(nativeId)} disappeared during snapshot.`, "INVALID_CONFIG_FILE");
|
|
99
|
+
}
|
|
100
|
+
assertSchedulerRemovalArtifact(finalArtifact.nativeId, expected, finalArtifact.invocation, finalArtifact.fingerprint);
|
|
101
|
+
}
|
|
102
|
+
if (!finalArtifact)
|
|
103
|
+
return;
|
|
104
|
+
resolvedNativeId = finalArtifact.nativeId;
|
|
85
105
|
try {
|
|
86
|
-
|
|
106
|
+
runOrThrow(exec, ["launchctl", "bootout", target(resolvedNativeId)], {
|
|
87
107
|
isOk: (r) => r.status === 0 || isServiceNotFoundResult(r),
|
|
88
108
|
message: (r) => `launchctl bootout failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
89
109
|
});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
fsLike.
|
|
93
|
-
|
|
94
|
-
// A disable override survives bootout and plist replacement. Clear it
|
|
95
|
-
// before bootstrap, then apply the desired state after registration.
|
|
96
|
-
enableStateTouched = true;
|
|
97
|
-
setEnableState(task.id, true);
|
|
98
|
-
runOrThrow(exec, ["launchctl", "bootstrap", `gui/${exec.uid()}`, file], {
|
|
99
|
-
message: (r) => `launchctl bootstrap failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
100
|
-
hint: "Ensure `launchctl` is available; on macOS it is part of the base system.",
|
|
101
|
-
});
|
|
102
|
-
if (!task.enabled) {
|
|
103
|
-
setEnableState(task.id, false);
|
|
104
|
-
}
|
|
110
|
+
// launchctl disable overrides persist after the plist is removed.
|
|
111
|
+
setEnableState(resolvedNativeId, true);
|
|
112
|
+
if (fsLike.exists(file))
|
|
113
|
+
fsLike.removeFile(file);
|
|
105
114
|
}
|
|
106
|
-
catch (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
let replacementUnloaded = false;
|
|
113
|
-
try {
|
|
114
|
-
const rollbackBootout = exec.run(["launchctl", "bootout", target(task.id)]);
|
|
115
|
-
replacementUnloaded = rollbackBootout.status === 0 || isServiceNotFoundResult(rollbackBootout);
|
|
116
|
-
if (!replacementUnloaded) {
|
|
117
|
-
rollbackErrors.push(new ConfigError(`launchctl bootout during rollback failed: ${rollbackBootout.stderr || rollbackBootout.stdout || "no output"}.`, "INVALID_CONFIG_FILE"));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch (rollbackError) {
|
|
121
|
-
rollbackErrors.push(rollbackError);
|
|
122
|
-
}
|
|
123
|
-
if (replacementUnloaded) {
|
|
124
|
-
try {
|
|
125
|
-
if (previousPlist === undefined) {
|
|
126
|
-
if (fsLike.exists(file))
|
|
127
|
-
fsLike.removeFile(file);
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
fsLike.writeFile(tempFile, previousPlist);
|
|
131
|
-
fsLike.replaceFile(tempFile, file);
|
|
132
|
-
}
|
|
133
|
-
priorFileRestored = true;
|
|
134
|
-
}
|
|
135
|
-
catch (rollbackError) {
|
|
136
|
-
rollbackErrors.push(rollbackError);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (previousPlist !== undefined && previousWasLoaded && priorFileRestored) {
|
|
141
|
-
try {
|
|
142
|
-
setEnableState(task.id, true);
|
|
143
|
-
const restore = exec.run(["launchctl", "bootstrap", `gui/${exec.uid()}`, file]);
|
|
144
|
-
if (restore.status !== 0) {
|
|
145
|
-
rollbackErrors.push(new ConfigError(`launchctl bootstrap during rollback failed: ${restore.stderr || restore.stdout || "no output"}.`, "INVALID_CONFIG_FILE"));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
catch (rollbackError) {
|
|
149
|
-
rollbackErrors.push(rollbackError);
|
|
150
|
-
}
|
|
151
|
-
if (!previousEnabled) {
|
|
152
|
-
try {
|
|
153
|
-
setEnableState(task.id, false);
|
|
154
|
-
}
|
|
155
|
-
catch (rollbackError) {
|
|
156
|
-
rollbackErrors.push(rollbackError);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
else if (enableStateTouched) {
|
|
161
|
-
try {
|
|
162
|
-
setEnableState(task.id, previousPlist === undefined || previousEnabled);
|
|
163
|
-
}
|
|
164
|
-
catch (rollbackError) {
|
|
165
|
-
rollbackErrors.push(rollbackError);
|
|
166
|
-
}
|
|
115
|
+
catch (primaryError) {
|
|
116
|
+
try {
|
|
117
|
+
const rollbackExpected = [
|
|
118
|
+
launchdRollbackExpectationForCurrent(resolvedNativeId, inspectionContext, initialArtifact),
|
|
119
|
+
];
|
|
120
|
+
restoreLaunchdBindings(snapshot, { exec, fsLike, agentsDir, plistPath, target, label, setEnableState }, rollbackExpected);
|
|
167
121
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
throw new AggregateError([err, ...rollbackErrors], `${message}; rollback for launchd task "${task.id}" was incomplete.`);
|
|
122
|
+
catch (rollbackError) {
|
|
123
|
+
throw new AggregateError([primaryError, rollbackError], `${errorMessage(primaryError)}; rollback for launchd removal ${JSON.stringify(nativeId)} was incomplete.`);
|
|
171
124
|
}
|
|
172
|
-
throw
|
|
125
|
+
throw primaryError;
|
|
173
126
|
}
|
|
174
|
-
finally {
|
|
175
|
-
if (fsLike.exists(tempFile))
|
|
176
|
-
fsLike.removeFile(tempFile);
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
uninstall(id) {
|
|
180
|
-
runOrThrow(exec, ["launchctl", "bootout", target(id)], {
|
|
181
|
-
isOk: (r) => r.status === 0 || isServiceNotFoundResult(r),
|
|
182
|
-
message: (r) => `launchctl bootout failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
183
|
-
});
|
|
184
|
-
// launchctl disable overrides persist after the plist is removed.
|
|
185
|
-
setEnableState(id, true);
|
|
186
|
-
const file = plistPath(id);
|
|
187
|
-
if (fsLike.exists(file))
|
|
188
|
-
fsLike.removeFile(file);
|
|
189
127
|
},
|
|
190
|
-
setEnabled(
|
|
191
|
-
setEnableState(
|
|
128
|
+
setEnabled(nativeId, enabled) {
|
|
129
|
+
setEnableState(nativeId, enabled);
|
|
192
130
|
},
|
|
193
131
|
list() {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
for (const file of fsLike.list(agentsDir)) {
|
|
198
|
-
if (file.startsWith(LAUNCHD_LABEL_PREFIX) && file.endsWith(".plist")) {
|
|
199
|
-
ids.push(file.slice(LAUNCHD_LABEL_PREFIX.length, -".plist".length));
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
if (ids.length === 0)
|
|
203
|
-
return [];
|
|
204
|
-
const disabledLabels = readDisabledLabels(exec);
|
|
205
|
-
return ids
|
|
206
|
-
.map((id) => inspectInstalledLaunchdTask(id, fsLike.readFile(plistPath(id)), disabledLabels, exec))
|
|
207
|
-
.filter((ref) => ref !== undefined);
|
|
132
|
+
return [
|
|
133
|
+
...inspectLaunchdState({ exec, fsLike, agentsDir, plistPath, target, label }).installed,
|
|
134
|
+
];
|
|
208
135
|
},
|
|
209
136
|
listForRebind() {
|
|
210
|
-
|
|
211
|
-
return [];
|
|
137
|
+
const namespace = inspectStableLaunchdNamespace([], { exec, fsLike, agentsDir, plistPath, target, label });
|
|
212
138
|
const refs = [];
|
|
213
|
-
for (const
|
|
214
|
-
if (
|
|
139
|
+
for (const entry of namespace.entries) {
|
|
140
|
+
if (entry.plist === undefined)
|
|
215
141
|
continue;
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
142
|
+
const installed = extractPlistInvocation(entry.plist);
|
|
143
|
+
const id = entry.nativeId;
|
|
144
|
+
const ref = {
|
|
145
|
+
id: installed ? schedulerLogicalBindingId(id, installed.invocation) : id,
|
|
146
|
+
...(installed?.target !== undefined ? { target: installed.target } : {}),
|
|
147
|
+
};
|
|
148
|
+
Object.defineProperty(ref, "nativeId", { value: id });
|
|
149
|
+
if (installed)
|
|
150
|
+
Object.defineProperty(ref, "invocation", { value: Object.freeze([...installed.invocation]) });
|
|
151
|
+
refs.push(ref);
|
|
219
152
|
}
|
|
220
153
|
return refs;
|
|
221
154
|
},
|
|
155
|
+
listNativeArtifacts() {
|
|
156
|
+
return [...inspectLaunchdState({ exec, fsLike, agentsDir, plistPath, target, label }).artifacts];
|
|
157
|
+
},
|
|
158
|
+
inspectBindings() {
|
|
159
|
+
return inspectLaunchdState({ exec, fsLike, agentsDir, plistPath, target, label });
|
|
160
|
+
},
|
|
161
|
+
snapshotBindings(ids) {
|
|
162
|
+
return snapshotLaunchdBindings(ids, { exec, fsLike, agentsDir, plistPath, target, label });
|
|
163
|
+
},
|
|
164
|
+
restoreBindings(snapshot, expectedCurrent) {
|
|
165
|
+
restoreLaunchdBindings(snapshot, { exec, fsLike, agentsDir, plistPath, target, label, setEnableState }, expectedCurrent);
|
|
166
|
+
},
|
|
222
167
|
expectedSignature(task, opts) {
|
|
223
|
-
return
|
|
168
|
+
return launchdFingerprint(buildPlistXml(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target), task.enabled, true);
|
|
224
169
|
},
|
|
225
170
|
};
|
|
226
171
|
}
|
|
227
|
-
function
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
172
|
+
function installLaunchdBinding(task, opts, expected, context) {
|
|
173
|
+
const { exec, fsLike, agentsDir, logDir, akmArgv, defaultContextPath, plistPath, label, target, setEnableState } = context;
|
|
174
|
+
if (expected)
|
|
175
|
+
assertSchedulerExpectationIdentity(expected, task);
|
|
176
|
+
// Capture PATH at install time so launchd (which strips the environment
|
|
177
|
+
// aggressively) can find the same binaries the user sees interactively.
|
|
178
|
+
const xml = buildPlistXml(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target);
|
|
179
|
+
const nativeId = schedulerBindingNativeId(task);
|
|
180
|
+
const file = plistPath(nativeId);
|
|
181
|
+
fsLike.ensureDir(agentsDir);
|
|
182
|
+
// launchd refuses to start a job when StandardOutPath/StandardErrorPath
|
|
183
|
+
// points at a non-existent directory; create it before reading or mutating
|
|
184
|
+
// launchd state so a local preparation failure has no scheduler side effect.
|
|
185
|
+
fsLike.ensureDir(logDir);
|
|
186
|
+
const inspectionContext = { exec, fsLike, agentsDir, plistPath, target, label };
|
|
187
|
+
const initialNamespace = inspectStableLaunchdNamespace([nativeId], inspectionContext);
|
|
188
|
+
const initialArtifact = expected
|
|
189
|
+
? assertSchedulerNativeArtifactCardinality(initialNamespace.inspection.artifacts, nativeId, expected.state === "absent" ? 0 : 1)
|
|
190
|
+
: assertLaunchdDirectMutationOwner(initialNamespace.inspection.artifacts, nativeId, task);
|
|
191
|
+
if (expected)
|
|
192
|
+
assertSchedulerMutationArtifact(initialArtifact, expected);
|
|
193
|
+
const priorEntry = initialArtifact
|
|
194
|
+
? initialNamespace.entries.find((entry) => entry.nativeId === initialArtifact.nativeId)
|
|
195
|
+
: undefined;
|
|
196
|
+
const previousPlist = priorEntry?.plist;
|
|
197
|
+
const previousEnabled = priorEntry?.enabled ?? true;
|
|
198
|
+
const tempFile = path.join(agentsDir, `.${nativeId}.${Date.now()}.tmp`);
|
|
199
|
+
fsLike.writeFile(tempFile, xml);
|
|
200
|
+
let bootoutCompleted = false;
|
|
201
|
+
let previousWasLoaded = false;
|
|
202
|
+
let fileReplaced = false;
|
|
203
|
+
let enableStateTouched = false;
|
|
243
204
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
205
|
+
if (expected) {
|
|
206
|
+
const finalInspection = inspectStableLaunchdNamespace([nativeId], inspectionContext).inspection;
|
|
207
|
+
const finalArtifact = assertSchedulerNativeArtifactCardinality(finalInspection.artifacts, nativeId, expected.state === "absent" ? 0 : 1);
|
|
208
|
+
assertSchedulerMutationArtifact(finalArtifact, expected);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const finalArtifact = assertLaunchdDirectMutationOwner(inspectStableLaunchdNamespace([nativeId], inspectionContext).inspection.artifacts, nativeId, task);
|
|
212
|
+
if (finalArtifact?.nativeId !== initialArtifact?.nativeId ||
|
|
213
|
+
finalArtifact?.fingerprint !== initialArtifact?.fingerprint) {
|
|
214
|
+
throw new ConfigError(`launchd task ${JSON.stringify(nativeId)} changed while it was being prepared; refusing to replace an unverified owner.`, "INVALID_CONFIG_FILE");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const bootout = runOrThrow(exec, ["launchctl", "bootout", target(nativeId)], {
|
|
218
|
+
isOk: (r) => r.status === 0 || isServiceNotFoundResult(r),
|
|
219
|
+
message: (r) => `launchctl bootout failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
220
|
+
});
|
|
221
|
+
bootoutCompleted = true;
|
|
222
|
+
previousWasLoaded = previousPlist !== undefined && bootout.status === 0;
|
|
223
|
+
fsLike.replaceFile(tempFile, file);
|
|
224
|
+
fileReplaced = true;
|
|
225
|
+
// A disable override survives bootout and plist replacement. Clear it
|
|
226
|
+
// before bootstrap, then apply the desired state after registration.
|
|
227
|
+
enableStateTouched = true;
|
|
228
|
+
setEnableState(nativeId, true);
|
|
229
|
+
runOrThrow(exec, ["launchctl", "bootstrap", `gui/${exec.uid()}`, file], {
|
|
230
|
+
message: (r) => `launchctl bootstrap failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
231
|
+
hint: "Ensure `launchctl` is available; on macOS it is part of the base system.",
|
|
232
|
+
});
|
|
233
|
+
if (!task.enabled)
|
|
234
|
+
setEnableState(nativeId, false);
|
|
247
235
|
}
|
|
248
|
-
catch {
|
|
249
|
-
|
|
236
|
+
catch (err) {
|
|
237
|
+
if (!bootoutCompleted)
|
|
238
|
+
throw err;
|
|
239
|
+
const rollbackErrors = [];
|
|
240
|
+
let priorFileRestored = !fileReplaced;
|
|
241
|
+
if (fileReplaced) {
|
|
242
|
+
let replacementUnloaded = false;
|
|
243
|
+
try {
|
|
244
|
+
const rollbackBootout = exec.run(["launchctl", "bootout", target(nativeId)]);
|
|
245
|
+
replacementUnloaded = rollbackBootout.status === 0 || isServiceNotFoundResult(rollbackBootout);
|
|
246
|
+
if (!replacementUnloaded) {
|
|
247
|
+
rollbackErrors.push(new ConfigError(`launchctl bootout during rollback failed: ${rollbackBootout.stderr || rollbackBootout.stdout || "no output"}.`, "INVALID_CONFIG_FILE"));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (rollbackError) {
|
|
251
|
+
rollbackErrors.push(rollbackError);
|
|
252
|
+
}
|
|
253
|
+
if (replacementUnloaded) {
|
|
254
|
+
try {
|
|
255
|
+
if (previousPlist === undefined) {
|
|
256
|
+
if (fsLike.exists(file))
|
|
257
|
+
fsLike.removeFile(file);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
fsLike.writeFile(tempFile, previousPlist);
|
|
261
|
+
fsLike.replaceFile(tempFile, file);
|
|
262
|
+
}
|
|
263
|
+
priorFileRestored = true;
|
|
264
|
+
}
|
|
265
|
+
catch (rollbackError) {
|
|
266
|
+
rollbackErrors.push(rollbackError);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
restoreLaunchdInstallState({
|
|
271
|
+
exec,
|
|
272
|
+
nativeId,
|
|
273
|
+
file,
|
|
274
|
+
previousPlist,
|
|
275
|
+
previousEnabled,
|
|
276
|
+
previousWasLoaded,
|
|
277
|
+
priorFileRestored,
|
|
278
|
+
enableStateTouched,
|
|
279
|
+
setEnableState,
|
|
280
|
+
rollbackErrors,
|
|
281
|
+
});
|
|
282
|
+
if (rollbackErrors.length > 0) {
|
|
283
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
284
|
+
throw new AggregateError([err, ...rollbackErrors], `${message}; rollback for launchd task "${task.id}" was incomplete.`);
|
|
285
|
+
}
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
if (fsLike.exists(tempFile))
|
|
290
|
+
fsLike.removeFile(tempFile);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function restoreLaunchdInstallState(input) {
|
|
294
|
+
const { exec, nativeId, file, previousPlist, previousEnabled, previousWasLoaded, priorFileRestored, enableStateTouched, setEnableState, rollbackErrors, } = input;
|
|
295
|
+
if (previousPlist !== undefined && previousWasLoaded && priorFileRestored) {
|
|
296
|
+
try {
|
|
297
|
+
setEnableState(nativeId, true);
|
|
298
|
+
const restore = exec.run(["launchctl", "bootstrap", `gui/${exec.uid()}`, file]);
|
|
299
|
+
if (restore.status !== 0) {
|
|
300
|
+
rollbackErrors.push(new ConfigError(`launchctl bootstrap during rollback failed: ${restore.stderr || restore.stdout || "no output"}.`, "INVALID_CONFIG_FILE"));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (rollbackError) {
|
|
304
|
+
rollbackErrors.push(rollbackError);
|
|
305
|
+
}
|
|
306
|
+
if (!previousEnabled) {
|
|
307
|
+
try {
|
|
308
|
+
setEnableState(nativeId, false);
|
|
309
|
+
}
|
|
310
|
+
catch (rollbackError) {
|
|
311
|
+
rollbackErrors.push(rollbackError);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else if (enableStateTouched) {
|
|
316
|
+
try {
|
|
317
|
+
setEnableState(nativeId, previousPlist === undefined || previousEnabled);
|
|
318
|
+
}
|
|
319
|
+
catch (rollbackError) {
|
|
320
|
+
rollbackErrors.push(rollbackError);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function snapshotLaunchdBindings(ids, context) {
|
|
325
|
+
const requestedKeys = new Set(ids.map(schedulerNativeArtifactKey));
|
|
326
|
+
const namespace = inspectStableLaunchdNamespace(ids, context);
|
|
327
|
+
const matchingEntries = namespace.entries.filter((entry) => requestedKeys.has(schedulerNativeArtifactKey(entry.nativeId)));
|
|
328
|
+
const matchedKeys = new Set(matchingEntries.map((entry) => schedulerNativeArtifactKey(entry.nativeId)));
|
|
329
|
+
const entries = [
|
|
330
|
+
...matchingEntries.map((entry) => Object.freeze({
|
|
331
|
+
id: entry.nativeId,
|
|
332
|
+
...(entry.plist !== undefined ? { plist: entry.plist } : {}),
|
|
333
|
+
loaded: entry.loaded,
|
|
334
|
+
enabled: entry.enabled,
|
|
335
|
+
})),
|
|
336
|
+
...ids
|
|
337
|
+
.filter((id) => !matchedKeys.has(schedulerNativeArtifactKey(id)))
|
|
338
|
+
.map((id) => Object.freeze({ id, loaded: false, enabled: true })),
|
|
339
|
+
];
|
|
340
|
+
const artifacts = matchingEntries.map((entry) => entry.artifact);
|
|
341
|
+
return Object.freeze({
|
|
342
|
+
kind: LAUNCHD_SNAPSHOT,
|
|
343
|
+
nativeIds: Object.freeze([...ids]),
|
|
344
|
+
artifacts: Object.freeze(artifacts),
|
|
345
|
+
entries: Object.freeze(entries),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function restoreLaunchdBindings(snapshot, context, expectedCurrent) {
|
|
349
|
+
if (!isLaunchdBindingSnapshot(snapshot)) {
|
|
350
|
+
throw new ConfigError("Invalid launchd scheduler snapshot.", "INVALID_CONFIG_FILE");
|
|
351
|
+
}
|
|
352
|
+
const preflightErrors = [];
|
|
353
|
+
const duplicateSnapshotKeys = new Set();
|
|
354
|
+
const requestedIdsByKey = new Map();
|
|
355
|
+
for (const nativeId of snapshot.nativeIds) {
|
|
356
|
+
const key = schedulerNativeArtifactKey(nativeId);
|
|
357
|
+
const priorId = requestedIdsByKey.get(key);
|
|
358
|
+
if (priorId !== undefined) {
|
|
359
|
+
duplicateSnapshotKeys.add(key);
|
|
360
|
+
preflightErrors.push(new ConfigError(`Launchd scheduler snapshot requests normalized duplicate artifacts ${JSON.stringify(priorId)} and ${JSON.stringify(nativeId)}; refusing restore.`, "INVALID_CONFIG_FILE"));
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
requestedIdsByKey.set(key, nativeId);
|
|
250
364
|
}
|
|
365
|
+
const snapshotEntryIdsByKey = new Map();
|
|
366
|
+
for (const entry of snapshot.entries) {
|
|
367
|
+
const key = schedulerNativeArtifactKey(entry.id);
|
|
368
|
+
const priorId = snapshotEntryIdsByKey.get(key);
|
|
369
|
+
if (priorId !== undefined) {
|
|
370
|
+
if (duplicateSnapshotKeys.has(key))
|
|
371
|
+
continue;
|
|
372
|
+
duplicateSnapshotKeys.add(key);
|
|
373
|
+
preflightErrors.push(new ConfigError(`Launchd scheduler snapshot contains normalized duplicate artifacts ${JSON.stringify(priorId)} and ${JSON.stringify(entry.id)}; refusing restore.`, "INVALID_CONFIG_FILE"));
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
snapshotEntryIdsByKey.set(key, entry.id);
|
|
377
|
+
}
|
|
378
|
+
let rollbackInventory;
|
|
251
379
|
try {
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
id,
|
|
255
|
-
signature: normalizeSignature(xml),
|
|
256
|
-
...metadata,
|
|
257
|
-
};
|
|
380
|
+
rollbackInventory = inspectStableLaunchdNamespace(snapshot.nativeIds, context).inspection;
|
|
258
381
|
}
|
|
259
|
-
catch {
|
|
260
|
-
|
|
382
|
+
catch (error) {
|
|
383
|
+
preflightErrors.push(error);
|
|
384
|
+
}
|
|
385
|
+
if (preflightErrors.length > 0) {
|
|
386
|
+
throw new AggregateError(preflightErrors, `Failed to preflight ${preflightErrors.length} launchd scheduler restore operation(s).`);
|
|
387
|
+
}
|
|
388
|
+
const expectationsById = new Map();
|
|
389
|
+
const expectationIdsByKey = new Map();
|
|
390
|
+
if (expectedCurrent) {
|
|
391
|
+
for (const expected of expectedCurrent) {
|
|
392
|
+
const key = schedulerNativeArtifactKey(expected.nativeId);
|
|
393
|
+
const priorExact = expectationsById.get(expected.nativeId);
|
|
394
|
+
const priorKeyId = expectationIdsByKey.get(key);
|
|
395
|
+
if (priorExact !== undefined || priorKeyId !== undefined) {
|
|
396
|
+
preflightErrors.push(new ConfigError(`Launchd rollback expectations contain duplicate normalized artifact ${JSON.stringify(expected.nativeId)}.`, "INVALID_CONFIG_FILE"));
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
expectationsById.set(expected.nativeId, expected);
|
|
400
|
+
expectationIdsByKey.set(key, expected.nativeId);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const plannedEntries = [];
|
|
404
|
+
const coveredExpectationIds = new Set();
|
|
405
|
+
if (rollbackInventory) {
|
|
406
|
+
for (const entry of snapshot.entries) {
|
|
407
|
+
if (duplicateSnapshotKeys.has(schedulerNativeArtifactKey(entry.id)))
|
|
408
|
+
continue;
|
|
409
|
+
try {
|
|
410
|
+
let current;
|
|
411
|
+
if (expectedCurrent) {
|
|
412
|
+
const expected = expectationsById.get(entry.id);
|
|
413
|
+
if (!expected) {
|
|
414
|
+
throw new ConfigError(`Missing launchd rollback expectation for ${JSON.stringify(entry.id)}.`, "INVALID_CONFIG_FILE");
|
|
415
|
+
}
|
|
416
|
+
coveredExpectationIds.add(entry.id);
|
|
417
|
+
current = assertSchedulerRollbackArtifactCardinality(rollbackInventory.artifacts, expected);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
const currentCount = rollbackInventory.artifacts.filter((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === schedulerNativeArtifactKey(entry.id)).length;
|
|
421
|
+
current = assertSchedulerNativeArtifactCardinality(rollbackInventory.artifacts, entry.id, currentCount === 0 ? 0 : 1);
|
|
422
|
+
if (current !== undefined && current.nativeId !== entry.id) {
|
|
423
|
+
throw new ConfigError(`Launchd scheduler artifact ${JSON.stringify(entry.id)} changed to case- or suffix-equivalent spelling ${JSON.stringify(current.nativeId)} before restore.`, "INVALID_CONFIG_FILE");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (entry.loaded && entry.plist === undefined) {
|
|
427
|
+
assertUnrestorableLaunchdSnapshotUnchanged(entry, rollbackInventory.artifacts);
|
|
428
|
+
}
|
|
429
|
+
plannedEntries.push(Object.freeze({
|
|
430
|
+
entry,
|
|
431
|
+
immediateExpectation: launchdRollbackExpectationFromArtifact(entry.id, current),
|
|
432
|
+
noOp: entry.loaded && entry.plist === undefined,
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
preflightErrors.push(error);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (expectedCurrent) {
|
|
441
|
+
for (const expected of expectedCurrent) {
|
|
442
|
+
if (!coveredExpectationIds.has(expected.nativeId)) {
|
|
443
|
+
preflightErrors.push(new ConfigError(`Unexpected launchd rollback expectation for ${JSON.stringify(expected.nativeId)} is not covered by the snapshot.`, "INVALID_CONFIG_FILE"));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (preflightErrors.length > 0) {
|
|
448
|
+
throw new AggregateError(preflightErrors, `Failed to preflight ${preflightErrors.length} launchd scheduler restore operation(s).`);
|
|
449
|
+
}
|
|
450
|
+
const errors = [];
|
|
451
|
+
for (const planned of plannedEntries) {
|
|
452
|
+
if (planned.noOp)
|
|
453
|
+
continue;
|
|
454
|
+
try {
|
|
455
|
+
const immediateInventory = inspectStableLaunchdNamespace(snapshot.nativeIds, context).inspection;
|
|
456
|
+
assertSchedulerRollbackArtifactCardinality(immediateInventory.artifacts, planned.immediateExpectation);
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
errors.push(error);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
restoreLaunchdBindingEntry(planned.entry, context, errors);
|
|
463
|
+
}
|
|
464
|
+
if (errors.length > 0) {
|
|
465
|
+
throw new AggregateError(errors, `Failed to completely restore ${errors.length} launchd scheduler operation(s).`);
|
|
261
466
|
}
|
|
262
467
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
468
|
+
function launchdRollbackExpectationFromArtifact(nativeId, current) {
|
|
469
|
+
if (current === undefined) {
|
|
470
|
+
return Object.freeze({ nativeId, allowed: Object.freeze([{ state: "absent" }]) });
|
|
471
|
+
}
|
|
472
|
+
if (current.nativeId !== nativeId || current.fingerprint === undefined) {
|
|
473
|
+
throw new ConfigError(`Launchd scheduler artifact ${JSON.stringify(nativeId)} has no exact restorable fingerprint.`, "INVALID_CONFIG_FILE");
|
|
474
|
+
}
|
|
475
|
+
return Object.freeze({
|
|
476
|
+
nativeId,
|
|
477
|
+
allowed: Object.freeze([
|
|
478
|
+
Object.freeze({
|
|
479
|
+
state: "present",
|
|
480
|
+
...(current.bindingId !== undefined ? { bindingId: current.bindingId } : {}),
|
|
481
|
+
...(current.invocation !== undefined ? { invocation: current.invocation } : {}),
|
|
482
|
+
fingerprint: current.fingerprint,
|
|
483
|
+
}),
|
|
484
|
+
]),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
function assertUnrestorableLaunchdSnapshotUnchanged(entry, currentArtifacts) {
|
|
488
|
+
const current = assertSchedulerNativeArtifactCardinality(currentArtifacts, entry.id, 1);
|
|
489
|
+
const expectedFingerprint = launchdMissingPlistArtifact(entry.id, entry.enabled, entry.loaded).fingerprint;
|
|
490
|
+
if (current?.nativeId !== entry.id ||
|
|
491
|
+
current.invocation !== undefined ||
|
|
492
|
+
current.bindingId !== undefined ||
|
|
493
|
+
current.fingerprint !== expectedFingerprint) {
|
|
494
|
+
throw new ConfigError(`Loaded launchd artifact ${JSON.stringify(entry.id)} has no restorable plist and changed after its snapshot; refusing mutation.`, "INVALID_CONFIG_FILE");
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function inspectStableLaunchdNamespace(seedIds, context) {
|
|
498
|
+
const first = captureLaunchdNamespacePass(seedIds, context);
|
|
499
|
+
const second = captureLaunchdNamespacePass(seedIds, context);
|
|
500
|
+
if (first.stabilityKey !== second.stabilityKey) {
|
|
501
|
+
throw new ConfigError("The launchd AKM service namespace changed while scheduler state was being stabilized.", "INVALID_CONFIG_FILE");
|
|
502
|
+
}
|
|
503
|
+
return second.namespace;
|
|
504
|
+
}
|
|
505
|
+
function captureLaunchdNamespacePass(seedIds, context) {
|
|
506
|
+
const domain = context.exec.run(["launchctl", "print", `gui/${context.exec.uid()}`]);
|
|
507
|
+
if (domain.status !== 0) {
|
|
508
|
+
throw new ConfigError(`launchctl failed to enumerate the loaded user domain during scheduler state inspection: ${domain.stderr || domain.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
|
|
509
|
+
}
|
|
510
|
+
const loadedLabels = parseLaunchdLoadedLabels(domain.stdout);
|
|
511
|
+
if (loadedLabels === undefined) {
|
|
512
|
+
throw new ConfigError("launchctl returned an unsafe, unsupported, or oversized loaded-service inventory during scheduler state inspection.", "INVALID_CONFIG_FILE");
|
|
513
|
+
}
|
|
514
|
+
const disabledLabels = readDisabledLabels(context.exec);
|
|
515
|
+
if (disabledLabels === undefined) {
|
|
516
|
+
throw new ConfigError("launchctl print-disabled failed during scheduler state inspection.", "INVALID_CONFIG_FILE");
|
|
517
|
+
}
|
|
518
|
+
const akmDisabledLabels = [...disabledLabels].filter((label) => label.startsWith(LAUNCHD_LABEL_PREFIX)).sort();
|
|
519
|
+
const plistEntries = [];
|
|
520
|
+
if (context.fsLike.exists(context.agentsDir)) {
|
|
521
|
+
for (const file of context.fsLike.list(context.agentsDir).sort()) {
|
|
522
|
+
if (!file.startsWith(LAUNCHD_LABEL_PREFIX) || !file.endsWith(".plist"))
|
|
523
|
+
continue;
|
|
524
|
+
const nativeId = file.slice(LAUNCHD_LABEL_PREFIX.length, -".plist".length);
|
|
525
|
+
plistEntries.push(Object.freeze([nativeId, context.fsLike.readFile(context.plistPath(nativeId))]));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const ids = new Set(seedIds);
|
|
529
|
+
for (const [nativeId] of plistEntries)
|
|
530
|
+
ids.add(nativeId);
|
|
531
|
+
for (const serviceLabel of loadedLabels)
|
|
532
|
+
ids.add(serviceLabel.slice(LAUNCHD_LABEL_PREFIX.length));
|
|
533
|
+
for (const serviceLabel of akmDisabledLabels)
|
|
534
|
+
ids.add(serviceLabel.slice(LAUNCHD_LABEL_PREFIX.length));
|
|
535
|
+
if (ids.size > MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES) {
|
|
536
|
+
throw new ConfigError(`launchd AKM scheduler inventory exceeds ${MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES} namespace entries.`, "INVALID_CONFIG_FILE");
|
|
537
|
+
}
|
|
538
|
+
const plistByNativeId = new Map(plistEntries);
|
|
539
|
+
const entries = [];
|
|
540
|
+
for (const nativeId of [...ids].sort()) {
|
|
541
|
+
const serviceLabel = context.label(nativeId);
|
|
542
|
+
const plist = plistByNativeId.get(nativeId);
|
|
543
|
+
const enabled = !disabledLabels.has(serviceLabel);
|
|
544
|
+
const loaded = loadedLabels.has(serviceLabel);
|
|
545
|
+
if (plist === undefined && enabled && !loaded)
|
|
546
|
+
continue;
|
|
547
|
+
const artifact = plist === undefined
|
|
548
|
+
? launchdMissingPlistArtifact(nativeId, enabled, loaded)
|
|
549
|
+
: launchdArtifact(nativeId, plist, enabled, loaded);
|
|
550
|
+
entries.push(Object.freeze({ nativeId, ...(plist !== undefined ? { plist } : {}), enabled, loaded, artifact }));
|
|
551
|
+
}
|
|
552
|
+
const artifacts = entries.map((entry) => entry.artifact);
|
|
553
|
+
const installed = [];
|
|
554
|
+
for (const entry of entries) {
|
|
555
|
+
if (entry.plist === undefined)
|
|
556
|
+
continue;
|
|
557
|
+
const parsed = extractPlistInvocation(entry.plist);
|
|
558
|
+
if (!parsed)
|
|
559
|
+
continue;
|
|
560
|
+
installed.push(withInstalledInvocation({
|
|
561
|
+
id: schedulerLogicalBindingId(entry.nativeId, parsed.invocation),
|
|
562
|
+
...(entry.loaded ? { signature: entry.artifact.fingerprint } : {}),
|
|
563
|
+
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
564
|
+
binding: parsed.binding,
|
|
565
|
+
contextPath: parsed.contextPath,
|
|
566
|
+
}, parsed.invocation, entry.nativeId));
|
|
567
|
+
}
|
|
568
|
+
return Object.freeze({
|
|
569
|
+
namespace: Object.freeze({
|
|
570
|
+
inspection: Object.freeze({ installed: Object.freeze(installed), artifacts: Object.freeze(artifacts) }),
|
|
571
|
+
entries: Object.freeze(entries),
|
|
572
|
+
}),
|
|
573
|
+
stabilityKey: JSON.stringify({
|
|
574
|
+
loadedLabels: [...loadedLabels].sort(),
|
|
575
|
+
disabledLabels: akmDisabledLabels,
|
|
576
|
+
plistEntries,
|
|
577
|
+
}),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
function restoreLaunchdBindingEntry(entry, context, errors) {
|
|
581
|
+
try {
|
|
582
|
+
runOrThrow(context.exec, ["launchctl", "bootout", context.target(entry.id)], {
|
|
583
|
+
isOk: (result) => result.status === 0 || isServiceNotFoundResult(result),
|
|
584
|
+
message: (result) => `launchctl bootout failed while restoring task "${entry.id}": ${result.stderr || result.stdout || "no output"}.`,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
errors.push(error);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const file = context.plistPath(entry.id);
|
|
592
|
+
let priorFileRestored = false;
|
|
593
|
+
try {
|
|
594
|
+
if (entry.plist === undefined) {
|
|
595
|
+
if (context.fsLike.exists(file))
|
|
596
|
+
context.fsLike.removeFile(file);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
context.fsLike.ensureDir(context.agentsDir);
|
|
600
|
+
context.fsLike.writeFile(file, entry.plist);
|
|
601
|
+
}
|
|
602
|
+
priorFileRestored = true;
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
errors.push(error);
|
|
606
|
+
}
|
|
607
|
+
if (entry.loaded && entry.plist !== undefined && priorFileRestored) {
|
|
608
|
+
let overrideCleared = false;
|
|
609
|
+
try {
|
|
610
|
+
// A disable override survives bootout. Clear it before bootstrap or
|
|
611
|
+
// launchd can refuse to register the exact prior loaded definition.
|
|
612
|
+
context.setEnableState(entry.id, true);
|
|
613
|
+
overrideCleared = true;
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
errors.push(error);
|
|
617
|
+
}
|
|
618
|
+
if (overrideCleared) {
|
|
619
|
+
try {
|
|
620
|
+
runOrThrow(context.exec, ["launchctl", "bootstrap", `gui/${context.exec.uid()}`, file], {
|
|
621
|
+
message: (result) => `launchctl bootstrap failed while restoring task "${entry.id}": ${result.stderr || result.stdout || "no output"}.`,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
errors.push(error);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
try {
|
|
630
|
+
// Apply the prior override last even when an earlier step failed; this is
|
|
631
|
+
// both the exact state ordering and the best available partial recovery.
|
|
632
|
+
context.setEnableState(entry.id, entry.enabled);
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
errors.push(error);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function isLaunchdBindingSnapshot(value) {
|
|
639
|
+
return (typeof value === "object" &&
|
|
640
|
+
value !== null &&
|
|
641
|
+
value.kind === LAUNCHD_SNAPSHOT &&
|
|
642
|
+
Array.isArray(value.entries));
|
|
643
|
+
}
|
|
644
|
+
function inspectLaunchdState(context) {
|
|
645
|
+
return inspectStableLaunchdNamespace([], context).inspection;
|
|
646
|
+
}
|
|
647
|
+
function assertLaunchdDirectMutationOwner(artifacts, nativeId, task) {
|
|
648
|
+
const key = schedulerNativeArtifactKey(nativeId);
|
|
649
|
+
const count = artifacts.filter((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === key).length;
|
|
650
|
+
const artifact = assertSchedulerNativeArtifactCardinality(artifacts, nativeId, count === 0 ? 0 : 1);
|
|
651
|
+
if (artifact)
|
|
652
|
+
assertSchedulerNativeArtifactOwner(artifact.nativeId, task, artifact.invocation);
|
|
653
|
+
return artifact;
|
|
654
|
+
}
|
|
655
|
+
function assertLaunchdDirectRemovalOwner(artifacts, nativeId) {
|
|
656
|
+
const key = schedulerNativeArtifactKey(nativeId);
|
|
657
|
+
const count = artifacts.filter((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === key).length;
|
|
658
|
+
const artifact = assertSchedulerNativeArtifactCardinality(artifacts, nativeId, count === 0 ? 0 : 1);
|
|
659
|
+
if (!artifact)
|
|
660
|
+
return undefined;
|
|
661
|
+
if (artifact.nativeId !== nativeId || artifact.invocation === undefined) {
|
|
662
|
+
throw new ConfigError(`launchd artifact ${JSON.stringify(artifact.nativeId)} is not the exact proven requested owner ${JSON.stringify(nativeId)}; refusing direct removal.`, "INVALID_CONFIG_FILE");
|
|
663
|
+
}
|
|
664
|
+
return artifact;
|
|
665
|
+
}
|
|
666
|
+
function launchdRollbackExpectationForCurrent(nativeId, context, priorArtifact) {
|
|
667
|
+
const namespace = inspectStableLaunchdNamespace([nativeId], context);
|
|
668
|
+
const key = schedulerNativeArtifactKey(nativeId);
|
|
669
|
+
const count = namespace.inspection.artifacts.filter((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === key).length;
|
|
670
|
+
const artifact = assertSchedulerNativeArtifactCardinality(namespace.inspection.artifacts, nativeId, count === 0 ? 0 : 1);
|
|
671
|
+
if (!artifact) {
|
|
672
|
+
return Object.freeze({ nativeId, allowed: Object.freeze([{ state: "absent" }]) });
|
|
673
|
+
}
|
|
674
|
+
if (artifact.fingerprint === undefined ||
|
|
675
|
+
artifact.invocation === undefined ||
|
|
676
|
+
priorArtifact?.invocation === undefined ||
|
|
677
|
+
!sameStringArray(artifact.invocation, priorArtifact.invocation)) {
|
|
678
|
+
throw new ConfigError(`launchd artifact ${JSON.stringify(nativeId)} has no exact transaction-owned rollback fingerprint.`, "INVALID_CONFIG_FILE");
|
|
679
|
+
}
|
|
680
|
+
return Object.freeze({
|
|
681
|
+
nativeId,
|
|
682
|
+
allowed: Object.freeze([
|
|
683
|
+
Object.freeze({
|
|
684
|
+
state: "present",
|
|
685
|
+
...(artifact.bindingId !== undefined ? { bindingId: artifact.bindingId } : {}),
|
|
686
|
+
invocation: Object.freeze([...artifact.invocation]),
|
|
687
|
+
fingerprint: artifact.fingerprint,
|
|
688
|
+
}),
|
|
689
|
+
]),
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
function sameStringArray(left, right) {
|
|
693
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
694
|
+
}
|
|
695
|
+
function errorMessage(error) {
|
|
696
|
+
return error instanceof Error ? error.message : String(error);
|
|
697
|
+
}
|
|
698
|
+
function launchdMissingPlistArtifact(nativeId, enabled, loaded) {
|
|
699
|
+
const artifact = { nativeId };
|
|
700
|
+
Object.defineProperty(artifact, "fingerprint", {
|
|
701
|
+
value: `launchd:missing-plist:enabled=${enabled}:loaded=${loaded}`,
|
|
702
|
+
});
|
|
703
|
+
return artifact;
|
|
704
|
+
}
|
|
705
|
+
function launchdArtifact(nativeId, raw, enabled, loaded) {
|
|
706
|
+
const parsed = extractPlistInvocation(raw);
|
|
707
|
+
const owner = parsed ? schedulerLogicalBindingOwner(nativeId, parsed.invocation) : undefined;
|
|
708
|
+
const artifact = parsed
|
|
709
|
+
? {
|
|
710
|
+
nativeId,
|
|
711
|
+
...(owner !== undefined ? { bindingId: owner } : {}),
|
|
712
|
+
invocation: Object.freeze([...parsed.invocation]),
|
|
713
|
+
}
|
|
714
|
+
: { nativeId };
|
|
715
|
+
if (enabled !== undefined && loaded !== undefined) {
|
|
716
|
+
Object.defineProperty(artifact, "fingerprint", { value: launchdFingerprint(raw, enabled, loaded) });
|
|
717
|
+
}
|
|
718
|
+
return artifact;
|
|
719
|
+
}
|
|
720
|
+
function launchdFingerprint(raw, enabled, loaded) {
|
|
721
|
+
const signed = raw.replace(/<!-- akm-enabled:(?:true|false) -->/, `<!-- akm-enabled:${enabled} -->`);
|
|
722
|
+
return `${normalizeSignature(signed)}:loaded=${loaded}`;
|
|
723
|
+
}
|
|
724
|
+
function withInstalledInvocation(ref, invocation, nativeId) {
|
|
725
|
+
Object.defineProperty(ref, "nativeId", { value: nativeId });
|
|
726
|
+
Object.defineProperty(ref, "invocation", { value: Object.freeze([...invocation]) });
|
|
727
|
+
return ref;
|
|
728
|
+
}
|
|
729
|
+
function launchdDefaultContextPath(options, scheduledContext) {
|
|
730
|
+
const pathEnv = options.envPath === false
|
|
731
|
+
? undefined
|
|
732
|
+
: typeof options.envPath === "string"
|
|
733
|
+
? options.envPath
|
|
734
|
+
: (process.env.PATH ?? "");
|
|
735
|
+
return schedulerContextPath(schedulerContextDescriptor(scheduledContext, pathEnv ?? ""));
|
|
736
|
+
}
|
|
737
|
+
function setLaunchdEnableState(exec, nativeTarget, enabled) {
|
|
738
|
+
const verb = enabled ? "enable" : "disable";
|
|
739
|
+
runOrThrow(exec, ["launchctl", verb, nativeTarget], {
|
|
740
|
+
message: (result) => `launchctl ${verb} failed: ${result.stderr || result.stdout || "no output"}.`,
|
|
741
|
+
});
|
|
269
742
|
}
|
|
270
743
|
export function extractPlistInvocation(xml) {
|
|
271
744
|
const block = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
|
|
272
745
|
if (!block)
|
|
273
746
|
return undefined;
|
|
274
747
|
const args = [...block[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => decodeXmlEntities(m[1]));
|
|
275
|
-
return
|
|
748
|
+
return parseScheduledBindingArgv(args);
|
|
276
749
|
}
|
|
277
750
|
function decodeXmlEntities(value) {
|
|
278
751
|
return value
|
|
@@ -283,17 +756,21 @@ function decodeXmlEntities(value) {
|
|
|
283
756
|
.replaceAll("&", "&");
|
|
284
757
|
}
|
|
285
758
|
// ── XML builder (exported for tests) ────────────────────────────────────────
|
|
286
|
-
export function buildPlistXml(task, akmArgv, logDir, contextPath,
|
|
287
|
-
const spec = parseSchedule(task.
|
|
759
|
+
export function buildPlistXml(task, akmArgv, logDir, contextPath, _target) {
|
|
760
|
+
const spec = parseSchedule(task.cron, "launchd");
|
|
288
761
|
const trigger = translateToLaunchd(spec);
|
|
289
|
-
const invocation =
|
|
762
|
+
const invocation = buildScheduledBindingInvocation(akmArgv, contextPath, task.invocation);
|
|
290
763
|
const argv = invocation.argv;
|
|
291
764
|
const programArgs = argv.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
292
|
-
const
|
|
765
|
+
const nativeId = schedulerBindingNativeId(task);
|
|
766
|
+
const logPath = path.join(logDir, `${nativeId}.log`);
|
|
293
767
|
const triggerXml = renderLaunchdTrigger(trigger);
|
|
768
|
+
const executionEvidence = task.executionEvidenceDigest === undefined
|
|
769
|
+
? ""
|
|
770
|
+
: ` <!-- akm-workflow-evidence:${assertSchedulerExecutionEvidenceDigest(task.executionEvidenceDigest)} -->\n`;
|
|
294
771
|
const xml = launchdTemplate
|
|
295
|
-
.replace("<dict>\n", `<dict>\n <!-- akm-enabled:${task.enabled} -->\n`)
|
|
296
|
-
.replace("{{LABEL}}", LAUNCHD_LABEL_PREFIX + escapeXml(
|
|
772
|
+
.replace("<dict>\n", `<dict>\n <!-- akm-enabled:${task.enabled} -->\n${executionEvidence}`)
|
|
773
|
+
.replace("{{LABEL}}", LAUNCHD_LABEL_PREFIX + escapeXml(nativeId))
|
|
297
774
|
.replace("{{PROGRAM_ARGS}}", programArgs)
|
|
298
775
|
.replaceAll("{{LOG_PATH}}", escapeXml(logPath))
|
|
299
776
|
.replace("{{ENV_VARS}}", "")
|
|
@@ -340,6 +817,114 @@ function renderCalendar(calendar, indent) {
|
|
|
340
817
|
function normalizeSignature(xml) {
|
|
341
818
|
return xml.replace(/\r\n/g, "\n").trim();
|
|
342
819
|
}
|
|
820
|
+
/** Parse only proven loaded-service labels from bounded launchctl domain/list output. */
|
|
821
|
+
export function parseLaunchdLoadedLabels(output) {
|
|
822
|
+
if (Buffer.byteLength(output, "utf8") > MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES || hasUnsafeLaunchdControlCharacter(output)) {
|
|
823
|
+
return undefined;
|
|
824
|
+
}
|
|
825
|
+
const lines = output.replace(/\r\n?/gu, "\n").split("\n");
|
|
826
|
+
const labels = new Set();
|
|
827
|
+
let akmEntryCount = 0;
|
|
828
|
+
const add = (label) => {
|
|
829
|
+
if (!LAUNCHD_AKM_LABEL_RE.test(label))
|
|
830
|
+
return false;
|
|
831
|
+
akmEntryCount += 1;
|
|
832
|
+
if (akmEntryCount > MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES || labels.has(label))
|
|
833
|
+
return false;
|
|
834
|
+
labels.add(label);
|
|
835
|
+
return true;
|
|
836
|
+
};
|
|
837
|
+
const firstContent = lines.findIndex((line) => line.trim() !== "");
|
|
838
|
+
if (firstContent >= 0 && /^PID\s+Status\s+Label$/iu.test(lines[firstContent].trim())) {
|
|
839
|
+
for (const line of lines.slice(firstContent + 1)) {
|
|
840
|
+
if (!line.trim())
|
|
841
|
+
continue;
|
|
842
|
+
const row = /^\s*(?:-|\d+)\s+-?\d+\s+(\S+)\s*$/u.exec(line);
|
|
843
|
+
if (!row?.[1])
|
|
844
|
+
return undefined;
|
|
845
|
+
if (row[1].startsWith(LAUNCHD_LABEL_PREFIX) && !add(row[1]))
|
|
846
|
+
return undefined;
|
|
847
|
+
}
|
|
848
|
+
return labels;
|
|
849
|
+
}
|
|
850
|
+
const firstLine = firstContent < 0 ? undefined : lines[firstContent];
|
|
851
|
+
if (firstLine === undefined || !/^\s*(?:gui|user)\/\d+\s*=\s*\{\s*$/u.test(firstLine))
|
|
852
|
+
return undefined;
|
|
853
|
+
let lastContent = lines.length - 1;
|
|
854
|
+
while (lastContent >= 0 && !lines[lastContent]?.trim())
|
|
855
|
+
lastContent -= 1;
|
|
856
|
+
const lastLine = lines[lastContent];
|
|
857
|
+
if (lastContent <= firstContent || lastLine === undefined || !/^\s*\}\s*$/u.test(lastLine))
|
|
858
|
+
return undefined;
|
|
859
|
+
let depth = 1;
|
|
860
|
+
let servicesSeen = false;
|
|
861
|
+
let insideServices = false;
|
|
862
|
+
for (let index = firstContent + 1; index <= lastContent; index += 1) {
|
|
863
|
+
const line = lines[index];
|
|
864
|
+
if (index === lastContent) {
|
|
865
|
+
if (insideServices || depth !== 1)
|
|
866
|
+
return undefined;
|
|
867
|
+
depth = 0;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
const servicesBlock = /^\s*services\s*=\s*\{\s*$/u.test(line);
|
|
871
|
+
const emptyServicesBlock = /^\s*services\s*=\s*\{\s*\}\s*$/u.test(line);
|
|
872
|
+
if (servicesBlock || emptyServicesBlock) {
|
|
873
|
+
if (servicesSeen || insideServices || depth !== 1)
|
|
874
|
+
return undefined;
|
|
875
|
+
servicesSeen = true;
|
|
876
|
+
if (servicesBlock) {
|
|
877
|
+
insideServices = true;
|
|
878
|
+
depth += 1;
|
|
879
|
+
}
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (/^\s*services\s*=/u.test(line))
|
|
883
|
+
return undefined;
|
|
884
|
+
if (insideServices && depth === 2) {
|
|
885
|
+
if (!line.trim())
|
|
886
|
+
continue;
|
|
887
|
+
if (/^\s*\}\s*$/u.test(line)) {
|
|
888
|
+
insideServices = false;
|
|
889
|
+
depth -= 1;
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
const assignment = /^\s*-?\d+\s*=\s*"?([^"\s{}=]+)"?\s*$/u.exec(line);
|
|
893
|
+
const domainTable = /^\s*(?:-|\d+)\s+(?:-|-?\d+)\s+(\S+)\s*$/u.exec(line);
|
|
894
|
+
const dictionary = /^\s*"?([^"\s{}=]+)"?\s*=\s*\{\s*$/u.exec(line);
|
|
895
|
+
const candidate = assignment?.[1] ?? domainTable?.[1] ?? dictionary?.[1];
|
|
896
|
+
if (!candidate)
|
|
897
|
+
return undefined;
|
|
898
|
+
if (candidate.startsWith(LAUNCHD_LABEL_PREFIX) && !add(candidate))
|
|
899
|
+
return undefined;
|
|
900
|
+
if (dictionary)
|
|
901
|
+
depth += 1;
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (line.includes(LAUNCHD_LABEL_PREFIX))
|
|
905
|
+
return undefined;
|
|
906
|
+
depth += countCharacter(line, "{") - countCharacter(line, "}");
|
|
907
|
+
if (depth < 1 || (insideServices && depth < 2))
|
|
908
|
+
return undefined;
|
|
909
|
+
}
|
|
910
|
+
return servicesSeen && depth === 0 ? labels : undefined;
|
|
911
|
+
}
|
|
912
|
+
function countCharacter(value, needle) {
|
|
913
|
+
let count = 0;
|
|
914
|
+
for (const character of value)
|
|
915
|
+
if (character === needle)
|
|
916
|
+
count += 1;
|
|
917
|
+
return count;
|
|
918
|
+
}
|
|
919
|
+
function hasUnsafeLaunchdControlCharacter(output) {
|
|
920
|
+
for (let index = 0; index < output.length; index += 1) {
|
|
921
|
+
const code = output.charCodeAt(index);
|
|
922
|
+
if (code <= 8 || code === 11 || code === 12 || (code >= 14 && code <= 31) || (code >= 127 && code <= 159)) {
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
343
928
|
function readDisabledLabels(exec) {
|
|
344
929
|
try {
|
|
345
930
|
const result = exec.run(["launchctl", "print-disabled", `gui/${exec.uid()}`]);
|