@codyswann/lisa 2.317.7 → 2.319.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/cdk/package-lisa/package.lisa.json +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +12 -7
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/package-lisa/package.lisa.json +5 -5
- package/nestjs/package-lisa/package.lisa.json +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 +12 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
- package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +34 -6
- package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +12 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
- 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 +12 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
- 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 +12 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
- 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 +12 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
- 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 +12 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +65 -5
- package/scripts/check-security-floors.mjs +285 -0
- package/typescript/package-lisa/package.lisa.json +6 -6
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"i18n-js": "^4.5.1",
|
|
70
70
|
"patch-package": "^8.0.0",
|
|
71
71
|
"react-hook-form": "^7.70.0",
|
|
72
|
-
"tar": ">=7.5.
|
|
72
|
+
"tar": ">=7.5.19",
|
|
73
73
|
"text-encoding-polyfill": "^0.6.7",
|
|
74
74
|
"usehooks-ts": "^3.1.1",
|
|
75
75
|
"zod": "^4.3.5"
|
|
@@ -98,19 +98,19 @@
|
|
|
98
98
|
},
|
|
99
99
|
"resolutions": {
|
|
100
100
|
"@isaacs/brace-expansion": "^5.0.1",
|
|
101
|
-
"brace-expansion": ">=5.0.
|
|
101
|
+
"brace-expansion": ">=5.0.8",
|
|
102
102
|
"eslint-plugin-react-hooks": "^7.0.0",
|
|
103
103
|
"fast-xml-parser": "^5.10.1",
|
|
104
|
-
"tar": ">=7.5.
|
|
104
|
+
"tar": ">=7.5.19",
|
|
105
105
|
"websocket-driver": ">=0.7.5"
|
|
106
106
|
},
|
|
107
107
|
"overrides": {
|
|
108
108
|
"@isaacs/brace-expansion": "^5.0.1",
|
|
109
|
-
"brace-expansion": ">=5.0.
|
|
109
|
+
"brace-expansion": ">=5.0.8",
|
|
110
110
|
"eslint-plugin-react-hooks": "^7.0.0",
|
|
111
111
|
"fast-xml-parser": "^5.10.1",
|
|
112
112
|
"zod-validation-error": "^4.0.0",
|
|
113
|
-
"tar": ">=7.5.
|
|
113
|
+
"tar": ">=7.5.19",
|
|
114
114
|
"websocket-driver": ">=0.7.5"
|
|
115
115
|
}
|
|
116
116
|
},
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"lint:fix": "oxlint --fix && eslint . --fix"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@apollo/server": "^5.
|
|
52
|
+
"@apollo/server": "^5.4.0",
|
|
53
53
|
"@as-integrations/express5": "^1.1.2",
|
|
54
54
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.967.0",
|
|
55
55
|
"@aws-sdk/client-cognito-identity-provider": "^3.969.0",
|
package/package.json
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"brace-expansion": ">=5.0.8"
|
|
116
116
|
},
|
|
117
117
|
"name": "@codyswann/lisa",
|
|
118
|
-
"version": "2.
|
|
118
|
+
"version": "2.319.0",
|
|
119
119
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
120
120
|
"main": "dist/index.js",
|
|
121
121
|
"exports": {
|
|
@@ -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,19 @@ 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
|
+
* The two remote surfaces share the capability but not the timing, and a reader
|
|
33
|
+
* adding a third should not assume otherwise. `codex-cloud` re-runs its setup
|
|
34
|
+
* script when a container resumes, so materializing there picks up a rotated
|
|
35
|
+
* value. `claude-web` *skips* its setup script whenever a filesystem cache
|
|
36
|
+
* exists, so materializing there would strand a rotated value until the cache
|
|
37
|
+
* expired — its materialize step runs from a session-start hook instead.
|
|
31
38
|
*/
|
|
32
39
|
export const SURFACES = {
|
|
33
40
|
local: { materialized: false, mayWriteValues: false },
|
|
34
41
|
"github-actions": { materialized: false, mayWriteValues: false },
|
|
35
42
|
"codex-cloud": { materialized: true, mayWriteValues: true },
|
|
43
|
+
"claude-web": { materialized: true, mayWriteValues: true },
|
|
36
44
|
};
|
|
37
45
|
|
|
38
46
|
/** Config defaults when `.lisa.config.json` carries no `secrets` block. */
|
|
@@ -68,6 +76,10 @@ export function detectSurface(configured = null, env = process.env) {
|
|
|
68
76
|
return explicit;
|
|
69
77
|
}
|
|
70
78
|
if ((env.GITHUB_ACTIONS ?? "") === "true") return "github-actions";
|
|
79
|
+
// Compared to the exact string rather than tested for presence: the variable
|
|
80
|
+
// is documented as carrying "true" in a cloud session and never being true
|
|
81
|
+
// locally, so a presence test would misread a shell that exports it empty.
|
|
82
|
+
if ((env.CLAUDE_CODE_REMOTE ?? "") === "true") return "claude-web";
|
|
71
83
|
if ((env.CODEX_SANDBOX ?? env.CODEX_HOME ?? "") !== "") return "codex-cloud";
|
|
72
84
|
return "local";
|
|
73
85
|
}
|
|
@@ -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.`
|
|
@@ -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,19 @@ 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
|
+
* The two remote surfaces share the capability but not the timing, and a reader
|
|
33
|
+
* adding a third should not assume otherwise. `codex-cloud` re-runs its setup
|
|
34
|
+
* script when a container resumes, so materializing there picks up a rotated
|
|
35
|
+
* value. `claude-web` *skips* its setup script whenever a filesystem cache
|
|
36
|
+
* exists, so materializing there would strand a rotated value until the cache
|
|
37
|
+
* expired — its materialize step runs from a session-start hook instead.
|
|
31
38
|
*/
|
|
32
39
|
export const SURFACES = {
|
|
33
40
|
local: { materialized: false, mayWriteValues: false },
|
|
34
41
|
"github-actions": { materialized: false, mayWriteValues: false },
|
|
35
42
|
"codex-cloud": { materialized: true, mayWriteValues: true },
|
|
43
|
+
"claude-web": { materialized: true, mayWriteValues: true },
|
|
36
44
|
};
|
|
37
45
|
|
|
38
46
|
/** Config defaults when `.lisa.config.json` carries no `secrets` block. */
|
|
@@ -68,6 +76,10 @@ export function detectSurface(configured = null, env = process.env) {
|
|
|
68
76
|
return explicit;
|
|
69
77
|
}
|
|
70
78
|
if ((env.GITHUB_ACTIONS ?? "") === "true") return "github-actions";
|
|
79
|
+
// Compared to the exact string rather than tested for presence: the variable
|
|
80
|
+
// is documented as carrying "true" in a cloud session and never being true
|
|
81
|
+
// locally, so a presence test would misread a shell that exports it empty.
|
|
82
|
+
if ((env.CLAUDE_CODE_REMOTE ?? "") === "true") return "claude-web";
|
|
71
83
|
if ((env.CODEX_SANDBOX ?? env.CODEX_HOME ?? "") !== "") return "codex-cloud";
|
|
72
84
|
return "local";
|
|
73
85
|
}
|
|
@@ -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.`
|
|
@@ -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,19 @@ 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
|
+
* The two remote surfaces share the capability but not the timing, and a reader
|
|
33
|
+
* adding a third should not assume otherwise. `codex-cloud` re-runs its setup
|
|
34
|
+
* script when a container resumes, so materializing there picks up a rotated
|
|
35
|
+
* value. `claude-web` *skips* its setup script whenever a filesystem cache
|
|
36
|
+
* exists, so materializing there would strand a rotated value until the cache
|
|
37
|
+
* expired — its materialize step runs from a session-start hook instead.
|
|
31
38
|
*/
|
|
32
39
|
export const SURFACES = {
|
|
33
40
|
local: { materialized: false, mayWriteValues: false },
|
|
34
41
|
"github-actions": { materialized: false, mayWriteValues: false },
|
|
35
42
|
"codex-cloud": { materialized: true, mayWriteValues: true },
|
|
43
|
+
"claude-web": { materialized: true, mayWriteValues: true },
|
|
36
44
|
};
|
|
37
45
|
|
|
38
46
|
/** Config defaults when `.lisa.config.json` carries no `secrets` block. */
|
|
@@ -68,6 +76,10 @@ export function detectSurface(configured = null, env = process.env) {
|
|
|
68
76
|
return explicit;
|
|
69
77
|
}
|
|
70
78
|
if ((env.GITHUB_ACTIONS ?? "") === "true") return "github-actions";
|
|
79
|
+
// Compared to the exact string rather than tested for presence: the variable
|
|
80
|
+
// is documented as carrying "true" in a cloud session and never being true
|
|
81
|
+
// locally, so a presence test would misread a shell that exports it empty.
|
|
82
|
+
if ((env.CLAUDE_CODE_REMOTE ?? "") === "true") return "claude-web";
|
|
71
83
|
if ((env.CODEX_SANDBOX ?? env.CODEX_HOME ?? "") !== "") return "codex-cloud";
|
|
72
84
|
return "local";
|
|
73
85
|
}
|
|
@@ -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.`
|