@codyswann/lisa 2.331.2 → 2.332.0
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 +12 -3
- 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/aws-bootstrap.mjs +102 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +25 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +40 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +13 -1
- package/plugins/lisa/skills/lisa-secrets-access/scripts/aws-bootstrap.mjs +102 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +25 -2
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +40 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +13 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/aws-bootstrap.mjs +102 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +25 -2
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +40 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +13 -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/aws-bootstrap.mjs +102 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +25 -2
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +40 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +13 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/aws-bootstrap.mjs +102 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +25 -2
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +40 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +13 -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/aws-bootstrap.mjs +102 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +25 -2
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +40 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +13 -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.
|
|
118
|
+
"version": "2.332.0",
|
|
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": {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn the AWS bootstrap bundle into environment variables the AWS SDKs obey.
|
|
3
|
+
*
|
|
4
|
+
* `LISA_AWS_BOOTSTRAP_JSON` carries the agent's AWS identity as one JSON blob.
|
|
5
|
+
* Materializing it verbatim is not enough, because nothing reads that name:
|
|
6
|
+
* every AWS SDK and the CLI look for `AWS_ACCESS_KEY_ID` and
|
|
7
|
+
* `AWS_SECRET_ACCESS_KEY`, or for a profile.
|
|
8
|
+
*
|
|
9
|
+
* Writing profiles alone is also not enough, and that is the failure this
|
|
10
|
+
* module exists for. A Claude cloud container ships its own
|
|
11
|
+
* `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` in the ambient environment, and
|
|
12
|
+
* **environment variables outrank profile files in the credential chain**. So a
|
|
13
|
+
* session with correctly written `~/.aws/credentials` still failed:
|
|
14
|
+
*
|
|
15
|
+
* aws sts get-caller-identity
|
|
16
|
+
* InvalidClientTokenId: The security token included in the request is invalid.
|
|
17
|
+
*
|
|
18
|
+
* Deriving the pair into the materialized env file fixes it, because that file
|
|
19
|
+
* is sourced into the session and the last export wins. Exporting over a
|
|
20
|
+
* variable this project did not set is a real intrusion, so it is deliberate,
|
|
21
|
+
* narrow, and never silent:
|
|
22
|
+
*
|
|
23
|
+
* - only when the bundle actually parses and carries both halves
|
|
24
|
+
* - never over a value the secret store itself supplies under those names,
|
|
25
|
+
* which is a project stating an explicit intent
|
|
26
|
+
* - recorded in the notes file, so the override is discoverable by the same
|
|
27
|
+
* read-the-note path as any other credential
|
|
28
|
+
* @module aws-bootstrap
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** The names every AWS SDK and the CLI actually read. */
|
|
32
|
+
const ACCESS_KEY = "AWS_ACCESS_KEY_ID";
|
|
33
|
+
const SECRET_KEY = "AWS_SECRET_ACCESS_KEY";
|
|
34
|
+
const REGION = "AWS_DEFAULT_REGION";
|
|
35
|
+
|
|
36
|
+
/** The bundle that carries the agent's identity. */
|
|
37
|
+
export const BOOTSTRAP_KEY = "LISA_AWS_BOOTSTRAP_JSON";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read the bootstrap bundle, treating anything malformed as absent.
|
|
41
|
+
*
|
|
42
|
+
* A parse failure must not take materialization down with it: every other
|
|
43
|
+
* secret in the run is still valid and still needed, and a session with most of
|
|
44
|
+
* its credentials beats a session with none.
|
|
45
|
+
* @param {string} raw Bundle contents.
|
|
46
|
+
* @returns {object|null} Parsed bundle, or null.
|
|
47
|
+
*/
|
|
48
|
+
export function parseBootstrap(raw) {
|
|
49
|
+
if (typeof raw !== "string" || raw.trim() === "") return null;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derive the AWS variables implied by an already-selected secret set.
|
|
60
|
+
*
|
|
61
|
+
* Returns only additions; the caller merges. Nothing here mutates its input,
|
|
62
|
+
* so a caller can decide what to do with the result — including reporting it.
|
|
63
|
+
* @param {Map<string, {value: string}>} selected Secrets by exact name.
|
|
64
|
+
* @returns {Map<string, {value: string, note: string}>} Derived variables.
|
|
65
|
+
*/
|
|
66
|
+
export function deriveAwsEnvironment(selected) {
|
|
67
|
+
const derived = new Map();
|
|
68
|
+
const bundle = parseBootstrap(selected.get(BOOTSTRAP_KEY)?.value);
|
|
69
|
+
if (!bundle) return derived;
|
|
70
|
+
|
|
71
|
+
const accessKeyId = bundle.accessKeyId ?? bundle.aws_access_key_id;
|
|
72
|
+
const secretAccessKey =
|
|
73
|
+
bundle.secretAccessKey ?? bundle.aws_secret_access_key;
|
|
74
|
+
// Both halves or neither. Half a credential is not a weaker credential, it is
|
|
75
|
+
// a confusing failure — the SDK reports a signature error rather than a
|
|
76
|
+
// missing one.
|
|
77
|
+
if (!accessKeyId || !secretAccessKey) return derived;
|
|
78
|
+
|
|
79
|
+
// A project that stores these under their own names has said something
|
|
80
|
+
// explicit. Overriding that would be this module guessing against an operator.
|
|
81
|
+
if (selected.has(ACCESS_KEY) || selected.has(SECRET_KEY)) return derived;
|
|
82
|
+
|
|
83
|
+
const note =
|
|
84
|
+
`Derived from ${BOOTSTRAP_KEY} by lisa-secrets-access. Exported ` +
|
|
85
|
+
`deliberately so it overrides any ambient ${ACCESS_KEY} the host injects — ` +
|
|
86
|
+
`environment variables outrank profile files in the AWS credential chain, ` +
|
|
87
|
+
`so without this a stale host value wins and every call fails with ` +
|
|
88
|
+
`InvalidClientTokenId. This is the assume-only bootstrap identity; real ` +
|
|
89
|
+
`work assumes a role from it.`;
|
|
90
|
+
|
|
91
|
+
derived.set(ACCESS_KEY, { value: String(accessKeyId), note });
|
|
92
|
+
derived.set(SECRET_KEY, { value: String(secretAccessKey), note });
|
|
93
|
+
|
|
94
|
+
const region = bundle.region ?? bundle.defaultRegion;
|
|
95
|
+
if (region && !selected.has(REGION)) {
|
|
96
|
+
derived.set(REGION, {
|
|
97
|
+
value: String(region),
|
|
98
|
+
note: `Derived from ${BOOTSTRAP_KEY} by lisa-secrets-access.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return derived;
|
|
102
|
+
}
|
package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
writeFileSync,
|
|
28
28
|
} from "node:fs";
|
|
29
29
|
|
|
30
|
+
import { deriveAwsEnvironment } from "./aws-bootstrap.mjs";
|
|
30
31
|
import { renderEnv, renderNotes } from "./envfile.mjs";
|
|
31
32
|
import { fetchAll } from "./providers.mjs";
|
|
32
33
|
import { materializedPaths, readConfig } from "./surfaces.mjs";
|
|
@@ -74,25 +75,47 @@ export function materialize(cfg = readConfig()) {
|
|
|
74
75
|
);
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
// The AWS bundle is stored as one JSON blob under a name no SDK reads, so
|
|
79
|
+
// the variables it implies are derived here. Without them a host-injected
|
|
80
|
+
// AWS_ACCESS_KEY_ID wins — environment variables outrank profile files in the
|
|
81
|
+
// credential chain — and every AWS call fails with InvalidClientTokenId even
|
|
82
|
+
// though the real credential materialized correctly.
|
|
83
|
+
const derived = deriveAwsEnvironment(selected);
|
|
84
|
+
for (const [name, entry] of derived) selected.set(name, entry);
|
|
85
|
+
|
|
77
86
|
const { dir, valuesFile, notesFile } = materializedPaths(cfg.namespace);
|
|
78
87
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
79
88
|
chmodSync(dir, 0o700);
|
|
80
89
|
writeAtomic(valuesFile, renderEnv(selected));
|
|
81
90
|
writeAtomic(notesFile, renderNotes(selected));
|
|
82
|
-
return { count: selected.size, dir };
|
|
91
|
+
return { count: selected.size, derived: derived.size, dir };
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
function main() {
|
|
86
95
|
const cfg = readConfig();
|
|
87
96
|
if (process.argv.includes("--dry-run")) {
|
|
88
97
|
const selected = fetchAll(cfg);
|
|
98
|
+
// Derive here too, or the preview lies in the one place it matters most:
|
|
99
|
+
// the derived names are exactly the ones that override ambient host
|
|
100
|
+
// credentials, so omitting them hides the most surprising effect of the run
|
|
101
|
+
// and reports a smaller count than the real write produces.
|
|
102
|
+
for (const [name, entry] of deriveAwsEnvironment(selected)) {
|
|
103
|
+
selected.set(name, entry);
|
|
104
|
+
}
|
|
89
105
|
const { dir } = materializedPaths(cfg.namespace);
|
|
90
106
|
console.log(`would write ${selected.size} secret(s) to ${dir}`);
|
|
91
107
|
console.log([...selected.keys()].sort().join("\n"));
|
|
92
108
|
return;
|
|
93
109
|
}
|
|
94
|
-
const { count, dir } = materialize(cfg);
|
|
110
|
+
const { count, derived, dir } = materialize(cfg);
|
|
95
111
|
console.log(`materialized ${count} secret(s) and their notes into ${dir}`);
|
|
112
|
+
if (derived > 0) {
|
|
113
|
+
// Said out loud: this run exported variables the host may also have set.
|
|
114
|
+
console.log(
|
|
115
|
+
` ${derived} AWS variable(s) derived from the bootstrap bundle, ` +
|
|
116
|
+
`overriding any ambient value`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs
CHANGED
|
@@ -300,6 +300,44 @@ function installReleaseTree(tool, binDir) {
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Install a pinned artifact that IS the binary, with no archive around it.
|
|
305
|
+
*
|
|
306
|
+
* Several tools worth pinning publish a raw executable rather than an archive —
|
|
307
|
+
* jq ships `jq-linux-amd64` and a `sha256sum.txt`, which is ideal material for a
|
|
308
|
+
* checksummed pin and fits none of the archive kinds.
|
|
309
|
+
*
|
|
310
|
+
* Without this the only entry that passes `assertPinned` is `release-zip`, which
|
|
311
|
+
* then fails at install when `unzip` is handed a binary. A manifest entry that
|
|
312
|
+
* validates and cannot install is worse than one that is rejected outright: the
|
|
313
|
+
* error arrives during provisioning rather than during review.
|
|
314
|
+
*
|
|
315
|
+
* There is no `binary` field here, deliberately — the download is the binary,
|
|
316
|
+
* so there is nothing inside it to name.
|
|
317
|
+
* @param {object} tool Manifest entry.
|
|
318
|
+
* @param {string} binDir Directory to install into.
|
|
319
|
+
*/
|
|
320
|
+
function installReleaseBinary(tool, binDir) {
|
|
321
|
+
const temporary = join(binDir, `.${tool.name}-download`);
|
|
322
|
+
mkdirSync(temporary, { recursive: true });
|
|
323
|
+
try {
|
|
324
|
+
const artifact = join(temporary, tool.name);
|
|
325
|
+
execFileSync("curl", ["-fsSL", tool.url, "-o", artifact], {
|
|
326
|
+
stdio: "inherit",
|
|
327
|
+
});
|
|
328
|
+
// Verify before it reaches a directory on PATH. For this kind that ordering
|
|
329
|
+
// matters more than for the archives: the downloaded file is directly
|
|
330
|
+
// executable, so a wrong artifact placed first is a wrong artifact that can
|
|
331
|
+
// be run.
|
|
332
|
+
verifyChecksum(artifact, tool.sha256, tool.name);
|
|
333
|
+
execFileSync("install", ["-m", "0755", artifact, join(binDir, tool.name)], {
|
|
334
|
+
stdio: "inherit",
|
|
335
|
+
});
|
|
336
|
+
} finally {
|
|
337
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
303
341
|
/**
|
|
304
342
|
* Install a pinned global npm package.
|
|
305
343
|
* @param {object} tool Manifest entry.
|
|
@@ -328,6 +366,8 @@ export function installTool(tool, binDir) {
|
|
|
328
366
|
if (tool.install === "release-zip") installReleaseZip(tool, binDir);
|
|
329
367
|
else if (tool.install === "release-tar") installReleaseTar(tool, binDir);
|
|
330
368
|
else if (tool.install === "release-tree") installReleaseTree(tool, binDir);
|
|
369
|
+
else if (tool.install === "release-binary")
|
|
370
|
+
installReleaseBinary(tool, binDir);
|
|
331
371
|
else installNpmGlobal(tool);
|
|
332
372
|
}
|
|
333
373
|
|
|
@@ -354,6 +354,18 @@ export function assertPinned(tool) {
|
|
|
354
354
|
}
|
|
355
355
|
return;
|
|
356
356
|
}
|
|
357
|
+
// The download IS the binary, so there is no `binary` path to require — but
|
|
358
|
+
// the checksum matters more here than anywhere else, because the artifact is
|
|
359
|
+
// directly executable rather than something that has to be unpacked first.
|
|
360
|
+
if (tool.install === "release-binary") {
|
|
361
|
+
if (!tool.url || !tool.sha256) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`${tool.name}: a release-binary install needs both url and sha256.\n` +
|
|
364
|
+
`A version bump must move the checksum in the same reviewed commit.`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
357
369
|
if (tool.install === "npm-global") {
|
|
358
370
|
if (!tool.package)
|
|
359
371
|
throw new Error(`${tool.name}: npm-global install needs a package`);
|
|
@@ -361,6 +373,6 @@ export function assertPinned(tool) {
|
|
|
361
373
|
}
|
|
362
374
|
throw new Error(
|
|
363
375
|
`${tool.name}: unknown install method "${tool.install}".\n` +
|
|
364
|
-
`Supported: release-zip, release-tar, release-tree, npm-global.`
|
|
376
|
+
`Supported: release-zip, release-tar, release-tree, release-binary, npm-global.`
|
|
365
377
|
);
|
|
366
378
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn the AWS bootstrap bundle into environment variables the AWS SDKs obey.
|
|
3
|
+
*
|
|
4
|
+
* `LISA_AWS_BOOTSTRAP_JSON` carries the agent's AWS identity as one JSON blob.
|
|
5
|
+
* Materializing it verbatim is not enough, because nothing reads that name:
|
|
6
|
+
* every AWS SDK and the CLI look for `AWS_ACCESS_KEY_ID` and
|
|
7
|
+
* `AWS_SECRET_ACCESS_KEY`, or for a profile.
|
|
8
|
+
*
|
|
9
|
+
* Writing profiles alone is also not enough, and that is the failure this
|
|
10
|
+
* module exists for. A Claude cloud container ships its own
|
|
11
|
+
* `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` in the ambient environment, and
|
|
12
|
+
* **environment variables outrank profile files in the credential chain**. So a
|
|
13
|
+
* session with correctly written `~/.aws/credentials` still failed:
|
|
14
|
+
*
|
|
15
|
+
* aws sts get-caller-identity
|
|
16
|
+
* InvalidClientTokenId: The security token included in the request is invalid.
|
|
17
|
+
*
|
|
18
|
+
* Deriving the pair into the materialized env file fixes it, because that file
|
|
19
|
+
* is sourced into the session and the last export wins. Exporting over a
|
|
20
|
+
* variable this project did not set is a real intrusion, so it is deliberate,
|
|
21
|
+
* narrow, and never silent:
|
|
22
|
+
*
|
|
23
|
+
* - only when the bundle actually parses and carries both halves
|
|
24
|
+
* - never over a value the secret store itself supplies under those names,
|
|
25
|
+
* which is a project stating an explicit intent
|
|
26
|
+
* - recorded in the notes file, so the override is discoverable by the same
|
|
27
|
+
* read-the-note path as any other credential
|
|
28
|
+
* @module aws-bootstrap
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** The names every AWS SDK and the CLI actually read. */
|
|
32
|
+
const ACCESS_KEY = "AWS_ACCESS_KEY_ID";
|
|
33
|
+
const SECRET_KEY = "AWS_SECRET_ACCESS_KEY";
|
|
34
|
+
const REGION = "AWS_DEFAULT_REGION";
|
|
35
|
+
|
|
36
|
+
/** The bundle that carries the agent's identity. */
|
|
37
|
+
export const BOOTSTRAP_KEY = "LISA_AWS_BOOTSTRAP_JSON";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read the bootstrap bundle, treating anything malformed as absent.
|
|
41
|
+
*
|
|
42
|
+
* A parse failure must not take materialization down with it: every other
|
|
43
|
+
* secret in the run is still valid and still needed, and a session with most of
|
|
44
|
+
* its credentials beats a session with none.
|
|
45
|
+
* @param {string} raw Bundle contents.
|
|
46
|
+
* @returns {object|null} Parsed bundle, or null.
|
|
47
|
+
*/
|
|
48
|
+
export function parseBootstrap(raw) {
|
|
49
|
+
if (typeof raw !== "string" || raw.trim() === "") return null;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derive the AWS variables implied by an already-selected secret set.
|
|
60
|
+
*
|
|
61
|
+
* Returns only additions; the caller merges. Nothing here mutates its input,
|
|
62
|
+
* so a caller can decide what to do with the result — including reporting it.
|
|
63
|
+
* @param {Map<string, {value: string}>} selected Secrets by exact name.
|
|
64
|
+
* @returns {Map<string, {value: string, note: string}>} Derived variables.
|
|
65
|
+
*/
|
|
66
|
+
export function deriveAwsEnvironment(selected) {
|
|
67
|
+
const derived = new Map();
|
|
68
|
+
const bundle = parseBootstrap(selected.get(BOOTSTRAP_KEY)?.value);
|
|
69
|
+
if (!bundle) return derived;
|
|
70
|
+
|
|
71
|
+
const accessKeyId = bundle.accessKeyId ?? bundle.aws_access_key_id;
|
|
72
|
+
const secretAccessKey =
|
|
73
|
+
bundle.secretAccessKey ?? bundle.aws_secret_access_key;
|
|
74
|
+
// Both halves or neither. Half a credential is not a weaker credential, it is
|
|
75
|
+
// a confusing failure — the SDK reports a signature error rather than a
|
|
76
|
+
// missing one.
|
|
77
|
+
if (!accessKeyId || !secretAccessKey) return derived;
|
|
78
|
+
|
|
79
|
+
// A project that stores these under their own names has said something
|
|
80
|
+
// explicit. Overriding that would be this module guessing against an operator.
|
|
81
|
+
if (selected.has(ACCESS_KEY) || selected.has(SECRET_KEY)) return derived;
|
|
82
|
+
|
|
83
|
+
const note =
|
|
84
|
+
`Derived from ${BOOTSTRAP_KEY} by lisa-secrets-access. Exported ` +
|
|
85
|
+
`deliberately so it overrides any ambient ${ACCESS_KEY} the host injects — ` +
|
|
86
|
+
`environment variables outrank profile files in the AWS credential chain, ` +
|
|
87
|
+
`so without this a stale host value wins and every call fails with ` +
|
|
88
|
+
`InvalidClientTokenId. This is the assume-only bootstrap identity; real ` +
|
|
89
|
+
`work assumes a role from it.`;
|
|
90
|
+
|
|
91
|
+
derived.set(ACCESS_KEY, { value: String(accessKeyId), note });
|
|
92
|
+
derived.set(SECRET_KEY, { value: String(secretAccessKey), note });
|
|
93
|
+
|
|
94
|
+
const region = bundle.region ?? bundle.defaultRegion;
|
|
95
|
+
if (region && !selected.has(REGION)) {
|
|
96
|
+
derived.set(REGION, {
|
|
97
|
+
value: String(region),
|
|
98
|
+
note: `Derived from ${BOOTSTRAP_KEY} by lisa-secrets-access.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return derived;
|
|
102
|
+
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
writeFileSync,
|
|
28
28
|
} from "node:fs";
|
|
29
29
|
|
|
30
|
+
import { deriveAwsEnvironment } from "./aws-bootstrap.mjs";
|
|
30
31
|
import { renderEnv, renderNotes } from "./envfile.mjs";
|
|
31
32
|
import { fetchAll } from "./providers.mjs";
|
|
32
33
|
import { materializedPaths, readConfig } from "./surfaces.mjs";
|
|
@@ -74,25 +75,47 @@ export function materialize(cfg = readConfig()) {
|
|
|
74
75
|
);
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
// The AWS bundle is stored as one JSON blob under a name no SDK reads, so
|
|
79
|
+
// the variables it implies are derived here. Without them a host-injected
|
|
80
|
+
// AWS_ACCESS_KEY_ID wins — environment variables outrank profile files in the
|
|
81
|
+
// credential chain — and every AWS call fails with InvalidClientTokenId even
|
|
82
|
+
// though the real credential materialized correctly.
|
|
83
|
+
const derived = deriveAwsEnvironment(selected);
|
|
84
|
+
for (const [name, entry] of derived) selected.set(name, entry);
|
|
85
|
+
|
|
77
86
|
const { dir, valuesFile, notesFile } = materializedPaths(cfg.namespace);
|
|
78
87
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
79
88
|
chmodSync(dir, 0o700);
|
|
80
89
|
writeAtomic(valuesFile, renderEnv(selected));
|
|
81
90
|
writeAtomic(notesFile, renderNotes(selected));
|
|
82
|
-
return { count: selected.size, dir };
|
|
91
|
+
return { count: selected.size, derived: derived.size, dir };
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
function main() {
|
|
86
95
|
const cfg = readConfig();
|
|
87
96
|
if (process.argv.includes("--dry-run")) {
|
|
88
97
|
const selected = fetchAll(cfg);
|
|
98
|
+
// Derive here too, or the preview lies in the one place it matters most:
|
|
99
|
+
// the derived names are exactly the ones that override ambient host
|
|
100
|
+
// credentials, so omitting them hides the most surprising effect of the run
|
|
101
|
+
// and reports a smaller count than the real write produces.
|
|
102
|
+
for (const [name, entry] of deriveAwsEnvironment(selected)) {
|
|
103
|
+
selected.set(name, entry);
|
|
104
|
+
}
|
|
89
105
|
const { dir } = materializedPaths(cfg.namespace);
|
|
90
106
|
console.log(`would write ${selected.size} secret(s) to ${dir}`);
|
|
91
107
|
console.log([...selected.keys()].sort().join("\n"));
|
|
92
108
|
return;
|
|
93
109
|
}
|
|
94
|
-
const { count, dir } = materialize(cfg);
|
|
110
|
+
const { count, derived, dir } = materialize(cfg);
|
|
95
111
|
console.log(`materialized ${count} secret(s) and their notes into ${dir}`);
|
|
112
|
+
if (derived > 0) {
|
|
113
|
+
// Said out loud: this run exported variables the host may also have set.
|
|
114
|
+
console.log(
|
|
115
|
+
` ${derived} AWS variable(s) derived from the bootstrap bundle, ` +
|
|
116
|
+
`overriding any ambient value`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
@@ -300,6 +300,44 @@ function installReleaseTree(tool, binDir) {
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Install a pinned artifact that IS the binary, with no archive around it.
|
|
305
|
+
*
|
|
306
|
+
* Several tools worth pinning publish a raw executable rather than an archive —
|
|
307
|
+
* jq ships `jq-linux-amd64` and a `sha256sum.txt`, which is ideal material for a
|
|
308
|
+
* checksummed pin and fits none of the archive kinds.
|
|
309
|
+
*
|
|
310
|
+
* Without this the only entry that passes `assertPinned` is `release-zip`, which
|
|
311
|
+
* then fails at install when `unzip` is handed a binary. A manifest entry that
|
|
312
|
+
* validates and cannot install is worse than one that is rejected outright: the
|
|
313
|
+
* error arrives during provisioning rather than during review.
|
|
314
|
+
*
|
|
315
|
+
* There is no `binary` field here, deliberately — the download is the binary,
|
|
316
|
+
* so there is nothing inside it to name.
|
|
317
|
+
* @param {object} tool Manifest entry.
|
|
318
|
+
* @param {string} binDir Directory to install into.
|
|
319
|
+
*/
|
|
320
|
+
function installReleaseBinary(tool, binDir) {
|
|
321
|
+
const temporary = join(binDir, `.${tool.name}-download`);
|
|
322
|
+
mkdirSync(temporary, { recursive: true });
|
|
323
|
+
try {
|
|
324
|
+
const artifact = join(temporary, tool.name);
|
|
325
|
+
execFileSync("curl", ["-fsSL", tool.url, "-o", artifact], {
|
|
326
|
+
stdio: "inherit",
|
|
327
|
+
});
|
|
328
|
+
// Verify before it reaches a directory on PATH. For this kind that ordering
|
|
329
|
+
// matters more than for the archives: the downloaded file is directly
|
|
330
|
+
// executable, so a wrong artifact placed first is a wrong artifact that can
|
|
331
|
+
// be run.
|
|
332
|
+
verifyChecksum(artifact, tool.sha256, tool.name);
|
|
333
|
+
execFileSync("install", ["-m", "0755", artifact, join(binDir, tool.name)], {
|
|
334
|
+
stdio: "inherit",
|
|
335
|
+
});
|
|
336
|
+
} finally {
|
|
337
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
303
341
|
/**
|
|
304
342
|
* Install a pinned global npm package.
|
|
305
343
|
* @param {object} tool Manifest entry.
|
|
@@ -328,6 +366,8 @@ export function installTool(tool, binDir) {
|
|
|
328
366
|
if (tool.install === "release-zip") installReleaseZip(tool, binDir);
|
|
329
367
|
else if (tool.install === "release-tar") installReleaseTar(tool, binDir);
|
|
330
368
|
else if (tool.install === "release-tree") installReleaseTree(tool, binDir);
|
|
369
|
+
else if (tool.install === "release-binary")
|
|
370
|
+
installReleaseBinary(tool, binDir);
|
|
331
371
|
else installNpmGlobal(tool);
|
|
332
372
|
}
|
|
333
373
|
|
|
@@ -354,6 +354,18 @@ export function assertPinned(tool) {
|
|
|
354
354
|
}
|
|
355
355
|
return;
|
|
356
356
|
}
|
|
357
|
+
// The download IS the binary, so there is no `binary` path to require — but
|
|
358
|
+
// the checksum matters more here than anywhere else, because the artifact is
|
|
359
|
+
// directly executable rather than something that has to be unpacked first.
|
|
360
|
+
if (tool.install === "release-binary") {
|
|
361
|
+
if (!tool.url || !tool.sha256) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`${tool.name}: a release-binary install needs both url and sha256.\n` +
|
|
364
|
+
`A version bump must move the checksum in the same reviewed commit.`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
357
369
|
if (tool.install === "npm-global") {
|
|
358
370
|
if (!tool.package)
|
|
359
371
|
throw new Error(`${tool.name}: npm-global install needs a package`);
|
|
@@ -361,6 +373,6 @@ export function assertPinned(tool) {
|
|
|
361
373
|
}
|
|
362
374
|
throw new Error(
|
|
363
375
|
`${tool.name}: unknown install method "${tool.install}".\n` +
|
|
364
|
-
`Supported: release-zip, release-tar, release-tree, npm-global.`
|
|
376
|
+
`Supported: release-zip, release-tar, release-tree, release-binary, npm-global.`
|
|
365
377
|
);
|
|
366
378
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn the AWS bootstrap bundle into environment variables the AWS SDKs obey.
|
|
3
|
+
*
|
|
4
|
+
* `LISA_AWS_BOOTSTRAP_JSON` carries the agent's AWS identity as one JSON blob.
|
|
5
|
+
* Materializing it verbatim is not enough, because nothing reads that name:
|
|
6
|
+
* every AWS SDK and the CLI look for `AWS_ACCESS_KEY_ID` and
|
|
7
|
+
* `AWS_SECRET_ACCESS_KEY`, or for a profile.
|
|
8
|
+
*
|
|
9
|
+
* Writing profiles alone is also not enough, and that is the failure this
|
|
10
|
+
* module exists for. A Claude cloud container ships its own
|
|
11
|
+
* `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` in the ambient environment, and
|
|
12
|
+
* **environment variables outrank profile files in the credential chain**. So a
|
|
13
|
+
* session with correctly written `~/.aws/credentials` still failed:
|
|
14
|
+
*
|
|
15
|
+
* aws sts get-caller-identity
|
|
16
|
+
* InvalidClientTokenId: The security token included in the request is invalid.
|
|
17
|
+
*
|
|
18
|
+
* Deriving the pair into the materialized env file fixes it, because that file
|
|
19
|
+
* is sourced into the session and the last export wins. Exporting over a
|
|
20
|
+
* variable this project did not set is a real intrusion, so it is deliberate,
|
|
21
|
+
* narrow, and never silent:
|
|
22
|
+
*
|
|
23
|
+
* - only when the bundle actually parses and carries both halves
|
|
24
|
+
* - never over a value the secret store itself supplies under those names,
|
|
25
|
+
* which is a project stating an explicit intent
|
|
26
|
+
* - recorded in the notes file, so the override is discoverable by the same
|
|
27
|
+
* read-the-note path as any other credential
|
|
28
|
+
* @module aws-bootstrap
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** The names every AWS SDK and the CLI actually read. */
|
|
32
|
+
const ACCESS_KEY = "AWS_ACCESS_KEY_ID";
|
|
33
|
+
const SECRET_KEY = "AWS_SECRET_ACCESS_KEY";
|
|
34
|
+
const REGION = "AWS_DEFAULT_REGION";
|
|
35
|
+
|
|
36
|
+
/** The bundle that carries the agent's identity. */
|
|
37
|
+
export const BOOTSTRAP_KEY = "LISA_AWS_BOOTSTRAP_JSON";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read the bootstrap bundle, treating anything malformed as absent.
|
|
41
|
+
*
|
|
42
|
+
* A parse failure must not take materialization down with it: every other
|
|
43
|
+
* secret in the run is still valid and still needed, and a session with most of
|
|
44
|
+
* its credentials beats a session with none.
|
|
45
|
+
* @param {string} raw Bundle contents.
|
|
46
|
+
* @returns {object|null} Parsed bundle, or null.
|
|
47
|
+
*/
|
|
48
|
+
export function parseBootstrap(raw) {
|
|
49
|
+
if (typeof raw !== "string" || raw.trim() === "") return null;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derive the AWS variables implied by an already-selected secret set.
|
|
60
|
+
*
|
|
61
|
+
* Returns only additions; the caller merges. Nothing here mutates its input,
|
|
62
|
+
* so a caller can decide what to do with the result — including reporting it.
|
|
63
|
+
* @param {Map<string, {value: string}>} selected Secrets by exact name.
|
|
64
|
+
* @returns {Map<string, {value: string, note: string}>} Derived variables.
|
|
65
|
+
*/
|
|
66
|
+
export function deriveAwsEnvironment(selected) {
|
|
67
|
+
const derived = new Map();
|
|
68
|
+
const bundle = parseBootstrap(selected.get(BOOTSTRAP_KEY)?.value);
|
|
69
|
+
if (!bundle) return derived;
|
|
70
|
+
|
|
71
|
+
const accessKeyId = bundle.accessKeyId ?? bundle.aws_access_key_id;
|
|
72
|
+
const secretAccessKey =
|
|
73
|
+
bundle.secretAccessKey ?? bundle.aws_secret_access_key;
|
|
74
|
+
// Both halves or neither. Half a credential is not a weaker credential, it is
|
|
75
|
+
// a confusing failure — the SDK reports a signature error rather than a
|
|
76
|
+
// missing one.
|
|
77
|
+
if (!accessKeyId || !secretAccessKey) return derived;
|
|
78
|
+
|
|
79
|
+
// A project that stores these under their own names has said something
|
|
80
|
+
// explicit. Overriding that would be this module guessing against an operator.
|
|
81
|
+
if (selected.has(ACCESS_KEY) || selected.has(SECRET_KEY)) return derived;
|
|
82
|
+
|
|
83
|
+
const note =
|
|
84
|
+
`Derived from ${BOOTSTRAP_KEY} by lisa-secrets-access. Exported ` +
|
|
85
|
+
`deliberately so it overrides any ambient ${ACCESS_KEY} the host injects — ` +
|
|
86
|
+
`environment variables outrank profile files in the AWS credential chain, ` +
|
|
87
|
+
`so without this a stale host value wins and every call fails with ` +
|
|
88
|
+
`InvalidClientTokenId. This is the assume-only bootstrap identity; real ` +
|
|
89
|
+
`work assumes a role from it.`;
|
|
90
|
+
|
|
91
|
+
derived.set(ACCESS_KEY, { value: String(accessKeyId), note });
|
|
92
|
+
derived.set(SECRET_KEY, { value: String(secretAccessKey), note });
|
|
93
|
+
|
|
94
|
+
const region = bundle.region ?? bundle.defaultRegion;
|
|
95
|
+
if (region && !selected.has(REGION)) {
|
|
96
|
+
derived.set(REGION, {
|
|
97
|
+
value: String(region),
|
|
98
|
+
note: `Derived from ${BOOTSTRAP_KEY} by lisa-secrets-access.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return derived;
|
|
102
|
+
}
|