akm-cli 0.9.15 → 0.9.16-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 +52 -0
- package/dist/assets/hints/cli-hints-full.md +13 -6
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
- package/dist/cli/retired-commands.js +0 -2
- package/dist/commands/env/env-binding.js +4 -4
- package/dist/commands/env/env-cli.js +3 -3
- package/dist/commands/improve/improve-cli.js +19 -14
- package/dist/commands/improve/reflect.js +23 -2
- package/dist/commands/lint/base-linter.js +9 -0
- package/dist/commands/lint/env-key-rules.js +2 -2
- package/dist/commands/proposal/propose.js +15 -1
- package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
- package/dist/commands/proposal/validators/proposal-validators.js +5 -4
- package/dist/commands/read/search.js +33 -4
- package/dist/commands/read/show.js +21 -2
- package/dist/commands/registry-cli.js +5 -5
- package/dist/commands/sources/add-cli.js +59 -16
- package/dist/commands/sources/bundle-cli.js +35 -11
- package/dist/commands/sources/bundle-config-ops.js +30 -0
- package/dist/commands/sources/dangerous-env-audit.js +4 -4
- package/dist/commands/sources/installed-stashes.js +43 -28
- package/dist/commands/sources/source-add.js +33 -17
- package/dist/commands/sources/source-manage.js +34 -12
- package/dist/commands/sources/stash-skeleton.js +6 -3
- package/dist/commands/tasks/explain.js +4 -1
- package/dist/commands/tasks/tasks-cli.js +31 -9
- package/dist/commands/tasks/tasks.js +239 -194
- package/dist/commands/tasks/validate.js +20 -32
- package/dist/core/activation-policy.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +5 -0
- package/dist/core/adapter/execution-source.js +10 -29
- package/dist/core/config/config-schema.js +64 -8
- package/dist/core/config/config-sources.js +96 -2
- package/dist/core/config/config.js +190 -24
- package/dist/core/config/legacy-source-shape-shim.js +9 -0
- package/dist/core/config/schema/execution.js +23 -0
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/scheduler.js +20 -0
- package/dist/core/config/schema/search.js +1 -1
- package/dist/core/config/schema/sources-bundles.js +32 -1
- package/dist/core/content-safety.js +52 -0
- package/dist/core/maintenance-barrier.js +6 -6
- package/dist/core/type-presentation.js +1 -1
- package/dist/core/write-source.js +13 -8
- package/dist/indexer/bundle-identity-guard.js +45 -8
- package/dist/indexer/indexer.js +1 -1
- package/dist/indexer/materialize-embeddings.js +15 -1
- package/dist/indexer/search/search-source.js +29 -11
- package/dist/integrations/agent/execution-lowering.js +3 -2
- package/dist/integrations/agent/execution-preparation.js +32 -1
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/integrations/agent/request-lowering.js +3 -2
- package/dist/llm/client.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/registry/resolve.js +37 -10
- package/dist/scripts/akm-migrate-node.js +13644 -9894
- package/dist/scripts/akm-migrate.js +12626 -8876
- package/dist/setup/setup.js +3 -3
- package/dist/setup/steps/tasks.js +29 -36
- package/dist/sources/providers/git-install.js +17 -11
- package/dist/sources/providers/git-provider.js +12 -5
- package/dist/sources/providers/git-stash.js +38 -16
- package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
- package/dist/storage/repositories/index-vec-repository.js +100 -0
- package/dist/tasks/activation-config.js +90 -0
- package/dist/tasks/backends/cron.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -0
- package/dist/tasks/backends/schtasks.js +2 -0
- package/dist/tasks/embedded.js +4 -5
- package/dist/tasks/scheduler-binding.js +2 -2
- package/dist/tasks/scheduler-sync-preview.js +8 -1
- package/dist/tasks/scheduler-sync.js +19 -10
- package/dist/tasks/source/parse-task-source.js +10 -113
- package/dist/tasks/source/project-v4.js +2 -2
- package/dist/tasks/source/task-source-v4.js +4 -12
- package/dist/tasks/source/task-to-v3.js +4 -12
- package/dist/tasks/source/task-to-v4.js +40 -7
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.16.md +72 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
- package/docs/reference/cli.md +37 -29
- package/docs/reference/configuration.md +48 -5
- package/docs/reference/tasks.md +34 -29
- package/package.json +1 -1
- package/schemas/akm-config.json +112 -4
- package/schemas/akm-task.json +1 -2
|
@@ -8,9 +8,9 @@ import { UsageError } from "../../core/errors.js";
|
|
|
8
8
|
import { appendEvent } from "../../core/events.js";
|
|
9
9
|
import { warn } from "../../core/warn.js";
|
|
10
10
|
import { auditStashForDangerousKeys } from "./dangerous-env-audit.js";
|
|
11
|
-
import { akmRemove } from "./installed-stashes.js";
|
|
11
|
+
import { akmRemove, akmUpdate } from "./installed-stashes.js";
|
|
12
12
|
import { akmAdd } from "./source-add.js";
|
|
13
|
-
import { addStash } from "./source-manage.js";
|
|
13
|
+
import { addStash, removeStash } from "./source-manage.js";
|
|
14
14
|
// ── Shared website-options helper ──────────
|
|
15
15
|
export function buildWebsiteOptions(args) {
|
|
16
16
|
// getStringArg maps absent/blank to undefined; parsePositiveIntFlag maps
|
|
@@ -86,28 +86,41 @@ export const addCommand = defineJsonCommand({
|
|
|
86
86
|
description: "Mark a git bundle as writable so changes can be pushed back",
|
|
87
87
|
default: false,
|
|
88
88
|
},
|
|
89
|
+
before: { type: "string", description: "Insert the new bundle before this configured bundle" },
|
|
90
|
+
after: { type: "string", description: "Insert the new bundle after this configured bundle" },
|
|
91
|
+
credential: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Git HTTPS bearer credential reference ($VAR or secret://name); never a literal token",
|
|
94
|
+
},
|
|
89
95
|
"max-pages": { type: "string", description: "Maximum pages to crawl for website sources (default: 50)" },
|
|
90
96
|
"max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
|
|
91
|
-
"allow-insecure": {
|
|
97
|
+
"allow-insecure-transport": {
|
|
98
|
+
type: "boolean",
|
|
99
|
+
description: "Allow a plain HTTP source URL after explicitly accepting transport substitution risk.",
|
|
100
|
+
default: false,
|
|
101
|
+
},
|
|
102
|
+
"allow-dangerous-env-keys": {
|
|
92
103
|
type: "boolean",
|
|
93
|
-
description: "Allow
|
|
104
|
+
description: "Allow reviewed process-hijacking env keys (e.g. LD_PRELOAD, PATH) in the installed bundle.",
|
|
94
105
|
default: false,
|
|
95
106
|
},
|
|
96
107
|
},
|
|
97
108
|
async run({ args }) {
|
|
98
109
|
const ref = args.ref.trim();
|
|
99
|
-
const
|
|
100
|
-
const allowDangerousKeys =
|
|
110
|
+
const allowInsecureTransport = args["allow-insecure-transport"];
|
|
111
|
+
const allowDangerousKeys = args["allow-dangerous-env-keys"];
|
|
112
|
+
if (args.before && args.after)
|
|
113
|
+
throw new UsageError("Only one of --before or --after may be used.");
|
|
101
114
|
// --provider → declarative bundle source (URL for git/website; bare
|
|
102
|
-
// package spec for npm — R-013).
|
|
103
|
-
//
|
|
115
|
+
// package spec for npm — R-013). Git is materialized below before success;
|
|
116
|
+
// other providers remain declarative until bundle update.
|
|
104
117
|
if (args.provider) {
|
|
105
118
|
if (shouldWarnOnPlainHttp(ref)) {
|
|
106
|
-
if (!
|
|
119
|
+
if (!allowInsecureTransport) {
|
|
107
120
|
throw new UsageError("Source URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious payload. " +
|
|
108
|
-
"Use https:// or pass --allow-insecure if you have explicitly accepted the risk.", "INVALID_FLAG_VALUE", "Re-run with `--allow-insecure` only after confirming the URL is trusted.");
|
|
121
|
+
"Use https:// or pass --allow-insecure-transport if you have explicitly accepted the risk.", "INVALID_FLAG_VALUE", "Re-run with `--allow-insecure-transport` only after confirming the URL is trusted.");
|
|
109
122
|
}
|
|
110
|
-
warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious payload.");
|
|
123
|
+
warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure-transport was set; an on-path attacker could substitute a malicious payload.");
|
|
111
124
|
}
|
|
112
125
|
let parsedOptions;
|
|
113
126
|
if (args.options) {
|
|
@@ -130,7 +143,34 @@ export const addCommand = defineJsonCommand({
|
|
|
130
143
|
providerType: args.provider,
|
|
131
144
|
options: parsedOptions,
|
|
132
145
|
writable: args.writable,
|
|
146
|
+
before: args.before,
|
|
147
|
+
after: args.after,
|
|
148
|
+
credential: args.credential,
|
|
133
149
|
});
|
|
150
|
+
// Declarative git sources must be materialized and indexed before add
|
|
151
|
+
// reports success. This also makes --provider git behave consistently
|
|
152
|
+
// with inferred/registry git installs (#968/#970).
|
|
153
|
+
if (args.provider === "git" && result.added && result.entry?.name) {
|
|
154
|
+
let updated;
|
|
155
|
+
try {
|
|
156
|
+
updated = await akmUpdate({ target: result.entry.name, allowDangerousEnvKeys: allowDangerousKeys });
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
removeStash(result.entry.name);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
appendEvent({
|
|
163
|
+
eventType: "add",
|
|
164
|
+
metadata: {
|
|
165
|
+
target: ref,
|
|
166
|
+
provider: args.provider,
|
|
167
|
+
name: args.name ?? null,
|
|
168
|
+
writable: args.writable === true,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
output("add", { ...result, index: updated.index });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
134
174
|
appendEvent({
|
|
135
175
|
eventType: "add",
|
|
136
176
|
metadata: { target: ref, provider: args.provider, name: args.name ?? null, writable: args.writable === true },
|
|
@@ -139,11 +179,11 @@ export const addCommand = defineJsonCommand({
|
|
|
139
179
|
return;
|
|
140
180
|
}
|
|
141
181
|
if (shouldWarnOnPlainHttp(ref)) {
|
|
142
|
-
if (!
|
|
182
|
+
if (!allowInsecureTransport) {
|
|
143
183
|
throw new UsageError("Source URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious payload. " +
|
|
144
|
-
"Use https:// or pass --allow-insecure if you have explicitly accepted the risk.", "INVALID_FLAG_VALUE", "Re-run with `--allow-insecure` only after confirming the URL is trusted.");
|
|
184
|
+
"Use https:// or pass --allow-insecure-transport if you have explicitly accepted the risk.", "INVALID_FLAG_VALUE", "Re-run with `--allow-insecure-transport` only after confirming the URL is trusted.");
|
|
145
185
|
}
|
|
146
|
-
warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious payload.");
|
|
186
|
+
warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure-transport was set; an on-path attacker could substitute a malicious payload.");
|
|
147
187
|
}
|
|
148
188
|
const websiteOptions = buildWebsiteOptions(args);
|
|
149
189
|
const result = await akmAdd({
|
|
@@ -152,6 +192,9 @@ export const addCommand = defineJsonCommand({
|
|
|
152
192
|
options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
|
|
153
193
|
writable: args.writable,
|
|
154
194
|
adapter: args.adapter,
|
|
195
|
+
before: args.before,
|
|
196
|
+
after: args.after,
|
|
197
|
+
credential: args.credential,
|
|
155
198
|
});
|
|
156
199
|
appendEvent({
|
|
157
200
|
eventType: "add",
|
|
@@ -165,8 +208,8 @@ export const addCommand = defineJsonCommand({
|
|
|
165
208
|
// Resolve the stash root from the install result and scan any env files
|
|
166
209
|
// for dangerous env var keys. When findings are present the install is
|
|
167
210
|
// gated: TTY → interactive confirmation prompt; non-TTY without
|
|
168
|
-
// --allow-
|
|
169
|
-
//
|
|
211
|
+
// --allow-dangerous-env-keys → hard failure (exit 1). Pass the dedicated
|
|
212
|
+
// flag to skip the prompt non-interactively.
|
|
170
213
|
const installedStashRoot = result.installed?.stashRoot ??
|
|
171
214
|
(result.sourceAdded && "stashRoot" in result.sourceAdded ? result.sourceAdded.stashRoot : undefined);
|
|
172
215
|
if (installedStashRoot) {
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
* `proposal-new` shape). `show` is a new shape, `bundle-show`.
|
|
25
25
|
*/
|
|
26
26
|
import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
|
|
27
|
-
import { NotFoundError, UsageError } from "../../core/errors.js";
|
|
27
|
+
import { NotFoundError, TransientError, UsageError } from "../../core/errors.js";
|
|
28
28
|
import { appendEvent } from "../../core/events.js";
|
|
29
|
+
import { warn } from "../../core/warn.js";
|
|
29
30
|
import { addCommand } from "./add-cli.js";
|
|
30
31
|
import { akmInit } from "./init.js";
|
|
31
32
|
import { akmListSources, akmRemove, akmUpdate } from "./installed-stashes.js";
|
|
@@ -118,12 +119,20 @@ const removeCommand = defineJsonCommand({
|
|
|
118
119
|
},
|
|
119
120
|
});
|
|
120
121
|
const updateCommand = defineJsonCommand({
|
|
121
|
-
meta: {
|
|
122
|
+
meta: {
|
|
123
|
+
name: "update",
|
|
124
|
+
description: "Refresh one or all configured bundles and reconcile their index. Git refresh is explicit; schedule this command for automatic updates.",
|
|
125
|
+
},
|
|
122
126
|
args: {
|
|
123
127
|
target: { type: "positional", description: "Bundle to update (id or ref)", required: false },
|
|
124
128
|
all: { type: "boolean", description: "Update all configured bundles and report each outcome", default: false },
|
|
125
129
|
force: { type: "boolean", description: "Force fresh download even if version is unchanged", default: false },
|
|
126
|
-
"
|
|
130
|
+
"skip-if-locked": {
|
|
131
|
+
type: "boolean",
|
|
132
|
+
description: "Exit successfully when index/state database contention shows another akm operation is active",
|
|
133
|
+
default: false,
|
|
134
|
+
},
|
|
135
|
+
"allow-dangerous-env-keys": {
|
|
127
136
|
type: "boolean",
|
|
128
137
|
description: "Allow an update containing dangerous env keys (e.g. LD_PRELOAD, PATH). Use only after explicitly reviewing the staged bundle.",
|
|
129
138
|
default: false,
|
|
@@ -140,20 +149,31 @@ const updateCommand = defineJsonCommand({
|
|
|
140
149
|
},
|
|
141
150
|
},
|
|
142
151
|
async run({ args }) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
let result;
|
|
153
|
+
try {
|
|
154
|
+
result = await akmUpdate({
|
|
155
|
+
target: args.target,
|
|
156
|
+
all: args.all,
|
|
157
|
+
force: args.force,
|
|
158
|
+
yes: args.yes,
|
|
159
|
+
allowDangerousEnvKeys: args["allow-dangerous-env-keys"],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
const skippable = isSkippableBundleUpdateLock(error);
|
|
164
|
+
if (!args["skip-if-locked"] || !skippable)
|
|
165
|
+
throw error;
|
|
166
|
+
warn(`[bundle update] ${error.message}; skipping (--skip-if-locked)`);
|
|
167
|
+
output("update", { ok: true, skipped: { reason: "lock-held", code: error.code } });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
150
170
|
appendEvent({
|
|
151
171
|
eventType: "update",
|
|
152
172
|
metadata: {
|
|
153
173
|
target: args.target ?? null,
|
|
154
174
|
all: args.all === true,
|
|
155
175
|
force: args.force === true,
|
|
156
|
-
|
|
176
|
+
allowDangerousEnvKeys: args["allow-dangerous-env-keys"] === true,
|
|
157
177
|
processed: Array.isArray(result.processed)
|
|
158
178
|
? result.processed.length
|
|
159
179
|
: 0,
|
|
@@ -162,6 +182,10 @@ const updateCommand = defineJsonCommand({
|
|
|
162
182
|
output("update", result);
|
|
163
183
|
},
|
|
164
184
|
});
|
|
185
|
+
export function isSkippableBundleUpdateLock(error) {
|
|
186
|
+
return (error instanceof TransientError &&
|
|
187
|
+
["INDEX_DB_CONTENDED", "STATE_DB_CONTENDED", "MAINTENANCE_BARRIER_BUSY"].includes(error.code));
|
|
188
|
+
}
|
|
165
189
|
export const bundleCommand = defineGroupCommand({
|
|
166
190
|
meta: {
|
|
167
191
|
name: "bundle",
|
|
@@ -14,8 +14,38 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { bundleKeyForContentRoot, primaryBundlePath } from "../../core/config/config.js";
|
|
17
|
+
import { UsageError } from "../../core/errors.js";
|
|
17
18
|
import { deriveBundleId } from "../../indexer/installations.js";
|
|
18
19
|
export { primaryBundlePath };
|
|
20
|
+
/**
|
|
21
|
+
* Insert or move a bundle at an explicit config-map position. Object insertion
|
|
22
|
+
* order is bundle resolution priority after `defaultBundle`, which always
|
|
23
|
+
* remains first. Without a position an existing key keeps its place and a new
|
|
24
|
+
* key is appended.
|
|
25
|
+
*/
|
|
26
|
+
export function placeBundle(bundles, key, entry, position = {}) {
|
|
27
|
+
if (position.before && position.after) {
|
|
28
|
+
throw new UsageError("Only one of --before or --after may be used.");
|
|
29
|
+
}
|
|
30
|
+
const target = position.before ?? position.after;
|
|
31
|
+
if (!target)
|
|
32
|
+
return { ...bundles, [key]: entry };
|
|
33
|
+
if (target === key)
|
|
34
|
+
throw new UsageError(`Bundle "${key}" cannot be positioned relative to itself.`);
|
|
35
|
+
if (!(target in bundles))
|
|
36
|
+
throw new UsageError(`Bundle position target "${target}" is not configured.`);
|
|
37
|
+
const ordered = {};
|
|
38
|
+
for (const [existingKey, existingEntry] of Object.entries(bundles)) {
|
|
39
|
+
if (existingKey === key)
|
|
40
|
+
continue;
|
|
41
|
+
if (position.before === existingKey)
|
|
42
|
+
ordered[key] = entry;
|
|
43
|
+
ordered[existingKey] = existingEntry;
|
|
44
|
+
if (position.after === existingKey)
|
|
45
|
+
ordered[key] = entry;
|
|
46
|
+
}
|
|
47
|
+
return ordered;
|
|
48
|
+
}
|
|
19
49
|
/**
|
|
20
50
|
* Upsert the primary filesystem bundle (`{ path, writable: true }`) and point
|
|
21
51
|
* `defaultBundle` at it. Reuses the current default key when it already names a
|
|
@@ -85,13 +85,13 @@ export async function auditStashForDangerousKeys(opts) {
|
|
|
85
85
|
}
|
|
86
86
|
const stance = decideDangerousKeyInstall({
|
|
87
87
|
findingsPresent: allFindings.length > 0,
|
|
88
|
-
|
|
88
|
+
allowDangerousEnvKeys: opts.allowDangerousKeys,
|
|
89
89
|
});
|
|
90
90
|
if (stance === "allow")
|
|
91
91
|
return { blocked: false, findings: allFindings };
|
|
92
92
|
if (stance === "warn-allow") {
|
|
93
93
|
for (const finding of allFindings) {
|
|
94
|
-
warn(`[dangerous-env-key] ${finding.relPath}: key \`${finding.keyName}\` in ${finding.envRef} can hijack process execution via \`akm env run\`. Proceeding because --allow-
|
|
94
|
+
warn(`[dangerous-env-key] ${finding.relPath}: key \`${finding.keyName}\` in ${finding.envRef} can hijack process execution via \`akm env run\`. Proceeding because --allow-dangerous-env-keys was set.`);
|
|
95
95
|
}
|
|
96
96
|
return { blocked: false, findings: allFindings };
|
|
97
97
|
}
|
|
@@ -113,11 +113,11 @@ export async function auditStashForDangerousKeys(opts) {
|
|
|
113
113
|
const confirmed = await p.confirm({ message: `${operationTitle} anyway?`, initialValue: false });
|
|
114
114
|
if (!p.isCancel(confirmed) && confirmed === true)
|
|
115
115
|
return { blocked: false, findings: allFindings };
|
|
116
|
-
error = `${operationTitle} aborted: stash contains dangerous env keys. Remove the keys or re-run with --allow-
|
|
116
|
+
error = `${operationTitle} aborted: stash contains dangerous env keys. Remove the keys or re-run with --allow-dangerous-env-keys to bypass.`;
|
|
117
117
|
}
|
|
118
118
|
else {
|
|
119
119
|
const keyList = allFindings.map((finding) => ` - ${finding.keyName} (${finding.envRef})`).join("\n");
|
|
120
|
-
error = `${operationTitle} blocked: stash "${opts.ref}" contains dangerous env keys that can hijack process execution via \`akm env run\`:\n${keyList}\nRe-run with --allow-
|
|
120
|
+
error = `${operationTitle} blocked: stash "${opts.ref}" contains dangerous env keys that can hijack process execution via \`akm env run\`:\n${keyList}\nRe-run with --allow-dangerous-env-keys to bypass this check after reviewing the env file.`;
|
|
121
121
|
}
|
|
122
122
|
const rollbackWarning = await opts.rollback?.();
|
|
123
123
|
if (opts.renderBlockedError !== false) {
|
|
@@ -18,7 +18,7 @@ import fs from "node:fs";
|
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
|
|
20
20
|
import { isWithin, resolveStashDir } from "../../core/common.js";
|
|
21
|
-
import { acquireConfigReadFence, bundleComponentConfig, getSources, loadConfig } from "../../core/config/config.js";
|
|
21
|
+
import { acquireConfigReadFence, bundleComponentConfig, getSources, loadConfig, resolveSecret, } from "../../core/config/config.js";
|
|
22
22
|
import { AkmError, ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
23
23
|
import { isPathAbsent } from "../../core/path-access.js";
|
|
24
24
|
import { getDbPath, getRegistryCacheDir } from "../../core/paths.js";
|
|
@@ -106,6 +106,7 @@ function listManagedInstalls(config) {
|
|
|
106
106
|
componentRoot,
|
|
107
107
|
requiredRoots: lock.localRoot ? [path.resolve(lock.localRoot, componentRoot)] : [],
|
|
108
108
|
auditConfigGeneration: bundleAuditGeneration(bundle),
|
|
109
|
+
...(bundle.credential !== undefined ? { credential: bundle.credential } : {}),
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
return out;
|
|
@@ -641,12 +642,12 @@ async function verifyPublishedAudit(prepared, id) {
|
|
|
641
642
|
throw new ConfigError(`Writable source "${id}" changed while its audited Git target was materialized; refusing to activate unaudited bytes.`, "DANGEROUS_ENV_AUDIT_FAILED");
|
|
642
643
|
}
|
|
643
644
|
}
|
|
644
|
-
async function auditPreparedUpdate(prepared, id, ref,
|
|
645
|
+
async function auditPreparedUpdate(prepared, id, ref, allowDangerousEnvKeys) {
|
|
645
646
|
updateTransactionHook("staged", id);
|
|
646
647
|
const decision = await auditStashForDangerousKeys({
|
|
647
648
|
stashRoot: prepared.auditRoot,
|
|
648
649
|
ref,
|
|
649
|
-
allowDangerousKeys:
|
|
650
|
+
allowDangerousKeys: allowDangerousEnvKeys,
|
|
650
651
|
isTTY: process.stdin.isTTY === true,
|
|
651
652
|
operation: "update",
|
|
652
653
|
renderBlockedError: false,
|
|
@@ -713,6 +714,7 @@ function prepareWritableManagedUpdate(managed, force) {
|
|
|
713
714
|
writable: true,
|
|
714
715
|
writableRoot: stagedContentRoot,
|
|
715
716
|
...(stagedPhysicalRequiredRoots.length > 0 ? { writableRequiredRoots: stagedPhysicalRequiredRoots } : {}),
|
|
717
|
+
...(managed.credential ? { credential: resolveSecret(managed.credential, storeSecretResolver.resolveSecret) } : {}),
|
|
716
718
|
})
|
|
717
719
|
.then((staged) => {
|
|
718
720
|
const auditedTargetHead = gitHead(stagedRepo);
|
|
@@ -759,6 +761,9 @@ async function prepareManagedUpdate(managed, force) {
|
|
|
759
761
|
force,
|
|
760
762
|
writable: false,
|
|
761
763
|
cacheRootDir: stagingParent,
|
|
764
|
+
...(managed.credential
|
|
765
|
+
? { credential: resolveSecret(managed.credential, storeSecretResolver.resolveSecret) }
|
|
766
|
+
: {}),
|
|
762
767
|
});
|
|
763
768
|
if (!pathAtOrBelow(staged.cacheDir, stagingParent)) {
|
|
764
769
|
// Test/provider seams may return an already-isolated candidate. It remains
|
|
@@ -795,9 +800,9 @@ function resolveComponentAuditRoot(contentRoot, componentRoot, id) {
|
|
|
795
800
|
}
|
|
796
801
|
return resolved;
|
|
797
802
|
}
|
|
798
|
-
async function publishPreparedPlainUpdate(id, ref, prepared, stashDir,
|
|
803
|
+
async function publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowDangerousEnvKeys, full, expectedConfigGeneration) {
|
|
799
804
|
try {
|
|
800
|
-
await auditPreparedUpdate(prepared, id, ref,
|
|
805
|
+
await auditPreparedUpdate(prepared, id, ref, allowDangerousEnvKeys);
|
|
801
806
|
return await withAssetMutationLease("source-update", async () => {
|
|
802
807
|
let transaction;
|
|
803
808
|
let releaseConfigFence;
|
|
@@ -871,14 +876,18 @@ async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
|
|
|
871
876
|
const stagingParent = createStagingParent(path.dirname(livePaths.rootDir));
|
|
872
877
|
const stagedPaths = getCachePaths(repo.canonicalUrl, stagingParent);
|
|
873
878
|
try {
|
|
874
|
-
const
|
|
875
|
-
|
|
879
|
+
const hasLiveCheckout = writable && fs.existsSync(livePaths.repoDir);
|
|
880
|
+
const expectedOldHead = hasLiveCheckout ? gitHead(livePaths.repoDir) : undefined;
|
|
881
|
+
if (hasLiveCheckout) {
|
|
876
882
|
fs.cpSync(livePaths.rootDir, stagedPaths.rootDir, { recursive: true, preserveTimestamps: true });
|
|
877
883
|
}
|
|
878
884
|
const staged = await syncMirroredRepo(gitSource, {
|
|
879
885
|
force: true,
|
|
880
886
|
writable,
|
|
881
887
|
cacheRootDir: stagingParent,
|
|
888
|
+
...(gitSource.credential
|
|
889
|
+
? { credential: resolveSecret(gitSource.credential, storeSecretResolver.resolveSecret) }
|
|
890
|
+
: {}),
|
|
882
891
|
});
|
|
883
892
|
const configuredStagedAuditRoot = resolveComponentAuditRoot(resolveGitContentRoot(staged.contentDir), componentRoot, gitSource.name ?? "git");
|
|
884
893
|
if (!pathAtOrBelow(staged.cacheDir, stagingParent)) {
|
|
@@ -901,7 +910,13 @@ async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
|
|
|
901
910
|
const physicalStagedRepo = canonicalWritablePath(stagedPaths.repoDir, "staged plain Git checkout");
|
|
902
911
|
auditRoot = containedWritablePath(configuredStagedAuditRoot, physicalStagedRepo, `Post-sync component root for "${gitSource.name ?? "git"}"`);
|
|
903
912
|
publishedAuditRoot = remapStagedPath(configuredStagedAuditRoot, staged.cacheDir, livePaths.rootDir);
|
|
904
|
-
|
|
913
|
+
if (hasLiveCheckout) {
|
|
914
|
+
publishedAuditContainmentRoot = canonicalWritablePath(livePaths.repoDir, "plain Git checkout");
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
const physicalCacheParent = canonicalWritablePath(path.dirname(livePaths.rootDir), "plain Git cache parent");
|
|
918
|
+
publishedAuditContainmentRoot = path.join(physicalCacheParent, path.basename(livePaths.rootDir), path.relative(livePaths.rootDir, livePaths.repoDir));
|
|
919
|
+
}
|
|
905
920
|
publishedAuditExpectedPhysicalRoot = path.join(publishedAuditContainmentRoot, path.relative(physicalStagedRepo, auditRoot));
|
|
906
921
|
}
|
|
907
922
|
return {
|
|
@@ -909,7 +924,7 @@ async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
|
|
|
909
924
|
...(publishedAuditRoot ? { publishedAuditRoot } : {}),
|
|
910
925
|
...(publishedAuditContainmentRoot ? { publishedAuditContainmentRoot } : {}),
|
|
911
926
|
...(publishedAuditExpectedPhysicalRoot ? { publishedAuditExpectedPhysicalRoot } : {}),
|
|
912
|
-
publication: writable
|
|
927
|
+
publication: writable && hasLiveCheckout
|
|
913
928
|
? writableGitPublication({
|
|
914
929
|
stagedRepo: stagedPaths.repoDir,
|
|
915
930
|
liveRepo: livePaths.repoDir,
|
|
@@ -924,14 +939,14 @@ async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
|
|
|
924
939
|
throw error;
|
|
925
940
|
}
|
|
926
941
|
}
|
|
927
|
-
async function syncGitPlainSource(gitSource, stashDir,
|
|
942
|
+
async function syncGitPlainSource(gitSource, stashDir, allowDangerousEnvKeys) {
|
|
928
943
|
const id = gitSource.name ?? gitSource.url ?? "";
|
|
929
944
|
const currentConfig = loadConfig();
|
|
930
945
|
const currentSource = currentPlainSource(currentConfig, id, "git");
|
|
931
946
|
const ref = currentSource.url ?? "";
|
|
932
947
|
const fence = bundleAuditFence(currentConfig, id);
|
|
933
948
|
const prepared = await prepareGitPlainUpdate(currentSource, fence.componentRoot, fence.writable);
|
|
934
|
-
const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir,
|
|
949
|
+
const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowDangerousEnvKeys, true, fence.generation);
|
|
935
950
|
return { item: { id, kind: "git", ref }, index };
|
|
936
951
|
}
|
|
937
952
|
async function prepareWebsitePlainUpdate(websiteSource, componentRoot) {
|
|
@@ -962,24 +977,24 @@ async function prepareWebsitePlainUpdate(websiteSource, componentRoot) {
|
|
|
962
977
|
throw error;
|
|
963
978
|
}
|
|
964
979
|
}
|
|
965
|
-
async function syncWebsitePlainSource(websiteSource, stashDir,
|
|
980
|
+
async function syncWebsitePlainSource(websiteSource, stashDir, allowDangerousEnvKeys) {
|
|
966
981
|
const id = websiteSource.name ?? websiteSource.url ?? "";
|
|
967
982
|
const currentConfig = loadConfig();
|
|
968
983
|
const currentSource = currentPlainSource(currentConfig, id, "website");
|
|
969
984
|
const ref = currentSource.url ?? "";
|
|
970
985
|
const fence = bundleAuditFence(currentConfig, id);
|
|
971
986
|
const prepared = await prepareWebsitePlainUpdate(currentSource, fence.componentRoot);
|
|
972
|
-
const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir,
|
|
987
|
+
const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowDangerousEnvKeys, true, fence.generation);
|
|
973
988
|
return { item: { id, kind: "website", ref }, index };
|
|
974
989
|
}
|
|
975
990
|
/** Sync a git-mirrored (plain) source and return an UpdateResponse (single-target path). */
|
|
976
|
-
async function updateGitSource(stashDir, target, all, gitSource,
|
|
977
|
-
const synced = await syncGitPlainSource(gitSource, stashDir,
|
|
991
|
+
async function updateGitSource(stashDir, target, all, gitSource, allowDangerousEnvKeys) {
|
|
992
|
+
const synced = await syncGitPlainSource(gitSource, stashDir, allowDangerousEnvKeys);
|
|
978
993
|
return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
|
|
979
994
|
}
|
|
980
995
|
/** Re-crawl a website (plain) source and return an UpdateResponse (single-target path). */
|
|
981
|
-
async function updateWebsiteSource(stashDir, target, all, websiteSource,
|
|
982
|
-
const synced = await syncWebsitePlainSource(websiteSource, stashDir,
|
|
996
|
+
async function updateWebsiteSource(stashDir, target, all, websiteSource, allowDangerousEnvKeys) {
|
|
997
|
+
const synced = await syncWebsitePlainSource(websiteSource, stashDir, allowDangerousEnvKeys);
|
|
983
998
|
return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
|
|
984
999
|
}
|
|
985
1000
|
/** Reconcile a filesystem source's current bytes without provider hydration. */
|
|
@@ -1042,7 +1057,7 @@ function managedInstallViewOfPlainNpm(npmSource) {
|
|
|
1042
1057
|
* scripts; only the branch that can `rm -rf` needs a gate, not the whole
|
|
1043
1058
|
* command).
|
|
1044
1059
|
*/
|
|
1045
|
-
async function updateManagedInstall(managed, force, yes, stashDir,
|
|
1060
|
+
async function updateManagedInstall(managed, force, yes, stashDir, allowDangerousEnvKeys) {
|
|
1046
1061
|
// No pre-cleanup of the old root, even under --force: the providers already
|
|
1047
1062
|
// re-materialize staging-first (git clones into a `.tmp-*` sibling and swaps
|
|
1048
1063
|
// only on success), so destroying `managed.localRoot` BEFORE `syncFromRef`
|
|
@@ -1051,7 +1066,7 @@ async function updateManagedInstall(managed, force, yes, stashDir, allowInsecure
|
|
|
1051
1066
|
// config/lock publication and reindexing both succeed.
|
|
1052
1067
|
const prepared = await prepareManagedUpdate(managed, force);
|
|
1053
1068
|
try {
|
|
1054
|
-
await auditPreparedUpdate(prepared, managed.installId, managed.ref,
|
|
1069
|
+
await auditPreparedUpdate(prepared, managed.installId, managed.ref, allowDangerousEnvKeys);
|
|
1055
1070
|
const synced = prepared.synced;
|
|
1056
1071
|
const installedEntry = {
|
|
1057
1072
|
id: managed.installId,
|
|
@@ -1259,14 +1274,14 @@ export async function akmUpdate(input) {
|
|
|
1259
1274
|
const all = input?.all === true;
|
|
1260
1275
|
const force = input?.force === true;
|
|
1261
1276
|
const yes = input?.yes === true;
|
|
1262
|
-
const
|
|
1277
|
+
const allowDangerousEnvKeys = input?.allowDangerousEnvKeys === true;
|
|
1263
1278
|
const config = loadConfig();
|
|
1264
1279
|
const managedInstalls = listManagedInstalls(config);
|
|
1265
1280
|
if (target && !all) {
|
|
1266
1281
|
// Registry-managed install (lock-backed) — re-download from its locator.
|
|
1267
1282
|
const managed = resolveManagedTarget(config, target);
|
|
1268
1283
|
if (managed) {
|
|
1269
|
-
const updated = await updateManagedInstall(managed, force, yes, stashDir,
|
|
1284
|
+
const updated = await updateManagedInstall(managed, force, yes, stashDir, allowDangerousEnvKeys);
|
|
1270
1285
|
return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
|
|
1271
1286
|
}
|
|
1272
1287
|
// Plain git / website source (bundles without a lock) — provider re-sync.
|
|
@@ -1295,7 +1310,7 @@ export async function akmUpdate(input) {
|
|
|
1295
1310
|
return false;
|
|
1296
1311
|
});
|
|
1297
1312
|
if (gitMatch)
|
|
1298
|
-
return updateGitSource(stashDir, target, all, gitMatch,
|
|
1313
|
+
return updateGitSource(stashDir, target, all, gitMatch, allowDangerousEnvKeys);
|
|
1299
1314
|
const websiteMatch = stashes.find((s) => {
|
|
1300
1315
|
if (s.type !== "website")
|
|
1301
1316
|
return false;
|
|
@@ -1308,7 +1323,7 @@ export async function akmUpdate(input) {
|
|
|
1308
1323
|
return false;
|
|
1309
1324
|
});
|
|
1310
1325
|
if (websiteMatch)
|
|
1311
|
-
return updateWebsiteSource(stashDir, target, all, websiteMatch,
|
|
1326
|
+
return updateWebsiteSource(stashDir, target, all, websiteMatch, allowDangerousEnvKeys);
|
|
1312
1327
|
// Plain npm source (bundle without a lock) — sync via the registry
|
|
1313
1328
|
// pipeline and promote to a managed install (see
|
|
1314
1329
|
// managedInstallViewOfPlainNpm's doc comment for why npm can't stay
|
|
@@ -1323,7 +1338,7 @@ export async function akmUpdate(input) {
|
|
|
1323
1338
|
return false;
|
|
1324
1339
|
});
|
|
1325
1340
|
if (npmMatch) {
|
|
1326
|
-
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(npmMatch), force, yes, stashDir,
|
|
1341
|
+
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(npmMatch), force, yes, stashDir, allowDangerousEnvKeys);
|
|
1327
1342
|
return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
|
|
1328
1343
|
}
|
|
1329
1344
|
const filesystemMatch = stashes.find((source) => {
|
|
@@ -1346,7 +1361,7 @@ export async function akmUpdate(input) {
|
|
|
1346
1361
|
let latestIndex;
|
|
1347
1362
|
for (const managed of selected) {
|
|
1348
1363
|
try {
|
|
1349
|
-
const updated = await updateManagedInstall(managed, force, yes, stashDir,
|
|
1364
|
+
const updated = await updateManagedInstall(managed, force, yes, stashDir, allowDangerousEnvKeys);
|
|
1350
1365
|
processed.push(updated.item);
|
|
1351
1366
|
latestIndex = updated.index;
|
|
1352
1367
|
}
|
|
@@ -1371,17 +1386,17 @@ export async function akmUpdate(input) {
|
|
|
1371
1386
|
const id = plain.name ?? plain.path ?? plain.url ?? "";
|
|
1372
1387
|
try {
|
|
1373
1388
|
if (plain.type === "git") {
|
|
1374
|
-
const updated = await syncGitPlainSource(plain, stashDir,
|
|
1389
|
+
const updated = await syncGitPlainSource(plain, stashDir, allowDangerousEnvKeys);
|
|
1375
1390
|
plainSynced.push(updated.item);
|
|
1376
1391
|
latestIndex = updated.index;
|
|
1377
1392
|
}
|
|
1378
1393
|
else if (plain.type === "npm") {
|
|
1379
|
-
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(plain), force, yes, stashDir,
|
|
1394
|
+
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(plain), force, yes, stashDir, allowDangerousEnvKeys);
|
|
1380
1395
|
processed.push(updated.item);
|
|
1381
1396
|
latestIndex = updated.index;
|
|
1382
1397
|
}
|
|
1383
1398
|
else if (plain.type === "website") {
|
|
1384
|
-
const updated = await syncWebsitePlainSource(plain, stashDir,
|
|
1399
|
+
const updated = await syncWebsitePlainSource(plain, stashDir, allowDangerousEnvKeys);
|
|
1385
1400
|
plainSynced.push(updated.item);
|
|
1386
1401
|
latestIndex = updated.index;
|
|
1387
1402
|
}
|