@codyswann/lisa 2.318.0 → 2.320.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 +15 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +32 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +39 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/assets/session-start.sh +30 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +252 -20
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +56 -1
- package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
- package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +32 -3
- package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
- package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +39 -2
- package/plugins/lisa/skills/lisa-setup-remote-env/assets/session-start.sh +30 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +252 -20
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +56 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +32 -3
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +39 -2
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/assets/session-start.sh +30 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +252 -20
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +56 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +32 -3
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +39 -2
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/assets/session-start.sh +30 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +252 -20
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +56 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +32 -3
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +39 -2
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/assets/session-start.sh +30 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +252 -20
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +56 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
- package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +32 -3
- package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
- package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +39 -2
- package/plugins/src/base/skills/lisa-setup-remote-env/assets/session-start.sh +30 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +252 -20
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +56 -1
package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { execFileSync } from "node:child_process";
|
|
25
|
-
import { existsSync, statSync } from "node:fs";
|
|
25
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
26
27
|
|
|
27
28
|
import { readRemoteEnvConfig } from "./setup-remote-env.mjs";
|
|
28
29
|
import { extractVersion } from "./toolchain.mjs";
|
|
@@ -119,6 +120,43 @@ function verifyToolchain(tools) {
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Assert that a declared credential is genuinely readable, not a proxy stand-in.
|
|
125
|
+
*
|
|
126
|
+
* A surface may keep a credential outside the sandbox entirely and substitute
|
|
127
|
+
* the real value at egress. The variable is then present and non-empty, so a
|
|
128
|
+
* presence check passes — and a script that reads the variable and puts it in a
|
|
129
|
+
* header sends the placeholder and fails somewhere far from here, with an error
|
|
130
|
+
* that points at the service rather than at the environment.
|
|
131
|
+
*
|
|
132
|
+
* Reported without printing any value: the placeholder is compared, and a real
|
|
133
|
+
* credential is only ever reported as present.
|
|
134
|
+
*
|
|
135
|
+
* Takes its reporter as an argument rather than writing to this module's
|
|
136
|
+
* results array, so the rule can be exercised against a synthetic environment
|
|
137
|
+
* without a container and without leaking findings between runs.
|
|
138
|
+
* @param {string[]} required Declared credential names.
|
|
139
|
+
* @param {Record<string, string|undefined>} env Environment to inspect.
|
|
140
|
+
* @param {(ok: boolean, label: string, detail: string) => void} report Collector.
|
|
141
|
+
*/
|
|
142
|
+
export function verifyNotProxied(required, env, report) {
|
|
143
|
+
for (const name of required) {
|
|
144
|
+
const value = (env[name] ?? "").trim();
|
|
145
|
+
if (!value) {
|
|
146
|
+
report(false, `credential ${name}`, "declared but not present");
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
report(
|
|
150
|
+
value !== "proxy-injected",
|
|
151
|
+
`credential ${name}`,
|
|
152
|
+
value === "proxy-injected"
|
|
153
|
+
? 'reads as "proxy-injected" — substituted at egress, so anything ' +
|
|
154
|
+
"reading this variable directly receives the placeholder"
|
|
155
|
+
: "present"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
122
160
|
/**
|
|
123
161
|
* Assert the working tree is clean.
|
|
124
162
|
*
|
|
@@ -142,11 +180,28 @@ function verifyCleanCheckout() {
|
|
|
142
180
|
}
|
|
143
181
|
}
|
|
144
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Read the credential names the project declares it needs.
|
|
185
|
+
* @param {string} [cwd] Repository root.
|
|
186
|
+
* @returns {string[]} Declared names, or none when unconfigured.
|
|
187
|
+
*/
|
|
188
|
+
function readRequired(cwd = process.cwd()) {
|
|
189
|
+
const path = join(cwd, ".lisa.config.json");
|
|
190
|
+
if (!existsSync(path)) return [];
|
|
191
|
+
const required = JSON.parse(readFileSync(path, "utf8")).secrets?.require;
|
|
192
|
+
if (required == null) return [];
|
|
193
|
+
if (!Array.isArray(required)) {
|
|
194
|
+
throw new Error("secrets.require must be an array when present");
|
|
195
|
+
}
|
|
196
|
+
return required;
|
|
197
|
+
}
|
|
198
|
+
|
|
145
199
|
function main() {
|
|
146
200
|
const cfg = readRemoteEnvConfig();
|
|
147
201
|
const secretsDir = process.argv[2];
|
|
148
202
|
|
|
149
203
|
verifyToolchain(cfg.tools);
|
|
204
|
+
verifyNotProxied(readRequired(), process.env, check);
|
|
150
205
|
|
|
151
206
|
const surface = node([
|
|
152
207
|
new URL(
|
|
@@ -47,6 +47,16 @@ function fingerprint(value) {
|
|
|
47
47
|
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The value a proxied credential reads as instead of a usable token.
|
|
52
|
+
*
|
|
53
|
+
* Some surfaces keep a credential outside the sandbox entirely and substitute
|
|
54
|
+
* the real value at egress. The variable is then present and non-empty, which
|
|
55
|
+
* is exactly what makes it dangerous: a presence check passes, and only a
|
|
56
|
+
* consumer that reads the variable *itself* discovers it holds a placeholder.
|
|
57
|
+
*/
|
|
58
|
+
const PROXY_PLACEHOLDER = "proxy-injected";
|
|
59
|
+
|
|
50
60
|
/**
|
|
51
61
|
* Assert every declared name actually resolves.
|
|
52
62
|
* @param {object} cfg Resolved configuration.
|
|
@@ -56,18 +66,36 @@ function fingerprint(value) {
|
|
|
56
66
|
*/
|
|
57
67
|
export function checkRequired(cfg, provider, file, report) {
|
|
58
68
|
for (const name of cfg.require ?? []) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (resolves) report("ok", name, "resolves");
|
|
64
|
-
else
|
|
69
|
+
const fromEnv = (process.env[name] ?? "").trim();
|
|
70
|
+
const resolves = fromEnv || file.get(name) || provider.get(name)?.value;
|
|
71
|
+
|
|
72
|
+
if (!resolves) {
|
|
65
73
|
report(
|
|
66
74
|
"error",
|
|
67
75
|
name,
|
|
68
76
|
"declared in secrets.require but resolves nowhere — a startup error, " +
|
|
69
77
|
"not a late surprise"
|
|
70
78
|
);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Reported separately from "resolves" because the two are not the same
|
|
83
|
+
// claim. A tool that authenticates through the proxy works; a script that
|
|
84
|
+
// reads this variable and puts it in an Authorization header sends the
|
|
85
|
+
// literal placeholder and gets a confusing auth failure far from here.
|
|
86
|
+
if (fromEnv === PROXY_PLACEHOLDER) {
|
|
87
|
+
report(
|
|
88
|
+
"warn",
|
|
89
|
+
name,
|
|
90
|
+
`reads as the literal string "${PROXY_PLACEHOLDER}" — the value is ` +
|
|
91
|
+
`substituted at egress and never enters this environment. Tools that ` +
|
|
92
|
+
`authenticate through the proxy work; anything reading this variable ` +
|
|
93
|
+
`directly gets the placeholder, not a credential`
|
|
94
|
+
);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
report("ok", name, "resolves");
|
|
71
99
|
}
|
|
72
100
|
}
|
|
73
101
|
|
|
@@ -28,11 +28,36 @@ import { join } from "node:path";
|
|
|
28
28
|
* drift and leak. It is *required* on surfaces whose bootstrap runs before the
|
|
29
29
|
* consuming process exists and which therefore have no other channel; a remote
|
|
30
30
|
* agent container prepares itself during setup, long before any task starts.
|
|
31
|
+
*
|
|
32
|
+
* `materializeAt` — *when* the write happens, for surfaces that do one. The two
|
|
33
|
+
* remote surfaces share the capability but not the timing, and a reader adding a
|
|
34
|
+
* third should not assume otherwise. `codex-cloud` re-runs its setup script when
|
|
35
|
+
* a container resumes, so materializing during setup picks up a rotated value.
|
|
36
|
+
* `claude-web` *skips* its setup script whenever a filesystem cache exists, so
|
|
37
|
+
* materializing there would strand a rotated value until the cache expired —
|
|
38
|
+
* it materializes from a session-start hook instead, which runs every session.
|
|
39
|
+
*
|
|
40
|
+
* Stated as a capability rather than inferred from the surface name, so the
|
|
41
|
+
* setup runner selects its phases from the table instead of branching on which
|
|
42
|
+
* vendor it happens to be talking to.
|
|
31
43
|
*/
|
|
32
44
|
export const SURFACES = {
|
|
33
|
-
local: { materialized: false, mayWriteValues: false },
|
|
34
|
-
"github-actions": {
|
|
35
|
-
|
|
45
|
+
local: { materialized: false, mayWriteValues: false, materializeAt: null },
|
|
46
|
+
"github-actions": {
|
|
47
|
+
materialized: false,
|
|
48
|
+
mayWriteValues: false,
|
|
49
|
+
materializeAt: null,
|
|
50
|
+
},
|
|
51
|
+
"codex-cloud": {
|
|
52
|
+
materialized: true,
|
|
53
|
+
mayWriteValues: true,
|
|
54
|
+
materializeAt: "setup",
|
|
55
|
+
},
|
|
56
|
+
"claude-web": {
|
|
57
|
+
materialized: true,
|
|
58
|
+
mayWriteValues: true,
|
|
59
|
+
materializeAt: "session-start",
|
|
60
|
+
},
|
|
36
61
|
};
|
|
37
62
|
|
|
38
63
|
/** Config defaults when `.lisa.config.json` carries no `secrets` block. */
|
|
@@ -68,6 +93,10 @@ export function detectSurface(configured = null, env = process.env) {
|
|
|
68
93
|
return explicit;
|
|
69
94
|
}
|
|
70
95
|
if ((env.GITHUB_ACTIONS ?? "") === "true") return "github-actions";
|
|
96
|
+
// Compared to the exact string rather than tested for presence: the variable
|
|
97
|
+
// is documented as carrying "true" in a cloud session and never being true
|
|
98
|
+
// locally, so a presence test would misread a shell that exports it empty.
|
|
99
|
+
if ((env.CLAUDE_CODE_REMOTE ?? "") === "true") return "claude-web";
|
|
71
100
|
if ((env.CODEX_SANDBOX ?? env.CODEX_HOME ?? "") !== "") return "codex-cloud";
|
|
72
101
|
return "local";
|
|
73
102
|
}
|
|
@@ -19,14 +19,59 @@
|
|
|
19
19
|
import { existsSync, readFileSync } from "node:fs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
|
|
22
|
+
import { SURFACES as SURFACE_CAPABILITIES } from "./surfaces.mjs";
|
|
23
|
+
|
|
22
24
|
/** Providers with a read implementation today. */
|
|
23
25
|
const IMPLEMENTED_PROVIDERS = new Set(["bitwarden", "doppler", "env"]);
|
|
24
26
|
|
|
25
27
|
/** Providers named in the dispatch table but not yet implemented. */
|
|
26
28
|
const DECLARED_PROVIDERS = new Set(["1password", "aws", "vault"]);
|
|
27
29
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Surfaces the resolver knows.
|
|
32
|
+
*
|
|
33
|
+
* Derived from the resolver's own table rather than restated here. The two
|
|
34
|
+
* lists previously drifted apart by construction: adding a surface meant
|
|
35
|
+
* remembering to edit a second file, and forgetting produced a config that
|
|
36
|
+
* resolved correctly at runtime while `doctor` called it unknown.
|
|
37
|
+
*/
|
|
38
|
+
const SURFACES = new Set(Object.keys(SURFACE_CAPABILITIES));
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What a provisioned surface must record before anything dispatches to it.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately not uniform, because these surfaces do not bind the same way. A
|
|
44
|
+
* Codex Cloud environment is bound to one repository, so naming the repository
|
|
45
|
+
* is part of proving the environment is the right one. A Claude cloud
|
|
46
|
+
* environment has no repository at all — it is account-scoped configuration
|
|
47
|
+
* (network policy, variables, setup script) and the repository arrives per
|
|
48
|
+
* session — so its durable handle is the routine that dispatch fires.
|
|
49
|
+
*
|
|
50
|
+
* Requiring `repository` of every surface, as this file used to, would demand
|
|
51
|
+
* a field that cannot be true of `claude-web` in any meaningful sense.
|
|
52
|
+
*
|
|
53
|
+
* `repository` stays the default so every existing surface keeps its current
|
|
54
|
+
* contract. This file checks structure only — whether a declaration *could* be
|
|
55
|
+
* correct — so it deliberately does not restate the fuller preconditions that
|
|
56
|
+
* `lisa-remote-dispatch` enforces at the moment it actually dispatches.
|
|
57
|
+
*/
|
|
58
|
+
const SURFACE_BINDINGS = {
|
|
59
|
+
"claude-web": ["routineId", "fireUrl"],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The fields a surface must record, with the default applied.
|
|
64
|
+
*
|
|
65
|
+
* Read through one helper rather than at each call site, because the two
|
|
66
|
+
* callers answer different questions — "could this declaration be correct" and
|
|
67
|
+
* "may an automation dispatch to it" — and a fallback that drifted between them
|
|
68
|
+
* would let those answers disagree about the very same config.
|
|
69
|
+
* @param {string} surface Surface name.
|
|
70
|
+
* @returns {string[]} Field names that must be present.
|
|
71
|
+
*/
|
|
72
|
+
function bindingsFor(surface) {
|
|
73
|
+
return SURFACE_BINDINGS[surface] ?? ["repository"];
|
|
74
|
+
}
|
|
30
75
|
|
|
31
76
|
/** Install methods the toolchain runner supports. */
|
|
32
77
|
const INSTALL_METHODS = new Set(["release-zip", "npm-global"]);
|
|
@@ -139,15 +184,30 @@ export function validateRemoteEnv(remoteEnv) {
|
|
|
139
184
|
for (const [surface, block] of Object.entries(remoteEnv.surfaces ?? {})) {
|
|
140
185
|
if (!SURFACES.has(surface)) {
|
|
141
186
|
problems.push(`remoteEnv.surfaces has unknown surface "${surface}"`);
|
|
187
|
+
continue;
|
|
142
188
|
}
|
|
143
|
-
|
|
144
|
-
|
|
189
|
+
for (const field of bindingsFor(surface)) {
|
|
190
|
+
if (!block[field]) {
|
|
191
|
+
problems.push(`remoteEnv.surfaces["${surface}"] has no ${field}`);
|
|
192
|
+
}
|
|
145
193
|
}
|
|
146
194
|
}
|
|
147
195
|
|
|
148
196
|
return problems;
|
|
149
197
|
}
|
|
150
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Report whether a surface has been provisioned far enough to dispatch to.
|
|
201
|
+
* @param {object|undefined} remoteEnv The remote-environment block.
|
|
202
|
+
* @param {string} surface Surface name.
|
|
203
|
+
* @returns {boolean} Whether every binding field is recorded.
|
|
204
|
+
*/
|
|
205
|
+
export function isProvisioned(remoteEnv, surface) {
|
|
206
|
+
const block = remoteEnv?.surfaces?.[surface];
|
|
207
|
+
if (!block) return false;
|
|
208
|
+
return bindingsFor(surface).every(field => Boolean(block[field]));
|
|
209
|
+
}
|
|
210
|
+
|
|
151
211
|
/**
|
|
152
212
|
* Validate the `automations` block against declared surfaces.
|
|
153
213
|
* @param {object|undefined} automations The block, if present.
|
|
@@ -173,7 +233,7 @@ export function validateAutomations(automations, remoteEnv) {
|
|
|
173
233
|
);
|
|
174
234
|
continue;
|
|
175
235
|
}
|
|
176
|
-
if (!remoteEnv
|
|
236
|
+
if (!isProvisioned(remoteEnv, loop.executionEnv)) {
|
|
177
237
|
problems.push(
|
|
178
238
|
`automations["${name}"] dispatches to "${loop.executionEnv}", which is ` +
|
|
179
239
|
`not provisioned. Run /lisa:setup:remote-env ${loop.executionEnv} first.`
|
|
@@ -39,6 +39,20 @@ Omitting the install does not degrade gracefully. The entrypoint exits before th
|
|
|
39
39
|
|
|
40
40
|
Order matters. The toolchain comes first because the secrets step needs the provider CLI it installs. The hook comes last because it is the only part that may assume everything else is ready.
|
|
41
41
|
|
|
42
|
+
### Which phases run depends on the surface
|
|
43
|
+
|
|
44
|
+
Not every surface runs all three here, and the reason is worth understanding before changing it.
|
|
45
|
+
|
|
46
|
+
A surface that **re-runs this script when a container resumes** — Codex Cloud — should materialize during setup. Re-running is exactly what picks up a rotated value, an edited note, or a changed version pin.
|
|
47
|
+
|
|
48
|
+
A surface that **skips this script whenever a filesystem cache exists** — Claude Code web — must not. Materializing here would write the values once and then never refresh them, so a credential rotated on Tuesday would still be serving Monday's value until the cache expired days later. Those surfaces materialize from a session-start hook instead, which runs every session including a resumed one:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
bash scripts/lisa-remote-env/session-start.sh # guard; delegates to --phase=secrets
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The selection comes from the surface's `materializeAt` capability in `lisa-secrets-access`, not from its name, so adding a surface does not mean editing a branch. The hook is committed to the repository, so it also fires on a developer's machine — it exits `0` immediately there rather than failing, because a correct local session must not look broken.
|
|
55
|
+
|
|
42
56
|
## Toolchain manifest — two entry kinds
|
|
43
57
|
|
|
44
58
|
```json
|
|
@@ -131,6 +145,25 @@ Environment vars: LISA_SECRETS_SURFACE=codex-cloud
|
|
|
131
145
|
maintenance both run before task secrets exist>
|
|
132
146
|
```
|
|
133
147
|
|
|
148
|
+
### Claude Code web is emit-only, and that is not a fallback
|
|
149
|
+
|
|
150
|
+
**For Claude Code web there is no tier 1 or tier 2 to fall back from.** A cloud environment is account-scoped configuration — network access level, environment variables, setup script — edited only in the environment selector at claude.ai/code, which has no settings page, no direct URL, and no API. `/remote-env` selects an environment; it cannot create or edit one.
|
|
151
|
+
|
|
152
|
+
Note what this surface's environment is *not*: it carries no repository. The repository arrives per session, so an environment is reusable across every project, and there is nothing to bind. Its durable handle is the routine that dispatch fires, which is why `remoteEnv.surfaces["claude-web"]` records `routineId` and `fireUrl` rather than a repository.
|
|
153
|
+
|
|
154
|
+
Generate the exact text to paste:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
node scripts/setup-remote-env.mjs --emit=claude-web
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
It reads the project's own install command from its lockfile and the bootstrap name from `secrets.bootstrap.key`, then emits the environment fields, the `.claude/settings.json` hook block, and the two base-image surprises worth knowing before they cost an afternoon:
|
|
161
|
+
|
|
162
|
+
- **`gh` is not pre-installed.** If the project's flows shell out to it, add it to `remoteEnv.tools.install`, pinned and checksummed like anything else.
|
|
163
|
+
- **A proxied credential reads as the literal string `proxy-injected`.** Tools that authenticate through the GitHub proxy work; a script that reads the variable directly gets the placeholder. The read-back asserts this rather than leaving it to be discovered against a live service.
|
|
164
|
+
|
|
165
|
+
**Only the bootstrap belongs in the environment-variable box.** Values there are stored as plain text and are readable by anyone who uses the environment — on an organization-shared environment, that is every member of the organization. Everything else is materialized by the session-start hook. There is no dedicated secrets store on this surface, and personal versus shared environments cannot be told apart programmatically, so this one is a rule the operator upholds rather than something the tooling can enforce.
|
|
166
|
+
|
|
134
167
|
## Verification is tier-independent
|
|
135
168
|
|
|
136
169
|
**Whatever tier provisioned it, the same read-back proves it.** Trust comes from the verify, not the mechanism — which is what makes emit-tier as trustworthy as API-tier.
|
|
@@ -139,7 +172,9 @@ Environment vars: LISA_SECRETS_SURFACE=codex-cloud
|
|
|
139
172
|
scripts/verify-remote-env.mjs [SECRETS_DIR]
|
|
140
173
|
```
|
|
141
174
|
|
|
142
|
-
Asserts, without printing a value: each declared tool at its pinned or minimum version, the detected surface, the secrets directory at mode `0700`, both files at mode `0600`,
|
|
175
|
+
Asserts, without printing a value: each declared tool at its pinned or minimum version, the detected surface, the secrets directory at mode `0700`, both files at mode `0600`, a clean checkout, and that every name in `secrets.require` resolves to a real credential rather than a proxy placeholder.
|
|
176
|
+
|
|
177
|
+
That last one exists because presence is a weaker claim than usability. A credential the surface keeps outside the sandbox and substitutes at egress is present and non-empty, so a presence check passes — and a script that reads the variable and puts it in a header sends the placeholder and fails against the service, with an error pointing anywhere but at the environment.
|
|
143
178
|
|
|
144
179
|
**Never verify against vendor UI state.** On 2026-08-01 a Codex environments table reported zero tasks for an environment that had demonstrably completed one, because the task records carried a null environment identifier and the `--env` filter was correspondingly unreliable. Reconcile through durable identifiers only.
|
|
145
180
|
|
|
@@ -147,7 +182,7 @@ Asserts, without printing a value: each declared tool at its pinned or minimum v
|
|
|
147
182
|
|
|
148
183
|
Checked when setup runs, not at 3am:
|
|
149
184
|
|
|
150
|
-
- the environment exists and is bound to **this** repository as its default checkout;
|
|
185
|
+
- the environment exists, and on a surface that binds one, is bound to **this** repository as its default checkout — Claude cloud environments bind no repository at all, so there is nothing to check there;
|
|
151
186
|
- the bootstrap credential resolves;
|
|
152
187
|
- either a checkout-local Lisa skill is present, or the project dependency install has made the pinned `@codyswann/lisa` package available under `node_modules`.
|
|
153
188
|
|
|
@@ -157,6 +192,8 @@ Fail with a message naming what is missing. Never provision-and-hope.
|
|
|
157
192
|
|
|
158
193
|
On Codex Cloud the bootstrap must be an **environment variable, not a task secret**: setup runs on a new container and maintenance runs on cache resume, both before task secrets exist. The tradeoff is that the variable remains visible during the task, so compensate by keeping the machine account narrowly scoped and instructing the task never to inspect or use it.
|
|
159
194
|
|
|
195
|
+
On Claude Code web the same placement applies for the same reason, with the exposure widened rather than narrowed: there is no secrets store, values are stored as plain text, and anyone who uses the environment can read them. On an organization-shared environment that is every member. Keep the bootstrap in a **personal** environment, scope the machine account to the minimum, and treat every other credential as something the session-start hook materializes rather than something a human pastes.
|
|
196
|
+
|
|
160
197
|
## Related
|
|
161
198
|
|
|
162
199
|
- `lisa-secrets-access` — owns every part of the secrets contract this skill composes with.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Session-start entrypoint for surfaces that materialize secrets per session
|
|
4
|
+
# rather than during environment setup.
|
|
5
|
+
#
|
|
6
|
+
# Wired into the repository's `.claude/settings.json` as a SessionStart hook,
|
|
7
|
+
# so it is part of the clone and runs on every session — including a session
|
|
8
|
+
# resumed onto a cached environment, which is the case that matters.
|
|
9
|
+
#
|
|
10
|
+
# Why this exists at all: a cloud environment's setup script is skipped whenever
|
|
11
|
+
# a filesystem cache exists. Materializing there would write the values once and
|
|
12
|
+
# never refresh them, so a rotated credential would stay stale until the cache
|
|
13
|
+
# expired days later. This hook runs every session, so the copy on disk is
|
|
14
|
+
# always the provider's current view.
|
|
15
|
+
#
|
|
16
|
+
# It is deliberately a guard and a delegation, not a second implementation. The
|
|
17
|
+
# skill-resolution ladder is subtle enough that two copies would drift, so the
|
|
18
|
+
# real work stays in setup.sh and this file only decides whether to call it.
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
# Exit before doing anything on a machine that is not a remote session. The hook
|
|
22
|
+
# is committed to the repository, so it also fires on every local session; the
|
|
23
|
+
# materialize step would correctly refuse there, but failing on a developer's
|
|
24
|
+
# laptop every time they start a session is noise, not a signal.
|
|
25
|
+
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
here="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)"
|
|
30
|
+
exec bash "${here}/setup.sh" --phase=secrets "$@"
|