@codyswann/lisa 2.340.0 → 2.341.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/cli/environment-cmd.d.ts +40 -0
- package/dist/cli/environment-cmd.d.ts.map +1 -0
- package/dist/cli/environment-cmd.js +84 -0
- package/dist/cli/environment-cmd.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +13 -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/bootstrap-store.mjs +33 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +19 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +27 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/environment.mjs +227 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +136 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +33 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +19 -3
- package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +27 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/environment.mjs +227 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +136 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +33 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +19 -3
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +27 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/environment.mjs +227 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +136 -0
- 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/bootstrap-store.mjs +33 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +19 -3
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +27 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/environment.mjs +227 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +136 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +33 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +19 -3
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +27 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/environment.mjs +227 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +136 -0
- 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/bootstrap-store.mjs +33 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +19 -3
- package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +27 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/environment.mjs +227 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +136 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Configure one surface for one tenant.
|
|
4
|
+
*
|
|
5
|
+
* One verb, one axis — *which surface* — and the only difference between them
|
|
6
|
+
* is whether Lisa can execute there or has to hand an operator text to paste.
|
|
7
|
+
*
|
|
8
|
+
* environment local runs it here
|
|
9
|
+
* environment container emits an image definition and a run command
|
|
10
|
+
* environment claude-web emits, because the dialog has no API
|
|
11
|
+
* environment codex-cloud emits, same reason
|
|
12
|
+
*
|
|
13
|
+
* This replaces `remote-env --emit=<surface>`, whose name described the
|
|
14
|
+
* machinery rather than the task: from a laptop, `remote-env` reads as
|
|
15
|
+
* "prepare the remote environment I am currently in", which is the opposite of
|
|
16
|
+
* what an operator configuring a cloud environment is doing. The old spelling
|
|
17
|
+
* still works so nothing in flight breaks.
|
|
18
|
+
*
|
|
19
|
+
* `workstation` remains a separate command and a separate concern: it answers
|
|
20
|
+
* "what binaries does this machine have", with no tenant and no credentials. So
|
|
21
|
+
* **rotating a token is `environment local` again**, not re-running an
|
|
22
|
+
* installer that wants to reinstall six coding agents.
|
|
23
|
+
* @module environment
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { pathToFileURL } from "node:url";
|
|
27
|
+
|
|
28
|
+
/** Surfaces this command can configure, and how each is delivered. */
|
|
29
|
+
export const TARGETS = {
|
|
30
|
+
local: { mode: "run", label: "this machine" },
|
|
31
|
+
container: { mode: "emit", label: "a container image you build" },
|
|
32
|
+
"claude-web": { mode: "emit", label: "a Claude cloud environment" },
|
|
33
|
+
"codex-cloud": { mode: "emit", label: "a Codex Cloud environment" },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the surface name an operator asked for.
|
|
38
|
+
*
|
|
39
|
+
* An unknown name is an error rather than a default, because defaulting would
|
|
40
|
+
* configure something other than what they typed — and on this command the
|
|
41
|
+
* difference between surfaces is where a credential ends up.
|
|
42
|
+
* @param {string|undefined} name Requested surface.
|
|
43
|
+
* @returns {string} A key of {@link TARGETS}.
|
|
44
|
+
*/
|
|
45
|
+
export function assertTarget(name) {
|
|
46
|
+
if (!name || !TARGETS[name]) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`unknown environment "${name ?? ""}".\n` +
|
|
49
|
+
`Known: ${Object.keys(TARGETS).join(", ")}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return name;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Prepare THIS machine: store the bootstrap, then materialize.
|
|
57
|
+
*
|
|
58
|
+
* The prompt is skipped rather than blocked when nothing can answer it, and
|
|
59
|
+
* skipped rather than repeated when the machine already holds a bootstrap —
|
|
60
|
+
* `environment local` is also how an operator re-materializes after rotating a
|
|
61
|
+
* secret in the vault, and demanding the token again every time would make the
|
|
62
|
+
* common case the tedious one. `--rotate` is how you say you mean to replace it.
|
|
63
|
+
* @param {object} options Resolved tenant, provider and flags.
|
|
64
|
+
* @param {object} [deps] Injected seams, for tests.
|
|
65
|
+
* @returns {Promise<string[]>} Lines to report.
|
|
66
|
+
*/
|
|
67
|
+
export async function configureLocal(options, deps = {}) {
|
|
68
|
+
const { tenant, provider, bootstrapKey: key, rotate = false } = options;
|
|
69
|
+
const lines = [];
|
|
70
|
+
|
|
71
|
+
// A tenant is required here, unlike the emit paths, because this one WRITES.
|
|
72
|
+
// Without it the config falls back to the default namespace and materializes
|
|
73
|
+
// one tenant's credentials into another's directory — and on a machine
|
|
74
|
+
// serving several, the two would then share a store.
|
|
75
|
+
if (!tenant) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"environment local needs --tenant=<name>.\n" +
|
|
78
|
+
"It writes credentials into a per-tenant directory, so guessing the " +
|
|
79
|
+
"name would put one tenant's secrets where another's sessions read."
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const wired = deps.prompt
|
|
84
|
+
? deps
|
|
85
|
+
: { ...(await localDeps({ tenant, provider })), ...deps };
|
|
86
|
+
const { canPrompt, promptSecret } = wired.prompt;
|
|
87
|
+
const { storeBootstrap, hasBootstrap } = wired.store;
|
|
88
|
+
const materialize = wired.materialize;
|
|
89
|
+
|
|
90
|
+
if (!key) {
|
|
91
|
+
lines.push(
|
|
92
|
+
`${provider} has no environment-variable bootstrap, so there is nothing`,
|
|
93
|
+
`to store. Authenticate it the way its own CLI expects.`
|
|
94
|
+
);
|
|
95
|
+
} else if (hasBootstrap(key) && !rotate) {
|
|
96
|
+
lines.push(`${key} is already stored; pass --rotate to replace it.`);
|
|
97
|
+
} else if (!canPrompt()) {
|
|
98
|
+
// Not fatal. A non-interactive caller can legitimately have supplied the
|
|
99
|
+
// bootstrap through the environment, and refusing would break that.
|
|
100
|
+
lines.push(
|
|
101
|
+
`No terminal to prompt on, so ${key} was not stored.`,
|
|
102
|
+
`Set it in the environment, or re-run where a human can type.`
|
|
103
|
+
);
|
|
104
|
+
} else {
|
|
105
|
+
const value = promptSecret(`Paste the access token for ${tenant}: `);
|
|
106
|
+
if (!value) {
|
|
107
|
+
lines.push(`Nothing entered, so ${key} was not stored.`);
|
|
108
|
+
} else {
|
|
109
|
+
const { where } = storeBootstrap(key, value);
|
|
110
|
+
lines.push(`Stored ${key} in ${where}.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const result = await materialize();
|
|
115
|
+
lines.push(...result);
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Run the command.
|
|
121
|
+
* @param {string[]} argv Arguments after the command name.
|
|
122
|
+
* @param {object} [deps] Injected seams, for tests.
|
|
123
|
+
* @returns {Promise<number>} Exit code.
|
|
124
|
+
*/
|
|
125
|
+
export async function run(argv, deps = {}) {
|
|
126
|
+
const out = deps.log ?? console.log;
|
|
127
|
+
const err = deps.error ?? console.error;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const target = assertTarget(argv.find(a => !a.startsWith("-")));
|
|
131
|
+
const resolve = deps.resolveTarget ?? (await defaultResolver());
|
|
132
|
+
const identity = await resolve(argv);
|
|
133
|
+
|
|
134
|
+
if (TARGETS[target].mode === "run") {
|
|
135
|
+
const lines = await (deps.configureLocal ?? configureLocal)(
|
|
136
|
+
{ ...identity, rotate: argv.includes("--rotate") },
|
|
137
|
+
deps
|
|
138
|
+
);
|
|
139
|
+
out(lines.join("\n"));
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
out((deps.emit ?? (await defaultEmitter()))(target, identity));
|
|
144
|
+
return 0;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
err(String(error.message));
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Executed directly by the CLI, imported by tests. Without this the module
|
|
152
|
+
// exported a `run` nobody called and the command printed nothing at all.
|
|
153
|
+
if (
|
|
154
|
+
process.argv[1] &&
|
|
155
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
156
|
+
) {
|
|
157
|
+
run(process.argv.slice(2)).then(code => {
|
|
158
|
+
process.exitCode = code;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Wire the local path to the real prompt, store and materializer.
|
|
164
|
+
*
|
|
165
|
+
* Loaded lazily so the emit paths — which need none of it — do not pay for it,
|
|
166
|
+
* and so tests can substitute the lot without touching a keychain or a vault.
|
|
167
|
+
*
|
|
168
|
+
* `materialize` is called with `requested: true`: the operator typed the
|
|
169
|
+
* command whose purpose is to put credentials on this machine, which is a
|
|
170
|
+
* different question from whether an automated flow may write here. The
|
|
171
|
+
* capability guard still refuses the automated case.
|
|
172
|
+
* @returns {Promise<object>} Default dependencies for `configureLocal`.
|
|
173
|
+
*/
|
|
174
|
+
async function localDeps(identity) {
|
|
175
|
+
const at = name =>
|
|
176
|
+
new URL(`../../lisa-secrets-access/scripts/${name}`, import.meta.url).href;
|
|
177
|
+
const prompt = await import(at("prompt-secret.mjs"));
|
|
178
|
+
const store = await import(at("bootstrap-store.mjs"));
|
|
179
|
+
const secrets = await import(at("materialize-secrets.mjs"));
|
|
180
|
+
const surfaces = await import(at("surfaces.mjs"));
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
prompt,
|
|
184
|
+
store,
|
|
185
|
+
materialize: () => {
|
|
186
|
+
// The NAMED tenant, not whatever config the working directory holds.
|
|
187
|
+
// Reading the directory materialized the default namespace instead of the
|
|
188
|
+
// one on the command line — observed writing 49 secrets of the wrong
|
|
189
|
+
// tenant into the wrong directory.
|
|
190
|
+
const { count, derived, dir, profiles } = secrets.materialize(
|
|
191
|
+
surfaces.configForTenant(identity),
|
|
192
|
+
{ requested: true }
|
|
193
|
+
);
|
|
194
|
+
return [
|
|
195
|
+
`Materialized ${count} secret(s) into ${dir}.`,
|
|
196
|
+
...(derived ? [`${derived} variable(s) derived from the bundle.`] : []),
|
|
197
|
+
...(profiles?.length
|
|
198
|
+
? [`AWS profiles installed: ${profiles.join(", ")}`]
|
|
199
|
+
: []),
|
|
200
|
+
];
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Load the identity resolver from the sibling runner.
|
|
207
|
+
* @returns {Promise<Function>} The resolver.
|
|
208
|
+
*/
|
|
209
|
+
async function defaultResolver() {
|
|
210
|
+
// Already a URL — passing it through pathToFileURL again throws.
|
|
211
|
+
const mod = await import(
|
|
212
|
+
new URL("./setup-remote-env.mjs", import.meta.url).href
|
|
213
|
+
);
|
|
214
|
+
return argv => mod.resolveEmitTarget(argv);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Load the emitter from the sibling runner.
|
|
219
|
+
* @returns {Promise<Function>} The emitter.
|
|
220
|
+
*/
|
|
221
|
+
async function defaultEmitter() {
|
|
222
|
+
// Already a URL — passing it through pathToFileURL again throws.
|
|
223
|
+
const mod = await import(
|
|
224
|
+
new URL("./setup-remote-env.mjs", import.meta.url).href
|
|
225
|
+
);
|
|
226
|
+
return (target, identity) => mod.emitFor(target, identity);
|
|
227
|
+
}
|
|
@@ -1030,6 +1030,142 @@ export function emitClaudeWeb({
|
|
|
1030
1030
|
].join("\n");
|
|
1031
1031
|
}
|
|
1032
1032
|
|
|
1033
|
+
/**
|
|
1034
|
+
* Emit the configuration for a surface Lisa cannot reach.
|
|
1035
|
+
*
|
|
1036
|
+
* Dispatch rather than four call sites, so `environment` names a surface and
|
|
1037
|
+
* nothing downstream has to know which of them has an API and which does not.
|
|
1038
|
+
* @param {string} target Surface name.
|
|
1039
|
+
* @param {object} identity Resolved tenant, provider and bootstrap key.
|
|
1040
|
+
* @returns {string} Text to show the operator.
|
|
1041
|
+
*/
|
|
1042
|
+
export function emitFor(target, identity) {
|
|
1043
|
+
if (target === "claude-web") return emitClaudeWeb(identity);
|
|
1044
|
+
if (target === "codex-cloud") return emitCodexCloud(identity);
|
|
1045
|
+
if (target === "container") return emitContainer(identity);
|
|
1046
|
+
throw new Error(`no emit template for surface "${target}".`);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Produce the configuration a human pastes into a Codex Cloud environment.
|
|
1051
|
+
*
|
|
1052
|
+
* Differs from Claude's in three ways that are properties of the vendor rather
|
|
1053
|
+
* than of Lisa, and each has cost someone a debugging session.
|
|
1054
|
+
* @param {{bootstrapKey: string|null, tenant?: string|null, provider?: string}} options Project details.
|
|
1055
|
+
* @returns {string} Text to show the operator.
|
|
1056
|
+
*/
|
|
1057
|
+
export function emitCodexCloud({
|
|
1058
|
+
bootstrapKey,
|
|
1059
|
+
tenant = null,
|
|
1060
|
+
provider = null,
|
|
1061
|
+
}) {
|
|
1062
|
+
const key = bootstrapKey ?? "<not resolved>";
|
|
1063
|
+
const namespace = tenant ?? "<your namespace>";
|
|
1064
|
+
return [
|
|
1065
|
+
"Provisioning tier: EMIT — Codex Cloud exposes no environment API.",
|
|
1066
|
+
" `codex cloud` offers exec, status and list; nothing that writes an",
|
|
1067
|
+
" environment. So this is text for the settings page.",
|
|
1068
|
+
"",
|
|
1069
|
+
"Paste into the environment settings",
|
|
1070
|
+
"-----------------------------------",
|
|
1071
|
+
" Repository: the DEFAULT checkout for this environment",
|
|
1072
|
+
"",
|
|
1073
|
+
" Environment variables — NOT task secrets:",
|
|
1074
|
+
// Written as name=value, not as `export name='value'`. This is a settings
|
|
1075
|
+
// form, not a shell: the quotes would be stored as part of the value, and
|
|
1076
|
+
// the surface named must be this one rather than the shell block's.
|
|
1077
|
+
` LISA_TENANT=${namespace}`,
|
|
1078
|
+
...(bootstrapKey
|
|
1079
|
+
? [` ${key}=<read this from your credential manager>`]
|
|
1080
|
+
: [` (${provider} has no environment-variable bootstrap)`]),
|
|
1081
|
+
...(provider ? [` LISA_PROVIDER=${provider}`] : []),
|
|
1082
|
+
" LISA_SECRETS_SURFACE=codex-cloud",
|
|
1083
|
+
"",
|
|
1084
|
+
" Setup and cache-resume maintenance both run BEFORE task secrets",
|
|
1085
|
+
" exist, so a bootstrap placed in the secrets box is invisible to the",
|
|
1086
|
+
" only two steps that need it.",
|
|
1087
|
+
"",
|
|
1088
|
+
" Setup script:",
|
|
1089
|
+
` ${SETUP_FIELD}`,
|
|
1090
|
+
"",
|
|
1091
|
+
" Maintenance script:",
|
|
1092
|
+
" The SAME line. It runs when a container resumes from cache, and every",
|
|
1093
|
+
" step is idempotent and version-aware — so running it again is how a",
|
|
1094
|
+
" rotated value, an edited note, or a changed pin gets picked up.",
|
|
1095
|
+
"",
|
|
1096
|
+
"Watch for",
|
|
1097
|
+
"---------",
|
|
1098
|
+
" CODEX_ENV_NODE_VERSION overrides the repository's own .nvmrc and",
|
|
1099
|
+
" engines. An environment left on an older major breaks setup for a repo",
|
|
1100
|
+
" that requires a newer one, and the error blames the package manager.",
|
|
1101
|
+
"",
|
|
1102
|
+
" Codex clones `main`, not the repository's default branch. If those",
|
|
1103
|
+
" differ here, that is where the confusion starts.",
|
|
1104
|
+
"",
|
|
1105
|
+
" Setup logs are visible only in the Codex UI; no CLI retrieves them.",
|
|
1106
|
+
].join("\n");
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Produce an image definition and a run command for a container.
|
|
1111
|
+
*
|
|
1112
|
+
* Split across build and run on purpose: the binaries are baked once, and the
|
|
1113
|
+
* credential arrives at `docker run`. Baking it would put it in an image layer,
|
|
1114
|
+
* readable by anyone who can pull the image and surviving every
|
|
1115
|
+
* `docker history` — so the Dockerfile deliberately contains no secret.
|
|
1116
|
+
* @param {{bootstrapKey: string|null, tenant?: string|null, provider?: string}} options Project details.
|
|
1117
|
+
* @returns {string} Text to show the operator.
|
|
1118
|
+
*/
|
|
1119
|
+
export function emitContainer({
|
|
1120
|
+
bootstrapKey,
|
|
1121
|
+
tenant = null,
|
|
1122
|
+
provider = null,
|
|
1123
|
+
}) {
|
|
1124
|
+
const key = bootstrapKey ?? "<not resolved>";
|
|
1125
|
+
const namespace = tenant ?? "<your namespace>";
|
|
1126
|
+
return [
|
|
1127
|
+
"Provisioning tier: EMIT — an image is built from a file you own.",
|
|
1128
|
+
"",
|
|
1129
|
+
"Dockerfile",
|
|
1130
|
+
"----------",
|
|
1131
|
+
" Generate it with the bootstrap's own emitter, so the image and this",
|
|
1132
|
+
" machine install the identical set:",
|
|
1133
|
+
"",
|
|
1134
|
+
" npx -y @codyswann/lisa@latest workstation --print-dockerfile \\",
|
|
1135
|
+
` --provider=${provider ?? "bitwarden"} > Dockerfile.lisa`,
|
|
1136
|
+
" docker build -f Dockerfile.lisa -t lisa-workstation .",
|
|
1137
|
+
"",
|
|
1138
|
+
" Nothing secret belongs in it. An image layer is readable by anyone who",
|
|
1139
|
+
" can pull the image and survives every `docker history`, so the",
|
|
1140
|
+
" credential arrives at run time instead.",
|
|
1141
|
+
"",
|
|
1142
|
+
"docker run",
|
|
1143
|
+
"----------",
|
|
1144
|
+
" docker run --rm -it \\",
|
|
1145
|
+
` -e LISA_TENANT=${namespace} \\`,
|
|
1146
|
+
` -e ${key}="<this image's own access token>" \\`,
|
|
1147
|
+
...(provider ? [` -e LISA_PROVIDER=${provider} \\`] : []),
|
|
1148
|
+
" -e LISA_SECRETS_SURFACE=container \\",
|
|
1149
|
+
' -v "$PWD:/work" -w /work \\',
|
|
1150
|
+
" lisa-workstation",
|
|
1151
|
+
"",
|
|
1152
|
+
" LISA_SECRETS_SURFACE=container is what makes credentials materialize.",
|
|
1153
|
+
" Left to detection a container reads as `local`, which is",
|
|
1154
|
+
" materialized:false — right for a human at a keyboard whose provider CLI",
|
|
1155
|
+
" is authenticated, wrong for a container that has no keychain and dies",
|
|
1156
|
+
" with its filesystem.",
|
|
1157
|
+
"",
|
|
1158
|
+
" Give the image its own access token rather than reusing this machine's.",
|
|
1159
|
+
" If the image is shared, so is anything its token can read.",
|
|
1160
|
+
"",
|
|
1161
|
+
"Mounted repositories need nothing further",
|
|
1162
|
+
"-----------------------------------------",
|
|
1163
|
+
" A checkout you have already run `apply` and `sync` on carries its",
|
|
1164
|
+
" .lisa.config.json with it. Cloning INSIDE the container instead means",
|
|
1165
|
+
" running those there.",
|
|
1166
|
+
].join("\n");
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1033
1169
|
/**
|
|
1034
1170
|
* Report tooling the project appears to need but has not declared.
|
|
1035
1171
|
*
|
|
@@ -183,6 +183,39 @@ export function clearBootstrap(key, deps = {}) {
|
|
|
183
183
|
remove(deps.path ?? bootstrapFile(key, env), { force: true });
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Whether this machine already holds a bootstrap under that name.
|
|
188
|
+
*
|
|
189
|
+
* Asked so `environment local` can re-materialize after a rotation in the vault
|
|
190
|
+
* without demanding the token again — that is the common case, and prompting
|
|
191
|
+
* every time would make it the tedious one.
|
|
192
|
+
*
|
|
193
|
+
* Presence only. The value is never read here, because nothing about deciding
|
|
194
|
+
* whether to prompt needs it.
|
|
195
|
+
* @param {string} key Bootstrap variable name.
|
|
196
|
+
* @param {object} [deps] Injected seams, for tests.
|
|
197
|
+
* @returns {boolean} True when a value is stored.
|
|
198
|
+
*/
|
|
199
|
+
export function hasBootstrap(key, deps = {}) {
|
|
200
|
+
const kind = deps.kind ?? storeKind();
|
|
201
|
+
const env = deps.env ?? process.env;
|
|
202
|
+
|
|
203
|
+
if (kind === "keychain") {
|
|
204
|
+
const run = deps.run ?? execFileSync;
|
|
205
|
+
try {
|
|
206
|
+
run(
|
|
207
|
+
"security",
|
|
208
|
+
["find-generic-password", "-s", assertKey(key), "-a", env.USER ?? ""],
|
|
209
|
+
{ stdio: "ignore" }
|
|
210
|
+
);
|
|
211
|
+
return true;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return (deps.exists ?? existsSync)(deps.path ?? bootstrapFile(key, env));
|
|
217
|
+
}
|
|
218
|
+
|
|
186
219
|
/**
|
|
187
220
|
* Read a file-backed bootstrap, treating absence as empty.
|
|
188
221
|
*
|
|
@@ -299,12 +299,28 @@ export function installAwsProfiles(bundle, options = {}) {
|
|
|
299
299
|
return rendered.profiles;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
export function materialize(cfg = readConfig()) {
|
|
303
|
-
|
|
302
|
+
export function materialize(cfg = readConfig(), options = {}) {
|
|
303
|
+
// `requested` distinguishes an OPERATOR asking from a flow deciding.
|
|
304
|
+
//
|
|
305
|
+
// The capability governs automated behaviour: a session-start hook on a
|
|
306
|
+
// laptop must not write credentials to disk, because the provider CLI is
|
|
307
|
+
// authenticated there and a copy would add drift and exposure without adding
|
|
308
|
+
// capability. That reasoning holds, and the guard below still enforces it.
|
|
309
|
+
//
|
|
310
|
+
// It does not hold for `lisa environment local`, where the operator typed the
|
|
311
|
+
// command whose entire purpose is to put credentials on this machine — the
|
|
312
|
+
// AWS `-static` profiles reference variables that exist only once the env
|
|
313
|
+
// file is materialized, so refusing there means the profiles never work.
|
|
314
|
+
//
|
|
315
|
+
// Two different questions, so two different answers, rather than flipping
|
|
316
|
+
// `local` to `materialized: true` and silently starting to write on every
|
|
317
|
+
// machine that upgrades.
|
|
318
|
+
if (!cfg.capabilities.mayWriteValues && !options.requested) {
|
|
304
319
|
throw new Error(
|
|
305
320
|
`surface "${cfg.surface}" may not write secret values to disk.\n` +
|
|
306
321
|
`It can read through to the provider, so a copy on disk would add drift ` +
|
|
307
|
-
`and exposure without adding capability
|
|
322
|
+
`and exposure without adding capability.\n` +
|
|
323
|
+
`Run 'lisa environment local --tenant=<name>' to ask for it explicitly.`
|
|
308
324
|
);
|
|
309
325
|
}
|
|
310
326
|
|
|
@@ -252,6 +252,33 @@ function resolveBootstrap(bootstrap, provider, namespace) {
|
|
|
252
252
|
};
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Build a configuration for a tenant the operator named outright.
|
|
257
|
+
*
|
|
258
|
+
* Deliberately ignores any `.lisa.config.json` in the working directory. The
|
|
259
|
+
* flag is the more specific statement — someone who typed `--tenant=acme` means
|
|
260
|
+
* acme, whichever repository they happen to be standing in — and silently
|
|
261
|
+
* preferring the file would materialize a different tenant than the one on the
|
|
262
|
+
* command line.
|
|
263
|
+
*
|
|
264
|
+
* Getting this wrong is not a cosmetic error. Every namespace is a directory
|
|
265
|
+
* under `$XDG_CONFIG_HOME`, so resolving to the wrong one writes one tenant's
|
|
266
|
+
* credentials where another tenant's sessions read, and on a machine serving
|
|
267
|
+
* several the two would share a store.
|
|
268
|
+
* @param {{tenant: string, provider?: string}} options Named identity.
|
|
269
|
+
* @param {Record<string, string|undefined>} [env] Environment to inherit.
|
|
270
|
+
* @returns {object} A resolved configuration.
|
|
271
|
+
*/
|
|
272
|
+
export function configForTenant({ tenant, provider }, env = process.env) {
|
|
273
|
+
return withSurface(
|
|
274
|
+
fromEnvironment({
|
|
275
|
+
...env,
|
|
276
|
+
LISA_TENANT: tenant,
|
|
277
|
+
...(provider ? { LISA_PROVIDER: provider } : {}),
|
|
278
|
+
})
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
255
282
|
/**
|
|
256
283
|
* Build a configuration from the environment, for sessions with no checkout.
|
|
257
284
|
*
|