@codyswann/lisa 2.321.2 → 2.321.4
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/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +6 -4
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +2 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +39 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/rotate-secret.mjs +10 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +30 -1
- package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +2 -2
- package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +39 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/rotate-secret.mjs +10 -2
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +30 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +2 -2
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +39 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/rotate-secret.mjs +10 -2
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +30 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +2 -2
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +39 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/rotate-secret.mjs +10 -2
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +30 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +2 -2
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +39 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/rotate-secret.mjs +10 -2
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +30 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +2 -2
- package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +39 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/rotate-secret.mjs +10 -2
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +30 -1
package/package.json
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"brace-expansion": ">=5.0.9"
|
|
116
116
|
},
|
|
117
117
|
"name": "@codyswann/lisa",
|
|
118
|
-
"version": "2.321.
|
|
118
|
+
"version": "2.321.4",
|
|
119
119
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
120
120
|
"main": "dist/index.js",
|
|
121
121
|
"exports": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
21
21
|
|
|
22
|
-
import { fetchAll } from "./providers.mjs";
|
|
22
|
+
import { fetchAll, fetchRotatable } from "./providers.mjs";
|
|
23
23
|
import { readMaterialized } from "./resolve-secret.mjs";
|
|
24
24
|
import { readConfig } from "./surfaces.mjs";
|
|
25
25
|
|
|
@@ -220,7 +220,7 @@ function main() {
|
|
|
220
220
|
checkRequired(cfg, provider, file, report);
|
|
221
221
|
checkNaming(provider, report);
|
|
222
222
|
checkNotes(provider, report);
|
|
223
|
-
checkRotating(cfg,
|
|
223
|
+
checkRotating(cfg, fetchRotatable(cfg), report);
|
|
224
224
|
checkTwoStores(provider, file, report);
|
|
225
225
|
|
|
226
226
|
const order = { error: 0, warn: 1, ok: 2 };
|
|
@@ -243,6 +243,45 @@ export function fetchAll(cfg) {
|
|
|
243
243
|
return normalizeRows(fetchRaw(cfg), cfg.narrow);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/**
|
|
247
|
+
* The provider view the rotation path reads.
|
|
248
|
+
*
|
|
249
|
+
* `excludeKeys` keeps a credential off a **surface's disk**. Applying it here
|
|
250
|
+
* too hid the provider record, making `rotating` and `excludeKeys` mutually
|
|
251
|
+
* exclusive — and a credential you cannot see is one you cannot write back to.
|
|
252
|
+
* That is backwards for a consumable credential: the kind you least want
|
|
253
|
+
* materialized is the kind that most needs a proven write path.
|
|
254
|
+
*
|
|
255
|
+
* Nothing here materializes anything. `fetchAll` remains the only view feeding
|
|
256
|
+
* a surface, so an excluded rotating credential still never reaches disk.
|
|
257
|
+
* @param {object} cfg Resolved configuration.
|
|
258
|
+
* @returns {Map<string, {value: string, note: string, id: string|null}>} Selected secrets by name.
|
|
259
|
+
*/
|
|
260
|
+
export function fetchRotatable(cfg) {
|
|
261
|
+
return normalizeRows(fetchRaw(cfg), rotationNarrow(cfg));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Narrowing for the rotation view: project grants intact, `excludeKeys` waived
|
|
266
|
+
* for declared rotating names only.
|
|
267
|
+
*
|
|
268
|
+
* The waiver reaches only names in `secrets.rotating` — config, and therefore
|
|
269
|
+
* reviewed, the same declared-never-inferred rule the write path enforces.
|
|
270
|
+
* Project narrowing is the provider's own boundary and is never widened. Split
|
|
271
|
+
* from {@link fetchRotatable} so the decision is testable without a provider.
|
|
272
|
+
* @param {object} cfg Resolved configuration.
|
|
273
|
+
* @returns {{projectIds: string[], excludeKeys: string[]}} Narrowing controls.
|
|
274
|
+
*/
|
|
275
|
+
export function rotationNarrow(cfg) {
|
|
276
|
+
const rotating = new Set(cfg.rotating ?? []);
|
|
277
|
+
return {
|
|
278
|
+
projectIds: cfg.narrow?.projectIds ?? [],
|
|
279
|
+
excludeKeys: (cfg.narrow?.excludeKeys ?? []).filter(
|
|
280
|
+
key => !rotating.has(key)
|
|
281
|
+
),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
246
285
|
/**
|
|
247
286
|
* Find one raw row by exact key, bypassing the exposure boundary.
|
|
248
287
|
*
|
|
@@ -30,7 +30,12 @@
|
|
|
30
30
|
import { readFileSync } from "node:fs";
|
|
31
31
|
import { hostname } from "node:os";
|
|
32
32
|
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
LEASE_KEY,
|
|
35
|
+
fetchRotatable,
|
|
36
|
+
rawByKey,
|
|
37
|
+
writeSecret,
|
|
38
|
+
} from "./providers.mjs";
|
|
34
39
|
import { readConfig } from "./surfaces.mjs";
|
|
35
40
|
|
|
36
41
|
/** How long a lease is honoured before it is treated as abandoned. */
|
|
@@ -63,7 +68,10 @@ function assertRotating(name, cfg) {
|
|
|
63
68
|
* @returns {{value: string, id: string}} The current entry.
|
|
64
69
|
*/
|
|
65
70
|
function entryFor(name, cfg) {
|
|
66
|
-
|
|
71
|
+
// Rotation view, not the materialization view: a name may be excluded from
|
|
72
|
+
// every surface and still need its replacement persisted. assertRotating has
|
|
73
|
+
// already proved this name is declared in config.
|
|
74
|
+
const hit = fetchRotatable(cfg).get(name);
|
|
67
75
|
if (!hit) throw new Error(`${name} is not available to this account`);
|
|
68
76
|
if (!hit.id) {
|
|
69
77
|
throw new Error(
|
package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
rmSync,
|
|
38
38
|
writeFileSync,
|
|
39
39
|
} from "node:fs";
|
|
40
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
40
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
41
41
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
42
42
|
|
|
43
43
|
import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
|
|
@@ -197,6 +197,22 @@ function applyToolchain(tools, dryRun) {
|
|
|
197
197
|
mkdirSync(binDir, { recursive: true, mode: 0o755 });
|
|
198
198
|
const byName = new Map((tools.install ?? []).map(t => [t.name, t]));
|
|
199
199
|
|
|
200
|
+
// Installing a binary somewhere nothing looks is the same as not installing
|
|
201
|
+
// it. `~/.local/bin` is on PATH by default on a developer workstation and is
|
|
202
|
+
// NOT on a minimal container, so the toolchain step reported `install bws`,
|
|
203
|
+
// the file landed at ~/.local/bin/bws mode 755, and the very next step died
|
|
204
|
+
// with `spawnSync bws ENOENT`.
|
|
205
|
+
//
|
|
206
|
+
// This was invisible in every local test because a workstation shell already
|
|
207
|
+
// exports the directory — the environment doing the hiding was the one used
|
|
208
|
+
// to verify the fix.
|
|
209
|
+
//
|
|
210
|
+
// Prepended, not appended: a pinned-and-checksummed binary must win over
|
|
211
|
+
// whatever an image happens to ship under the same name.
|
|
212
|
+
if (!pathContains(binDir)) {
|
|
213
|
+
process.env.PATH = `${binDir}${delimiter}${process.env.PATH ?? ""}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
200
216
|
for (const step of plan) {
|
|
201
217
|
console.log(` ${step.action.padEnd(8)} ${step.reason}`);
|
|
202
218
|
if (step.action === "install" && !dryRun)
|
|
@@ -205,6 +221,19 @@ function applyToolchain(tools, dryRun) {
|
|
|
205
221
|
return plan;
|
|
206
222
|
}
|
|
207
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Whether a directory is already on PATH.
|
|
226
|
+
*
|
|
227
|
+
* Compared entry by entry rather than by substring: a substring test would
|
|
228
|
+
* consider `/root/.local/bin` present because `/root/.local/bin/extra` is, and
|
|
229
|
+
* would then skip the prepend that makes the tool findable.
|
|
230
|
+
* @param {string} dir Directory to look for.
|
|
231
|
+
* @returns {boolean} True when PATH already contains exactly that entry.
|
|
232
|
+
*/
|
|
233
|
+
export function pathContains(dir, path = process.env.PATH ?? "") {
|
|
234
|
+
return path.split(delimiter).filter(Boolean).includes(dir);
|
|
235
|
+
}
|
|
236
|
+
|
|
208
237
|
/**
|
|
209
238
|
* Run the project's own setup hook when it has one.
|
|
210
239
|
*
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
21
21
|
|
|
22
|
-
import { fetchAll } from "./providers.mjs";
|
|
22
|
+
import { fetchAll, fetchRotatable } from "./providers.mjs";
|
|
23
23
|
import { readMaterialized } from "./resolve-secret.mjs";
|
|
24
24
|
import { readConfig } from "./surfaces.mjs";
|
|
25
25
|
|
|
@@ -220,7 +220,7 @@ function main() {
|
|
|
220
220
|
checkRequired(cfg, provider, file, report);
|
|
221
221
|
checkNaming(provider, report);
|
|
222
222
|
checkNotes(provider, report);
|
|
223
|
-
checkRotating(cfg,
|
|
223
|
+
checkRotating(cfg, fetchRotatable(cfg), report);
|
|
224
224
|
checkTwoStores(provider, file, report);
|
|
225
225
|
|
|
226
226
|
const order = { error: 0, warn: 1, ok: 2 };
|
|
@@ -243,6 +243,45 @@ export function fetchAll(cfg) {
|
|
|
243
243
|
return normalizeRows(fetchRaw(cfg), cfg.narrow);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/**
|
|
247
|
+
* The provider view the rotation path reads.
|
|
248
|
+
*
|
|
249
|
+
* `excludeKeys` keeps a credential off a **surface's disk**. Applying it here
|
|
250
|
+
* too hid the provider record, making `rotating` and `excludeKeys` mutually
|
|
251
|
+
* exclusive — and a credential you cannot see is one you cannot write back to.
|
|
252
|
+
* That is backwards for a consumable credential: the kind you least want
|
|
253
|
+
* materialized is the kind that most needs a proven write path.
|
|
254
|
+
*
|
|
255
|
+
* Nothing here materializes anything. `fetchAll` remains the only view feeding
|
|
256
|
+
* a surface, so an excluded rotating credential still never reaches disk.
|
|
257
|
+
* @param {object} cfg Resolved configuration.
|
|
258
|
+
* @returns {Map<string, {value: string, note: string, id: string|null}>} Selected secrets by name.
|
|
259
|
+
*/
|
|
260
|
+
export function fetchRotatable(cfg) {
|
|
261
|
+
return normalizeRows(fetchRaw(cfg), rotationNarrow(cfg));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Narrowing for the rotation view: project grants intact, `excludeKeys` waived
|
|
266
|
+
* for declared rotating names only.
|
|
267
|
+
*
|
|
268
|
+
* The waiver reaches only names in `secrets.rotating` — config, and therefore
|
|
269
|
+
* reviewed, the same declared-never-inferred rule the write path enforces.
|
|
270
|
+
* Project narrowing is the provider's own boundary and is never widened. Split
|
|
271
|
+
* from {@link fetchRotatable} so the decision is testable without a provider.
|
|
272
|
+
* @param {object} cfg Resolved configuration.
|
|
273
|
+
* @returns {{projectIds: string[], excludeKeys: string[]}} Narrowing controls.
|
|
274
|
+
*/
|
|
275
|
+
export function rotationNarrow(cfg) {
|
|
276
|
+
const rotating = new Set(cfg.rotating ?? []);
|
|
277
|
+
return {
|
|
278
|
+
projectIds: cfg.narrow?.projectIds ?? [],
|
|
279
|
+
excludeKeys: (cfg.narrow?.excludeKeys ?? []).filter(
|
|
280
|
+
key => !rotating.has(key)
|
|
281
|
+
),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
246
285
|
/**
|
|
247
286
|
* Find one raw row by exact key, bypassing the exposure boundary.
|
|
248
287
|
*
|
|
@@ -30,7 +30,12 @@
|
|
|
30
30
|
import { readFileSync } from "node:fs";
|
|
31
31
|
import { hostname } from "node:os";
|
|
32
32
|
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
LEASE_KEY,
|
|
35
|
+
fetchRotatable,
|
|
36
|
+
rawByKey,
|
|
37
|
+
writeSecret,
|
|
38
|
+
} from "./providers.mjs";
|
|
34
39
|
import { readConfig } from "./surfaces.mjs";
|
|
35
40
|
|
|
36
41
|
/** How long a lease is honoured before it is treated as abandoned. */
|
|
@@ -63,7 +68,10 @@ function assertRotating(name, cfg) {
|
|
|
63
68
|
* @returns {{value: string, id: string}} The current entry.
|
|
64
69
|
*/
|
|
65
70
|
function entryFor(name, cfg) {
|
|
66
|
-
|
|
71
|
+
// Rotation view, not the materialization view: a name may be excluded from
|
|
72
|
+
// every surface and still need its replacement persisted. assertRotating has
|
|
73
|
+
// already proved this name is declared in config.
|
|
74
|
+
const hit = fetchRotatable(cfg).get(name);
|
|
67
75
|
if (!hit) throw new Error(`${name} is not available to this account`);
|
|
68
76
|
if (!hit.id) {
|
|
69
77
|
throw new Error(
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
rmSync,
|
|
38
38
|
writeFileSync,
|
|
39
39
|
} from "node:fs";
|
|
40
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
40
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
41
41
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
42
42
|
|
|
43
43
|
import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
|
|
@@ -197,6 +197,22 @@ function applyToolchain(tools, dryRun) {
|
|
|
197
197
|
mkdirSync(binDir, { recursive: true, mode: 0o755 });
|
|
198
198
|
const byName = new Map((tools.install ?? []).map(t => [t.name, t]));
|
|
199
199
|
|
|
200
|
+
// Installing a binary somewhere nothing looks is the same as not installing
|
|
201
|
+
// it. `~/.local/bin` is on PATH by default on a developer workstation and is
|
|
202
|
+
// NOT on a minimal container, so the toolchain step reported `install bws`,
|
|
203
|
+
// the file landed at ~/.local/bin/bws mode 755, and the very next step died
|
|
204
|
+
// with `spawnSync bws ENOENT`.
|
|
205
|
+
//
|
|
206
|
+
// This was invisible in every local test because a workstation shell already
|
|
207
|
+
// exports the directory — the environment doing the hiding was the one used
|
|
208
|
+
// to verify the fix.
|
|
209
|
+
//
|
|
210
|
+
// Prepended, not appended: a pinned-and-checksummed binary must win over
|
|
211
|
+
// whatever an image happens to ship under the same name.
|
|
212
|
+
if (!pathContains(binDir)) {
|
|
213
|
+
process.env.PATH = `${binDir}${delimiter}${process.env.PATH ?? ""}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
200
216
|
for (const step of plan) {
|
|
201
217
|
console.log(` ${step.action.padEnd(8)} ${step.reason}`);
|
|
202
218
|
if (step.action === "install" && !dryRun)
|
|
@@ -205,6 +221,19 @@ function applyToolchain(tools, dryRun) {
|
|
|
205
221
|
return plan;
|
|
206
222
|
}
|
|
207
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Whether a directory is already on PATH.
|
|
226
|
+
*
|
|
227
|
+
* Compared entry by entry rather than by substring: a substring test would
|
|
228
|
+
* consider `/root/.local/bin` present because `/root/.local/bin/extra` is, and
|
|
229
|
+
* would then skip the prepend that makes the tool findable.
|
|
230
|
+
* @param {string} dir Directory to look for.
|
|
231
|
+
* @returns {boolean} True when PATH already contains exactly that entry.
|
|
232
|
+
*/
|
|
233
|
+
export function pathContains(dir, path = process.env.PATH ?? "") {
|
|
234
|
+
return path.split(delimiter).filter(Boolean).includes(dir);
|
|
235
|
+
}
|
|
236
|
+
|
|
208
237
|
/**
|
|
209
238
|
* Run the project's own setup hook when it has one.
|
|
210
239
|
*
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
21
21
|
|
|
22
|
-
import { fetchAll } from "./providers.mjs";
|
|
22
|
+
import { fetchAll, fetchRotatable } from "./providers.mjs";
|
|
23
23
|
import { readMaterialized } from "./resolve-secret.mjs";
|
|
24
24
|
import { readConfig } from "./surfaces.mjs";
|
|
25
25
|
|
|
@@ -220,7 +220,7 @@ function main() {
|
|
|
220
220
|
checkRequired(cfg, provider, file, report);
|
|
221
221
|
checkNaming(provider, report);
|
|
222
222
|
checkNotes(provider, report);
|
|
223
|
-
checkRotating(cfg,
|
|
223
|
+
checkRotating(cfg, fetchRotatable(cfg), report);
|
|
224
224
|
checkTwoStores(provider, file, report);
|
|
225
225
|
|
|
226
226
|
const order = { error: 0, warn: 1, ok: 2 };
|
|
@@ -243,6 +243,45 @@ export function fetchAll(cfg) {
|
|
|
243
243
|
return normalizeRows(fetchRaw(cfg), cfg.narrow);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/**
|
|
247
|
+
* The provider view the rotation path reads.
|
|
248
|
+
*
|
|
249
|
+
* `excludeKeys` keeps a credential off a **surface's disk**. Applying it here
|
|
250
|
+
* too hid the provider record, making `rotating` and `excludeKeys` mutually
|
|
251
|
+
* exclusive — and a credential you cannot see is one you cannot write back to.
|
|
252
|
+
* That is backwards for a consumable credential: the kind you least want
|
|
253
|
+
* materialized is the kind that most needs a proven write path.
|
|
254
|
+
*
|
|
255
|
+
* Nothing here materializes anything. `fetchAll` remains the only view feeding
|
|
256
|
+
* a surface, so an excluded rotating credential still never reaches disk.
|
|
257
|
+
* @param {object} cfg Resolved configuration.
|
|
258
|
+
* @returns {Map<string, {value: string, note: string, id: string|null}>} Selected secrets by name.
|
|
259
|
+
*/
|
|
260
|
+
export function fetchRotatable(cfg) {
|
|
261
|
+
return normalizeRows(fetchRaw(cfg), rotationNarrow(cfg));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Narrowing for the rotation view: project grants intact, `excludeKeys` waived
|
|
266
|
+
* for declared rotating names only.
|
|
267
|
+
*
|
|
268
|
+
* The waiver reaches only names in `secrets.rotating` — config, and therefore
|
|
269
|
+
* reviewed, the same declared-never-inferred rule the write path enforces.
|
|
270
|
+
* Project narrowing is the provider's own boundary and is never widened. Split
|
|
271
|
+
* from {@link fetchRotatable} so the decision is testable without a provider.
|
|
272
|
+
* @param {object} cfg Resolved configuration.
|
|
273
|
+
* @returns {{projectIds: string[], excludeKeys: string[]}} Narrowing controls.
|
|
274
|
+
*/
|
|
275
|
+
export function rotationNarrow(cfg) {
|
|
276
|
+
const rotating = new Set(cfg.rotating ?? []);
|
|
277
|
+
return {
|
|
278
|
+
projectIds: cfg.narrow?.projectIds ?? [],
|
|
279
|
+
excludeKeys: (cfg.narrow?.excludeKeys ?? []).filter(
|
|
280
|
+
key => !rotating.has(key)
|
|
281
|
+
),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
246
285
|
/**
|
|
247
286
|
* Find one raw row by exact key, bypassing the exposure boundary.
|
|
248
287
|
*
|
|
@@ -30,7 +30,12 @@
|
|
|
30
30
|
import { readFileSync } from "node:fs";
|
|
31
31
|
import { hostname } from "node:os";
|
|
32
32
|
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
LEASE_KEY,
|
|
35
|
+
fetchRotatable,
|
|
36
|
+
rawByKey,
|
|
37
|
+
writeSecret,
|
|
38
|
+
} from "./providers.mjs";
|
|
34
39
|
import { readConfig } from "./surfaces.mjs";
|
|
35
40
|
|
|
36
41
|
/** How long a lease is honoured before it is treated as abandoned. */
|
|
@@ -63,7 +68,10 @@ function assertRotating(name, cfg) {
|
|
|
63
68
|
* @returns {{value: string, id: string}} The current entry.
|
|
64
69
|
*/
|
|
65
70
|
function entryFor(name, cfg) {
|
|
66
|
-
|
|
71
|
+
// Rotation view, not the materialization view: a name may be excluded from
|
|
72
|
+
// every surface and still need its replacement persisted. assertRotating has
|
|
73
|
+
// already proved this name is declared in config.
|
|
74
|
+
const hit = fetchRotatable(cfg).get(name);
|
|
67
75
|
if (!hit) throw new Error(`${name} is not available to this account`);
|
|
68
76
|
if (!hit.id) {
|
|
69
77
|
throw new Error(
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
rmSync,
|
|
38
38
|
writeFileSync,
|
|
39
39
|
} from "node:fs";
|
|
40
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
40
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
41
41
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
42
42
|
|
|
43
43
|
import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
|
|
@@ -197,6 +197,22 @@ function applyToolchain(tools, dryRun) {
|
|
|
197
197
|
mkdirSync(binDir, { recursive: true, mode: 0o755 });
|
|
198
198
|
const byName = new Map((tools.install ?? []).map(t => [t.name, t]));
|
|
199
199
|
|
|
200
|
+
// Installing a binary somewhere nothing looks is the same as not installing
|
|
201
|
+
// it. `~/.local/bin` is on PATH by default on a developer workstation and is
|
|
202
|
+
// NOT on a minimal container, so the toolchain step reported `install bws`,
|
|
203
|
+
// the file landed at ~/.local/bin/bws mode 755, and the very next step died
|
|
204
|
+
// with `spawnSync bws ENOENT`.
|
|
205
|
+
//
|
|
206
|
+
// This was invisible in every local test because a workstation shell already
|
|
207
|
+
// exports the directory — the environment doing the hiding was the one used
|
|
208
|
+
// to verify the fix.
|
|
209
|
+
//
|
|
210
|
+
// Prepended, not appended: a pinned-and-checksummed binary must win over
|
|
211
|
+
// whatever an image happens to ship under the same name.
|
|
212
|
+
if (!pathContains(binDir)) {
|
|
213
|
+
process.env.PATH = `${binDir}${delimiter}${process.env.PATH ?? ""}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
200
216
|
for (const step of plan) {
|
|
201
217
|
console.log(` ${step.action.padEnd(8)} ${step.reason}`);
|
|
202
218
|
if (step.action === "install" && !dryRun)
|
|
@@ -205,6 +221,19 @@ function applyToolchain(tools, dryRun) {
|
|
|
205
221
|
return plan;
|
|
206
222
|
}
|
|
207
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Whether a directory is already on PATH.
|
|
226
|
+
*
|
|
227
|
+
* Compared entry by entry rather than by substring: a substring test would
|
|
228
|
+
* consider `/root/.local/bin` present because `/root/.local/bin/extra` is, and
|
|
229
|
+
* would then skip the prepend that makes the tool findable.
|
|
230
|
+
* @param {string} dir Directory to look for.
|
|
231
|
+
* @returns {boolean} True when PATH already contains exactly that entry.
|
|
232
|
+
*/
|
|
233
|
+
export function pathContains(dir, path = process.env.PATH ?? "") {
|
|
234
|
+
return path.split(delimiter).filter(Boolean).includes(dir);
|
|
235
|
+
}
|
|
236
|
+
|
|
208
237
|
/**
|
|
209
238
|
* Run the project's own setup hook when it has one.
|
|
210
239
|
*
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
21
21
|
|
|
22
|
-
import { fetchAll } from "./providers.mjs";
|
|
22
|
+
import { fetchAll, fetchRotatable } from "./providers.mjs";
|
|
23
23
|
import { readMaterialized } from "./resolve-secret.mjs";
|
|
24
24
|
import { readConfig } from "./surfaces.mjs";
|
|
25
25
|
|
|
@@ -220,7 +220,7 @@ function main() {
|
|
|
220
220
|
checkRequired(cfg, provider, file, report);
|
|
221
221
|
checkNaming(provider, report);
|
|
222
222
|
checkNotes(provider, report);
|
|
223
|
-
checkRotating(cfg,
|
|
223
|
+
checkRotating(cfg, fetchRotatable(cfg), report);
|
|
224
224
|
checkTwoStores(provider, file, report);
|
|
225
225
|
|
|
226
226
|
const order = { error: 0, warn: 1, ok: 2 };
|
|
@@ -243,6 +243,45 @@ export function fetchAll(cfg) {
|
|
|
243
243
|
return normalizeRows(fetchRaw(cfg), cfg.narrow);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/**
|
|
247
|
+
* The provider view the rotation path reads.
|
|
248
|
+
*
|
|
249
|
+
* `excludeKeys` keeps a credential off a **surface's disk**. Applying it here
|
|
250
|
+
* too hid the provider record, making `rotating` and `excludeKeys` mutually
|
|
251
|
+
* exclusive — and a credential you cannot see is one you cannot write back to.
|
|
252
|
+
* That is backwards for a consumable credential: the kind you least want
|
|
253
|
+
* materialized is the kind that most needs a proven write path.
|
|
254
|
+
*
|
|
255
|
+
* Nothing here materializes anything. `fetchAll` remains the only view feeding
|
|
256
|
+
* a surface, so an excluded rotating credential still never reaches disk.
|
|
257
|
+
* @param {object} cfg Resolved configuration.
|
|
258
|
+
* @returns {Map<string, {value: string, note: string, id: string|null}>} Selected secrets by name.
|
|
259
|
+
*/
|
|
260
|
+
export function fetchRotatable(cfg) {
|
|
261
|
+
return normalizeRows(fetchRaw(cfg), rotationNarrow(cfg));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Narrowing for the rotation view: project grants intact, `excludeKeys` waived
|
|
266
|
+
* for declared rotating names only.
|
|
267
|
+
*
|
|
268
|
+
* The waiver reaches only names in `secrets.rotating` — config, and therefore
|
|
269
|
+
* reviewed, the same declared-never-inferred rule the write path enforces.
|
|
270
|
+
* Project narrowing is the provider's own boundary and is never widened. Split
|
|
271
|
+
* from {@link fetchRotatable} so the decision is testable without a provider.
|
|
272
|
+
* @param {object} cfg Resolved configuration.
|
|
273
|
+
* @returns {{projectIds: string[], excludeKeys: string[]}} Narrowing controls.
|
|
274
|
+
*/
|
|
275
|
+
export function rotationNarrow(cfg) {
|
|
276
|
+
const rotating = new Set(cfg.rotating ?? []);
|
|
277
|
+
return {
|
|
278
|
+
projectIds: cfg.narrow?.projectIds ?? [],
|
|
279
|
+
excludeKeys: (cfg.narrow?.excludeKeys ?? []).filter(
|
|
280
|
+
key => !rotating.has(key)
|
|
281
|
+
),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
246
285
|
/**
|
|
247
286
|
* Find one raw row by exact key, bypassing the exposure boundary.
|
|
248
287
|
*
|
|
@@ -30,7 +30,12 @@
|
|
|
30
30
|
import { readFileSync } from "node:fs";
|
|
31
31
|
import { hostname } from "node:os";
|
|
32
32
|
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
LEASE_KEY,
|
|
35
|
+
fetchRotatable,
|
|
36
|
+
rawByKey,
|
|
37
|
+
writeSecret,
|
|
38
|
+
} from "./providers.mjs";
|
|
34
39
|
import { readConfig } from "./surfaces.mjs";
|
|
35
40
|
|
|
36
41
|
/** How long a lease is honoured before it is treated as abandoned. */
|
|
@@ -63,7 +68,10 @@ function assertRotating(name, cfg) {
|
|
|
63
68
|
* @returns {{value: string, id: string}} The current entry.
|
|
64
69
|
*/
|
|
65
70
|
function entryFor(name, cfg) {
|
|
66
|
-
|
|
71
|
+
// Rotation view, not the materialization view: a name may be excluded from
|
|
72
|
+
// every surface and still need its replacement persisted. assertRotating has
|
|
73
|
+
// already proved this name is declared in config.
|
|
74
|
+
const hit = fetchRotatable(cfg).get(name);
|
|
67
75
|
if (!hit) throw new Error(`${name} is not available to this account`);
|
|
68
76
|
if (!hit.id) {
|
|
69
77
|
throw new Error(
|