@codyswann/lisa 2.338.4 → 2.339.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 +5 -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/providers.mjs +23 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +68 -4
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +117 -8
- package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +23 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +68 -4
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +117 -8
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +23 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +68 -4
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +117 -8
- 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/providers.mjs +23 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +68 -4
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +117 -8
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +23 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +68 -4
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +117 -8
- 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/providers.mjs +23 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +68 -4
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +117 -8
|
@@ -51,6 +51,29 @@ const PROVIDER_BOOTSTRAP_ENV = {
|
|
|
51
51
|
doppler: "DOPPLER_TOKEN",
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The tenant-scoped bootstrap variable a provider expects, by convention.
|
|
56
|
+
*
|
|
57
|
+
* Exists so the repo-less path cannot invent one. It used to compose
|
|
58
|
+
* `BWS_ACCESS_TOKEN_<namespace>` literally, ignoring the provider it had just
|
|
59
|
+
* resolved — so a Doppler tenant with no checkout was told to set a Bitwarden
|
|
60
|
+
* variable, and its CLI failed with "Missing access token". That is precisely
|
|
61
|
+
* the confusion the two questions above are separated to prevent, reintroduced
|
|
62
|
+
* on the one surface with no config file to correct it.
|
|
63
|
+
*
|
|
64
|
+
* `null` for a provider with no env-var bootstrap — 1Password, Vault and AWS
|
|
65
|
+
* authenticate by other means — because a name invented for them would be a
|
|
66
|
+
* variable nothing reads, which is worse than admitting there is none.
|
|
67
|
+
* `providerEnv` already treats an unmapped provider as "inject nothing".
|
|
68
|
+
* @param {string} provider Provider name.
|
|
69
|
+
* @param {string} namespace Tenant namespace.
|
|
70
|
+
* @returns {string|null} The variable to set, or null when the provider has none.
|
|
71
|
+
*/
|
|
72
|
+
export function bootstrapKeyFor(provider, namespace) {
|
|
73
|
+
const canonical = PROVIDER_BOOTSTRAP_ENV[provider];
|
|
74
|
+
return canonical ? `${canonical}_${namespace}` : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
54
77
|
/**
|
|
55
78
|
* Build the child environment for a provider CLI, injecting the bootstrap under
|
|
56
79
|
* the name that CLI actually reads.
|
|
@@ -17,6 +17,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
|
|
20
|
+
import { bootstrapKeyFor } from "./providers.mjs";
|
|
21
|
+
|
|
20
22
|
/**
|
|
21
23
|
* Capabilities, per surface.
|
|
22
24
|
*
|
|
@@ -79,6 +81,28 @@ export const SURFACES = {
|
|
|
79
81
|
// recoverable; an absent one means the environment does not work.
|
|
80
82
|
materializeAt: "both",
|
|
81
83
|
},
|
|
84
|
+
// A container an operator builds and runs themselves — locally, or anywhere
|
|
85
|
+
// Lisa is not the one provisioning the host.
|
|
86
|
+
//
|
|
87
|
+
// It exists because `local` was standing in for it, and `local` is
|
|
88
|
+
// `materialized: false`. That default is right for a human at a keyboard: the
|
|
89
|
+
// provider CLI is authenticated, secrets resolve live, and writing them to
|
|
90
|
+
// disk would be a second copy to keep honest. A container has no keychain and
|
|
91
|
+
// dies with its filesystem, so the same default leaves it with the CLIs
|
|
92
|
+
// installed and nothing to authenticate them.
|
|
93
|
+
//
|
|
94
|
+
// The workaround was to claim `claude-web`, which materializes correctly and
|
|
95
|
+
// is a lie — it made a local container indistinguishable from a cloud session
|
|
96
|
+
// in every diagnostic and log.
|
|
97
|
+
//
|
|
98
|
+
// `setup` rather than `both`: a container has no session-start hook to run,
|
|
99
|
+
// and it is started fresh rather than resumed from a vendor's cache, so the
|
|
100
|
+
// start of the container IS the refresh.
|
|
101
|
+
container: {
|
|
102
|
+
materialized: true,
|
|
103
|
+
mayWriteValues: true,
|
|
104
|
+
materializeAt: "setup",
|
|
105
|
+
},
|
|
82
106
|
};
|
|
83
107
|
|
|
84
108
|
/** Config defaults when `.lisa.config.json` carries no `secrets` block. */
|
|
@@ -185,17 +209,49 @@ export function readConfig(cwd = process.cwd(), env = process.env) {
|
|
|
185
209
|
throw new Error(`.lisa.config.json is not readable: ${err.message}`);
|
|
186
210
|
}
|
|
187
211
|
if (!cfg) return withSurface(DEFAULTS);
|
|
212
|
+
const provider = cfg.provider ?? DEFAULTS.provider;
|
|
213
|
+
const namespace = assertNamespace(cfg.namespace ?? DEFAULTS.namespace);
|
|
188
214
|
return withSurface({
|
|
189
|
-
provider
|
|
190
|
-
bootstrap:
|
|
215
|
+
provider,
|
|
216
|
+
bootstrap: resolveBootstrap(cfg.bootstrap, provider, namespace),
|
|
191
217
|
require: cfg.require ?? null,
|
|
192
218
|
rotating: cfg.rotating ?? [],
|
|
193
|
-
namespace
|
|
219
|
+
namespace,
|
|
194
220
|
narrow: { ...DEFAULTS.narrow, ...(cfg.narrow ?? {}) },
|
|
195
221
|
surface: cfg.surface ?? null,
|
|
196
222
|
});
|
|
197
223
|
}
|
|
198
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Fill in the bootstrap block a project did not spell out.
|
|
227
|
+
*
|
|
228
|
+
* Both defaults exist because a token stored on the machine was going unread.
|
|
229
|
+
*
|
|
230
|
+
* **The key** follows the same convention the repo-less path uses,
|
|
231
|
+
* `<PROVIDER_VAR>_<namespace>`, so a machine provisioned once serves every
|
|
232
|
+
* project of that tenant without each restating the name. A project that sets
|
|
233
|
+
* one keeps it: a name that is not derivable is a legitimate choice, and
|
|
234
|
+
* overriding it would point sessions at a variable nobody set.
|
|
235
|
+
*
|
|
236
|
+
* **The sources** gain `keychain` because `["env"]` alone meant a token in the
|
|
237
|
+
* OS keychain was never consulted — so the machine setup appeared to work and
|
|
238
|
+
* every project still failed until someone hand-edited its config. Env stays
|
|
239
|
+
* first, so a CI runner injecting the bootstrap never reaches for a local store.
|
|
240
|
+
* On a platform with no keychain the extra source reads as empty and costs
|
|
241
|
+
* nothing.
|
|
242
|
+
* @param {object|undefined} bootstrap The project's own block, if any.
|
|
243
|
+
* @param {string} provider Resolved provider name.
|
|
244
|
+
* @param {string} namespace Resolved namespace.
|
|
245
|
+
* @returns {{sources: string[], key: string|null}} The resolved block.
|
|
246
|
+
*/
|
|
247
|
+
function resolveBootstrap(bootstrap, provider, namespace) {
|
|
248
|
+
const given = bootstrap ?? {};
|
|
249
|
+
return {
|
|
250
|
+
sources: given.sources ?? ["env", "keychain"],
|
|
251
|
+
key: given.key ?? bootstrapKeyFor(provider, namespace),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
199
255
|
/**
|
|
200
256
|
* Build a configuration from the environment, for sessions with no checkout.
|
|
201
257
|
*
|
|
@@ -237,10 +293,18 @@ function fromEnvironment(env) {
|
|
|
237
293
|
namespace: assertNamespace(namespace),
|
|
238
294
|
bootstrap: {
|
|
239
295
|
...DEFAULTS.bootstrap,
|
|
296
|
+
// Derived from the provider resolved just above, not from Bitwarden's
|
|
297
|
+
// name. Hardcoding it told a Doppler tenant to set BWS_ACCESS_TOKEN_<ns>,
|
|
298
|
+
// which its CLI has never heard of — on the one surface that has no
|
|
299
|
+
// config file to override the guess.
|
|
300
|
+
// Same two defaults as a project config gets, for the same reasons —
|
|
301
|
+
// see `resolveBootstrap`. Stated here too because this path never reads
|
|
302
|
+
// a file, so it cannot inherit them.
|
|
303
|
+
sources: ["env", "keychain"],
|
|
240
304
|
key:
|
|
241
305
|
env.LISA_BOOTSTRAP_KEY ??
|
|
242
306
|
env.LISA_SECRETS_BOOTSTRAP_KEY ??
|
|
243
|
-
|
|
307
|
+
bootstrapKeyFor(provider, namespace),
|
|
244
308
|
},
|
|
245
309
|
};
|
|
246
310
|
}
|
|
@@ -885,6 +885,52 @@ export function pinEnvironment(environmentId, cwd = process.cwd()) {
|
|
|
885
885
|
return path;
|
|
886
886
|
}
|
|
887
887
|
|
|
888
|
+
/**
|
|
889
|
+
* The lines an operator puts above the field, for a session with no checkout.
|
|
890
|
+
*
|
|
891
|
+
* Emitted as shell that runs, not as a template with holes: every hole is a
|
|
892
|
+
* hand substitution in a settings box with no review, which is the failure this
|
|
893
|
+
* whole file exists to prevent. So a provider that cannot be bootstrapped by an
|
|
894
|
+
* environment variable gets a sentence explaining that, not an `export` of a
|
|
895
|
+
* placeholder — a line that would be a syntax error if pasted.
|
|
896
|
+
*
|
|
897
|
+
* `LISA_PROVIDER` is exported even though the field defaults it, because the
|
|
898
|
+
* field's default is Bitwarden. A Doppler tenant that omits it gets `bws`
|
|
899
|
+
* installed and its own CLI missing, having configured everything correctly.
|
|
900
|
+
* @param {object} options Resolved identity and display strings.
|
|
901
|
+
* @returns {string[]} Lines to include in the emitted guidance.
|
|
902
|
+
*/
|
|
903
|
+
function repoLessExports({ namespace, key, provider, bootstrapKey, tenant }) {
|
|
904
|
+
// Two different reasons the key can be missing, and telling them apart is the
|
|
905
|
+
// whole value of the message. "Nothing was named" is the operator's to fix by
|
|
906
|
+
// re-running; "this provider has no such variable" is not fixable at all and
|
|
907
|
+
// must not read as an omission.
|
|
908
|
+
if (!tenant) {
|
|
909
|
+
return [
|
|
910
|
+
" # Re-run with --tenant=<name> and these are filled in for you.",
|
|
911
|
+
` export LISA_TENANT=${namespace}`,
|
|
912
|
+
" export <bootstrap variable>='<read from your credential manager>'",
|
|
913
|
+
" export LISA_SECRETS_SURFACE=claude-web",
|
|
914
|
+
];
|
|
915
|
+
}
|
|
916
|
+
if (!bootstrapKey) {
|
|
917
|
+
return [
|
|
918
|
+
` # ${provider} has no environment-variable bootstrap, so there is`,
|
|
919
|
+
" # nothing to export for it. Authenticate it the way its own CLI",
|
|
920
|
+
" # expects, then set the three below.",
|
|
921
|
+
` export LISA_TENANT=${namespace}`,
|
|
922
|
+
` export LISA_PROVIDER=${provider}`,
|
|
923
|
+
" export LISA_SECRETS_SURFACE=claude-web",
|
|
924
|
+
];
|
|
925
|
+
}
|
|
926
|
+
return [
|
|
927
|
+
` export LISA_TENANT=${namespace}`,
|
|
928
|
+
` export ${key}='<the same value as above>'`,
|
|
929
|
+
...(provider ? [` export LISA_PROVIDER=${provider}`] : []),
|
|
930
|
+
" export LISA_SECRETS_SURFACE=claude-web",
|
|
931
|
+
];
|
|
932
|
+
}
|
|
933
|
+
|
|
888
934
|
/**
|
|
889
935
|
* Produce the configuration a human pastes to provision a Claude cloud surface.
|
|
890
936
|
*
|
|
@@ -893,11 +939,16 @@ export function pinEnvironment(environmentId, cwd = process.cwd()) {
|
|
|
893
939
|
* settings page, no direct URL, and no endpoint. Emit is not a degraded option
|
|
894
940
|
* here, it is the only one — so the read-back in `verify-remote-env.mjs` is
|
|
895
941
|
* what makes the result trustworthy, exactly as it would be at any other tier.
|
|
896
|
-
* @param {{bootstrapKey: string|null}} options Project details.
|
|
942
|
+
* @param {{bootstrapKey: string|null, tenant?: string|null, provider?: string}} options Project details.
|
|
897
943
|
* @returns {string} Text to show the operator.
|
|
898
944
|
*/
|
|
899
|
-
export function emitClaudeWeb({
|
|
900
|
-
|
|
945
|
+
export function emitClaudeWeb({
|
|
946
|
+
bootstrapKey,
|
|
947
|
+
tenant = null,
|
|
948
|
+
provider = null,
|
|
949
|
+
}) {
|
|
950
|
+
const key = bootstrapKey ?? "<not resolved>";
|
|
951
|
+
const namespace = tenant ?? "<your namespace>";
|
|
901
952
|
return [
|
|
902
953
|
"Provisioning tier: EMIT — and for this surface that is the only tier.",
|
|
903
954
|
" A Claude cloud environment is account-scoped configuration edited in the",
|
|
@@ -924,11 +975,9 @@ export function emitClaudeWeb({ bootstrapKey }) {
|
|
|
924
975
|
" while the image is being built, before the environment exists. So a",
|
|
925
976
|
" session with no checkout has to be told its tenant inside the script",
|
|
926
977
|
" itself, or the field finds nothing to prepare and exits 0 looking like",
|
|
927
|
-
" success. Put
|
|
978
|
+
" success. Put the lines below FIRST, then the field after them:",
|
|
928
979
|
"",
|
|
929
|
-
|
|
930
|
-
` export ${key}='<the same value as above>'`,
|
|
931
|
-
" export LISA_SECRETS_SURFACE=claude-web",
|
|
980
|
+
...repoLessExports({ namespace, key, provider, bootstrapKey, tenant }),
|
|
932
981
|
"",
|
|
933
982
|
" LISA_SECRETS_SURFACE is set explicitly rather than left to detection:",
|
|
934
983
|
" detection keys off CLAUDE_CODE_REMOTE, and a surface that does not set",
|
|
@@ -1096,6 +1145,66 @@ function readBootstrapKey(cwd = process.cwd()) {
|
|
|
1096
1145
|
return JSON.parse(readFileSync(path, "utf8")).secrets?.bootstrap?.key ?? null;
|
|
1097
1146
|
}
|
|
1098
1147
|
|
|
1148
|
+
/**
|
|
1149
|
+
* Work out what to tell the operator to export, for a surface that has no repo.
|
|
1150
|
+
*
|
|
1151
|
+
* Emitting used to read the key out of `.lisa.config.json` in the working
|
|
1152
|
+
* directory, which made the command that configures a REPO-LESS environment
|
|
1153
|
+
* require a repository — so it was run from a checkout that happened to be
|
|
1154
|
+
* nearby, or it printed a placeholder the operator substituted by hand.
|
|
1155
|
+
*
|
|
1156
|
+
* Explicit flags win, then the environment, then a checkout if the caller
|
|
1157
|
+
* happens to be standing in one, then the convention. The provider matters
|
|
1158
|
+
* because it decides the variable's name: `bws` reads `BWS_ACCESS_TOKEN` and
|
|
1159
|
+
* `doppler` reads `DOPPLER_TOKEN`, so a tenant on one told to export the
|
|
1160
|
+
* other's name gets "Missing access token" from a CLI it did configure.
|
|
1161
|
+
* @param {string[]} argv Process arguments.
|
|
1162
|
+
* @param {Record<string, string|undefined>} [env] Environment to read.
|
|
1163
|
+
* @param {string} [cwd] Directory to look for a config in.
|
|
1164
|
+
* @returns {Promise<{tenant: string|null, provider: string, bootstrapKey: string|null}>} Resolved identity.
|
|
1165
|
+
*/
|
|
1166
|
+
export async function resolveEmitTarget(
|
|
1167
|
+
argv,
|
|
1168
|
+
env = process.env,
|
|
1169
|
+
cwd = process.cwd()
|
|
1170
|
+
) {
|
|
1171
|
+
const flag = name =>
|
|
1172
|
+
argv.find(a => a.startsWith(`--${name}=`))?.slice(name.length + 3) ?? null;
|
|
1173
|
+
|
|
1174
|
+
const tenant =
|
|
1175
|
+
flag("tenant") ||
|
|
1176
|
+
(env.LISA_TENANT ?? env.LISA_SECRETS_NAMESPACE ?? "").trim() ||
|
|
1177
|
+
null;
|
|
1178
|
+
const provider =
|
|
1179
|
+
flag("provider") ||
|
|
1180
|
+
(env.LISA_PROVIDER ?? env.LISA_SECRETS_PROVIDER ?? "").trim() ||
|
|
1181
|
+
"bitwarden";
|
|
1182
|
+
|
|
1183
|
+
// Validated before it can reach the output, by the same rule the runtime path
|
|
1184
|
+
// applies. This one becomes `export LISA_TENANT=<value>` in a block an
|
|
1185
|
+
// operator pastes verbatim, so `my tenant` or `has$var` would emit shell that
|
|
1186
|
+
// breaks — or worse, expands — where nothing reviews it. Refusing here costs
|
|
1187
|
+
// a re-run; emitting it costs a container nobody can explain.
|
|
1188
|
+
if (tenant) {
|
|
1189
|
+
const { assertNamespace } = await import(
|
|
1190
|
+
pathToFileURL(siblingScript("lisa-secrets-access", "surfaces.mjs")).href
|
|
1191
|
+
);
|
|
1192
|
+
assertNamespace(tenant);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// A configured key outranks the convention: a project may legitimately use a
|
|
1196
|
+
// name that is not derivable, and this command must not tell its operator to
|
|
1197
|
+
// set a different one from the one their sessions actually read.
|
|
1198
|
+
const configured = readBootstrapKey(cwd);
|
|
1199
|
+
if (configured) return { tenant, provider, bootstrapKey: configured };
|
|
1200
|
+
if (!tenant) return { tenant, provider, bootstrapKey: null };
|
|
1201
|
+
|
|
1202
|
+
const { bootstrapKeyFor } = await import(
|
|
1203
|
+
pathToFileURL(siblingScript("lisa-secrets-access", "providers.mjs")).href
|
|
1204
|
+
);
|
|
1205
|
+
return { tenant, provider, bootstrapKey: bootstrapKeyFor(provider, tenant) };
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1099
1208
|
async function main() {
|
|
1100
1209
|
// `--print-tools` answers "which CLIs does this secret set imply", reading
|
|
1101
1210
|
// the notes materialized alongside the values.
|
|
@@ -1170,7 +1279,7 @@ async function main() {
|
|
|
1170
1279
|
`Emitting is implemented for claude-web, which has no other tier.`
|
|
1171
1280
|
);
|
|
1172
1281
|
}
|
|
1173
|
-
console.log(emitClaudeWeb(
|
|
1282
|
+
console.log(emitClaudeWeb(await resolveEmitTarget(process.argv)));
|
|
1174
1283
|
return;
|
|
1175
1284
|
}
|
|
1176
1285
|
|
|
@@ -51,6 +51,29 @@ const PROVIDER_BOOTSTRAP_ENV = {
|
|
|
51
51
|
doppler: "DOPPLER_TOKEN",
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The tenant-scoped bootstrap variable a provider expects, by convention.
|
|
56
|
+
*
|
|
57
|
+
* Exists so the repo-less path cannot invent one. It used to compose
|
|
58
|
+
* `BWS_ACCESS_TOKEN_<namespace>` literally, ignoring the provider it had just
|
|
59
|
+
* resolved — so a Doppler tenant with no checkout was told to set a Bitwarden
|
|
60
|
+
* variable, and its CLI failed with "Missing access token". That is precisely
|
|
61
|
+
* the confusion the two questions above are separated to prevent, reintroduced
|
|
62
|
+
* on the one surface with no config file to correct it.
|
|
63
|
+
*
|
|
64
|
+
* `null` for a provider with no env-var bootstrap — 1Password, Vault and AWS
|
|
65
|
+
* authenticate by other means — because a name invented for them would be a
|
|
66
|
+
* variable nothing reads, which is worse than admitting there is none.
|
|
67
|
+
* `providerEnv` already treats an unmapped provider as "inject nothing".
|
|
68
|
+
* @param {string} provider Provider name.
|
|
69
|
+
* @param {string} namespace Tenant namespace.
|
|
70
|
+
* @returns {string|null} The variable to set, or null when the provider has none.
|
|
71
|
+
*/
|
|
72
|
+
export function bootstrapKeyFor(provider, namespace) {
|
|
73
|
+
const canonical = PROVIDER_BOOTSTRAP_ENV[provider];
|
|
74
|
+
return canonical ? `${canonical}_${namespace}` : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
54
77
|
/**
|
|
55
78
|
* Build the child environment for a provider CLI, injecting the bootstrap under
|
|
56
79
|
* the name that CLI actually reads.
|
|
@@ -17,6 +17,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
|
|
20
|
+
import { bootstrapKeyFor } from "./providers.mjs";
|
|
21
|
+
|
|
20
22
|
/**
|
|
21
23
|
* Capabilities, per surface.
|
|
22
24
|
*
|
|
@@ -79,6 +81,28 @@ export const SURFACES = {
|
|
|
79
81
|
// recoverable; an absent one means the environment does not work.
|
|
80
82
|
materializeAt: "both",
|
|
81
83
|
},
|
|
84
|
+
// A container an operator builds and runs themselves — locally, or anywhere
|
|
85
|
+
// Lisa is not the one provisioning the host.
|
|
86
|
+
//
|
|
87
|
+
// It exists because `local` was standing in for it, and `local` is
|
|
88
|
+
// `materialized: false`. That default is right for a human at a keyboard: the
|
|
89
|
+
// provider CLI is authenticated, secrets resolve live, and writing them to
|
|
90
|
+
// disk would be a second copy to keep honest. A container has no keychain and
|
|
91
|
+
// dies with its filesystem, so the same default leaves it with the CLIs
|
|
92
|
+
// installed and nothing to authenticate them.
|
|
93
|
+
//
|
|
94
|
+
// The workaround was to claim `claude-web`, which materializes correctly and
|
|
95
|
+
// is a lie — it made a local container indistinguishable from a cloud session
|
|
96
|
+
// in every diagnostic and log.
|
|
97
|
+
//
|
|
98
|
+
// `setup` rather than `both`: a container has no session-start hook to run,
|
|
99
|
+
// and it is started fresh rather than resumed from a vendor's cache, so the
|
|
100
|
+
// start of the container IS the refresh.
|
|
101
|
+
container: {
|
|
102
|
+
materialized: true,
|
|
103
|
+
mayWriteValues: true,
|
|
104
|
+
materializeAt: "setup",
|
|
105
|
+
},
|
|
82
106
|
};
|
|
83
107
|
|
|
84
108
|
/** Config defaults when `.lisa.config.json` carries no `secrets` block. */
|
|
@@ -185,17 +209,49 @@ export function readConfig(cwd = process.cwd(), env = process.env) {
|
|
|
185
209
|
throw new Error(`.lisa.config.json is not readable: ${err.message}`);
|
|
186
210
|
}
|
|
187
211
|
if (!cfg) return withSurface(DEFAULTS);
|
|
212
|
+
const provider = cfg.provider ?? DEFAULTS.provider;
|
|
213
|
+
const namespace = assertNamespace(cfg.namespace ?? DEFAULTS.namespace);
|
|
188
214
|
return withSurface({
|
|
189
|
-
provider
|
|
190
|
-
bootstrap:
|
|
215
|
+
provider,
|
|
216
|
+
bootstrap: resolveBootstrap(cfg.bootstrap, provider, namespace),
|
|
191
217
|
require: cfg.require ?? null,
|
|
192
218
|
rotating: cfg.rotating ?? [],
|
|
193
|
-
namespace
|
|
219
|
+
namespace,
|
|
194
220
|
narrow: { ...DEFAULTS.narrow, ...(cfg.narrow ?? {}) },
|
|
195
221
|
surface: cfg.surface ?? null,
|
|
196
222
|
});
|
|
197
223
|
}
|
|
198
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Fill in the bootstrap block a project did not spell out.
|
|
227
|
+
*
|
|
228
|
+
* Both defaults exist because a token stored on the machine was going unread.
|
|
229
|
+
*
|
|
230
|
+
* **The key** follows the same convention the repo-less path uses,
|
|
231
|
+
* `<PROVIDER_VAR>_<namespace>`, so a machine provisioned once serves every
|
|
232
|
+
* project of that tenant without each restating the name. A project that sets
|
|
233
|
+
* one keeps it: a name that is not derivable is a legitimate choice, and
|
|
234
|
+
* overriding it would point sessions at a variable nobody set.
|
|
235
|
+
*
|
|
236
|
+
* **The sources** gain `keychain` because `["env"]` alone meant a token in the
|
|
237
|
+
* OS keychain was never consulted — so the machine setup appeared to work and
|
|
238
|
+
* every project still failed until someone hand-edited its config. Env stays
|
|
239
|
+
* first, so a CI runner injecting the bootstrap never reaches for a local store.
|
|
240
|
+
* On a platform with no keychain the extra source reads as empty and costs
|
|
241
|
+
* nothing.
|
|
242
|
+
* @param {object|undefined} bootstrap The project's own block, if any.
|
|
243
|
+
* @param {string} provider Resolved provider name.
|
|
244
|
+
* @param {string} namespace Resolved namespace.
|
|
245
|
+
* @returns {{sources: string[], key: string|null}} The resolved block.
|
|
246
|
+
*/
|
|
247
|
+
function resolveBootstrap(bootstrap, provider, namespace) {
|
|
248
|
+
const given = bootstrap ?? {};
|
|
249
|
+
return {
|
|
250
|
+
sources: given.sources ?? ["env", "keychain"],
|
|
251
|
+
key: given.key ?? bootstrapKeyFor(provider, namespace),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
199
255
|
/**
|
|
200
256
|
* Build a configuration from the environment, for sessions with no checkout.
|
|
201
257
|
*
|
|
@@ -237,10 +293,18 @@ function fromEnvironment(env) {
|
|
|
237
293
|
namespace: assertNamespace(namespace),
|
|
238
294
|
bootstrap: {
|
|
239
295
|
...DEFAULTS.bootstrap,
|
|
296
|
+
// Derived from the provider resolved just above, not from Bitwarden's
|
|
297
|
+
// name. Hardcoding it told a Doppler tenant to set BWS_ACCESS_TOKEN_<ns>,
|
|
298
|
+
// which its CLI has never heard of — on the one surface that has no
|
|
299
|
+
// config file to override the guess.
|
|
300
|
+
// Same two defaults as a project config gets, for the same reasons —
|
|
301
|
+
// see `resolveBootstrap`. Stated here too because this path never reads
|
|
302
|
+
// a file, so it cannot inherit them.
|
|
303
|
+
sources: ["env", "keychain"],
|
|
240
304
|
key:
|
|
241
305
|
env.LISA_BOOTSTRAP_KEY ??
|
|
242
306
|
env.LISA_SECRETS_BOOTSTRAP_KEY ??
|
|
243
|
-
|
|
307
|
+
bootstrapKeyFor(provider, namespace),
|
|
244
308
|
},
|
|
245
309
|
};
|
|
246
310
|
}
|
|
@@ -885,6 +885,52 @@ export function pinEnvironment(environmentId, cwd = process.cwd()) {
|
|
|
885
885
|
return path;
|
|
886
886
|
}
|
|
887
887
|
|
|
888
|
+
/**
|
|
889
|
+
* The lines an operator puts above the field, for a session with no checkout.
|
|
890
|
+
*
|
|
891
|
+
* Emitted as shell that runs, not as a template with holes: every hole is a
|
|
892
|
+
* hand substitution in a settings box with no review, which is the failure this
|
|
893
|
+
* whole file exists to prevent. So a provider that cannot be bootstrapped by an
|
|
894
|
+
* environment variable gets a sentence explaining that, not an `export` of a
|
|
895
|
+
* placeholder — a line that would be a syntax error if pasted.
|
|
896
|
+
*
|
|
897
|
+
* `LISA_PROVIDER` is exported even though the field defaults it, because the
|
|
898
|
+
* field's default is Bitwarden. A Doppler tenant that omits it gets `bws`
|
|
899
|
+
* installed and its own CLI missing, having configured everything correctly.
|
|
900
|
+
* @param {object} options Resolved identity and display strings.
|
|
901
|
+
* @returns {string[]} Lines to include in the emitted guidance.
|
|
902
|
+
*/
|
|
903
|
+
function repoLessExports({ namespace, key, provider, bootstrapKey, tenant }) {
|
|
904
|
+
// Two different reasons the key can be missing, and telling them apart is the
|
|
905
|
+
// whole value of the message. "Nothing was named" is the operator's to fix by
|
|
906
|
+
// re-running; "this provider has no such variable" is not fixable at all and
|
|
907
|
+
// must not read as an omission.
|
|
908
|
+
if (!tenant) {
|
|
909
|
+
return [
|
|
910
|
+
" # Re-run with --tenant=<name> and these are filled in for you.",
|
|
911
|
+
` export LISA_TENANT=${namespace}`,
|
|
912
|
+
" export <bootstrap variable>='<read from your credential manager>'",
|
|
913
|
+
" export LISA_SECRETS_SURFACE=claude-web",
|
|
914
|
+
];
|
|
915
|
+
}
|
|
916
|
+
if (!bootstrapKey) {
|
|
917
|
+
return [
|
|
918
|
+
` # ${provider} has no environment-variable bootstrap, so there is`,
|
|
919
|
+
" # nothing to export for it. Authenticate it the way its own CLI",
|
|
920
|
+
" # expects, then set the three below.",
|
|
921
|
+
` export LISA_TENANT=${namespace}`,
|
|
922
|
+
` export LISA_PROVIDER=${provider}`,
|
|
923
|
+
" export LISA_SECRETS_SURFACE=claude-web",
|
|
924
|
+
];
|
|
925
|
+
}
|
|
926
|
+
return [
|
|
927
|
+
` export LISA_TENANT=${namespace}`,
|
|
928
|
+
` export ${key}='<the same value as above>'`,
|
|
929
|
+
...(provider ? [` export LISA_PROVIDER=${provider}`] : []),
|
|
930
|
+
" export LISA_SECRETS_SURFACE=claude-web",
|
|
931
|
+
];
|
|
932
|
+
}
|
|
933
|
+
|
|
888
934
|
/**
|
|
889
935
|
* Produce the configuration a human pastes to provision a Claude cloud surface.
|
|
890
936
|
*
|
|
@@ -893,11 +939,16 @@ export function pinEnvironment(environmentId, cwd = process.cwd()) {
|
|
|
893
939
|
* settings page, no direct URL, and no endpoint. Emit is not a degraded option
|
|
894
940
|
* here, it is the only one — so the read-back in `verify-remote-env.mjs` is
|
|
895
941
|
* what makes the result trustworthy, exactly as it would be at any other tier.
|
|
896
|
-
* @param {{bootstrapKey: string|null}} options Project details.
|
|
942
|
+
* @param {{bootstrapKey: string|null, tenant?: string|null, provider?: string}} options Project details.
|
|
897
943
|
* @returns {string} Text to show the operator.
|
|
898
944
|
*/
|
|
899
|
-
export function emitClaudeWeb({
|
|
900
|
-
|
|
945
|
+
export function emitClaudeWeb({
|
|
946
|
+
bootstrapKey,
|
|
947
|
+
tenant = null,
|
|
948
|
+
provider = null,
|
|
949
|
+
}) {
|
|
950
|
+
const key = bootstrapKey ?? "<not resolved>";
|
|
951
|
+
const namespace = tenant ?? "<your namespace>";
|
|
901
952
|
return [
|
|
902
953
|
"Provisioning tier: EMIT — and for this surface that is the only tier.",
|
|
903
954
|
" A Claude cloud environment is account-scoped configuration edited in the",
|
|
@@ -924,11 +975,9 @@ export function emitClaudeWeb({ bootstrapKey }) {
|
|
|
924
975
|
" while the image is being built, before the environment exists. So a",
|
|
925
976
|
" session with no checkout has to be told its tenant inside the script",
|
|
926
977
|
" itself, or the field finds nothing to prepare and exits 0 looking like",
|
|
927
|
-
" success. Put
|
|
978
|
+
" success. Put the lines below FIRST, then the field after them:",
|
|
928
979
|
"",
|
|
929
|
-
|
|
930
|
-
` export ${key}='<the same value as above>'`,
|
|
931
|
-
" export LISA_SECRETS_SURFACE=claude-web",
|
|
980
|
+
...repoLessExports({ namespace, key, provider, bootstrapKey, tenant }),
|
|
932
981
|
"",
|
|
933
982
|
" LISA_SECRETS_SURFACE is set explicitly rather than left to detection:",
|
|
934
983
|
" detection keys off CLAUDE_CODE_REMOTE, and a surface that does not set",
|
|
@@ -1096,6 +1145,66 @@ function readBootstrapKey(cwd = process.cwd()) {
|
|
|
1096
1145
|
return JSON.parse(readFileSync(path, "utf8")).secrets?.bootstrap?.key ?? null;
|
|
1097
1146
|
}
|
|
1098
1147
|
|
|
1148
|
+
/**
|
|
1149
|
+
* Work out what to tell the operator to export, for a surface that has no repo.
|
|
1150
|
+
*
|
|
1151
|
+
* Emitting used to read the key out of `.lisa.config.json` in the working
|
|
1152
|
+
* directory, which made the command that configures a REPO-LESS environment
|
|
1153
|
+
* require a repository — so it was run from a checkout that happened to be
|
|
1154
|
+
* nearby, or it printed a placeholder the operator substituted by hand.
|
|
1155
|
+
*
|
|
1156
|
+
* Explicit flags win, then the environment, then a checkout if the caller
|
|
1157
|
+
* happens to be standing in one, then the convention. The provider matters
|
|
1158
|
+
* because it decides the variable's name: `bws` reads `BWS_ACCESS_TOKEN` and
|
|
1159
|
+
* `doppler` reads `DOPPLER_TOKEN`, so a tenant on one told to export the
|
|
1160
|
+
* other's name gets "Missing access token" from a CLI it did configure.
|
|
1161
|
+
* @param {string[]} argv Process arguments.
|
|
1162
|
+
* @param {Record<string, string|undefined>} [env] Environment to read.
|
|
1163
|
+
* @param {string} [cwd] Directory to look for a config in.
|
|
1164
|
+
* @returns {Promise<{tenant: string|null, provider: string, bootstrapKey: string|null}>} Resolved identity.
|
|
1165
|
+
*/
|
|
1166
|
+
export async function resolveEmitTarget(
|
|
1167
|
+
argv,
|
|
1168
|
+
env = process.env,
|
|
1169
|
+
cwd = process.cwd()
|
|
1170
|
+
) {
|
|
1171
|
+
const flag = name =>
|
|
1172
|
+
argv.find(a => a.startsWith(`--${name}=`))?.slice(name.length + 3) ?? null;
|
|
1173
|
+
|
|
1174
|
+
const tenant =
|
|
1175
|
+
flag("tenant") ||
|
|
1176
|
+
(env.LISA_TENANT ?? env.LISA_SECRETS_NAMESPACE ?? "").trim() ||
|
|
1177
|
+
null;
|
|
1178
|
+
const provider =
|
|
1179
|
+
flag("provider") ||
|
|
1180
|
+
(env.LISA_PROVIDER ?? env.LISA_SECRETS_PROVIDER ?? "").trim() ||
|
|
1181
|
+
"bitwarden";
|
|
1182
|
+
|
|
1183
|
+
// Validated before it can reach the output, by the same rule the runtime path
|
|
1184
|
+
// applies. This one becomes `export LISA_TENANT=<value>` in a block an
|
|
1185
|
+
// operator pastes verbatim, so `my tenant` or `has$var` would emit shell that
|
|
1186
|
+
// breaks — or worse, expands — where nothing reviews it. Refusing here costs
|
|
1187
|
+
// a re-run; emitting it costs a container nobody can explain.
|
|
1188
|
+
if (tenant) {
|
|
1189
|
+
const { assertNamespace } = await import(
|
|
1190
|
+
pathToFileURL(siblingScript("lisa-secrets-access", "surfaces.mjs")).href
|
|
1191
|
+
);
|
|
1192
|
+
assertNamespace(tenant);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// A configured key outranks the convention: a project may legitimately use a
|
|
1196
|
+
// name that is not derivable, and this command must not tell its operator to
|
|
1197
|
+
// set a different one from the one their sessions actually read.
|
|
1198
|
+
const configured = readBootstrapKey(cwd);
|
|
1199
|
+
if (configured) return { tenant, provider, bootstrapKey: configured };
|
|
1200
|
+
if (!tenant) return { tenant, provider, bootstrapKey: null };
|
|
1201
|
+
|
|
1202
|
+
const { bootstrapKeyFor } = await import(
|
|
1203
|
+
pathToFileURL(siblingScript("lisa-secrets-access", "providers.mjs")).href
|
|
1204
|
+
);
|
|
1205
|
+
return { tenant, provider, bootstrapKey: bootstrapKeyFor(provider, tenant) };
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1099
1208
|
async function main() {
|
|
1100
1209
|
// `--print-tools` answers "which CLIs does this secret set imply", reading
|
|
1101
1210
|
// the notes materialized alongside the values.
|
|
@@ -1170,7 +1279,7 @@ async function main() {
|
|
|
1170
1279
|
`Emitting is implemented for claude-web, which has no other tier.`
|
|
1171
1280
|
);
|
|
1172
1281
|
}
|
|
1173
|
-
console.log(emitClaudeWeb(
|
|
1282
|
+
console.log(emitClaudeWeb(await resolveEmitTarget(process.argv)));
|
|
1174
1283
|
return;
|
|
1175
1284
|
}
|
|
1176
1285
|
|