@clue-ai/cli 0.0.5 → 0.0.7
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/README.md +18 -7
- package/bin/clue-cli.mjs +898 -762
- package/commands/claude-code/clue-init.md +9 -2
- package/commands/codex/clue-init.md +9 -2
- package/package.json +1 -1
- package/src/ai-provider.mjs +147 -0
- package/src/command-spec.mjs +9 -7
- package/src/contracts.mjs +51 -16
- package/src/init-tool.mjs +158 -125
- package/src/lifecycle-init.mjs +180 -205
- package/src/public-schema.cjs +48 -1
- package/src/semantic-agent-runner.mjs +157 -0
- package/src/semantic-ai-config.mjs +17 -0
- package/src/semantic-ci.mjs +525 -204
- package/src/setup-check.mjs +399 -372
- package/src/setup-prepare.mjs +361 -147
- package/src/setup-tool.mjs +379 -229
package/src/setup-check.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { access, readFile } from "node:fs/promises";
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { validateSemanticCiRequest } from "./contracts.mjs";
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
findLifecycleCallApiNames,
|
|
6
|
+
findLifecycleGuardViolations,
|
|
6
7
|
} from "./lifecycle-guard.mjs";
|
|
7
8
|
import { listAllowedSourceFiles } from "./path-policy.mjs";
|
|
8
9
|
import { runSemanticInventory } from "./semantic-ci.mjs";
|
|
@@ -10,426 +11,452 @@ import { runSemanticInventory } from "./semantic-ci.mjs";
|
|
|
10
11
|
const DEFAULT_WORKFLOW_PATH = ".github/workflows/clue-semantic-snapshot.yml";
|
|
11
12
|
|
|
12
13
|
const disallowedWorkflowMetadataPattern =
|
|
13
|
-
|
|
14
|
+
/github\.(actor|triggering_actor|repository_owner)|github\.event\.sender|github\.event\.repository\.name|"default_branch"\s*:/;
|
|
14
15
|
const SETUP_SKILLS = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
"clue-setup-orchestrator",
|
|
17
|
+
"clue-route-semantic-snapshot",
|
|
18
|
+
"clue-semantic-gen",
|
|
19
|
+
"clue-sdk-instrumentation",
|
|
20
|
+
"clue-setup-audit",
|
|
21
|
+
"clue-local-verification",
|
|
22
|
+
"clue-setup-report",
|
|
22
23
|
];
|
|
23
24
|
const TARGET_SKILL_ROOTS = {
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
codex: [".agents", "skills"],
|
|
26
|
+
claude_code: [".claude", "skills"],
|
|
26
27
|
};
|
|
27
28
|
const SOURCE_EXTENSIONS = [".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
28
29
|
const REQUIRED_LIFECYCLE_APIS = [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
"ClueInit",
|
|
31
|
+
"ClueIdentify",
|
|
32
|
+
"ClueSetAccount",
|
|
33
|
+
"ClueLogout",
|
|
33
34
|
];
|
|
35
|
+
|
|
36
|
+
const semanticRequestFromWorkflow = (workflow) => {
|
|
37
|
+
const match = workflow.match(
|
|
38
|
+
/CLUE_SEMANTIC_REQUEST_JSON: \|\n([\s\S]*?)\n\s*run: \|/,
|
|
39
|
+
);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const requestJson = match[1]
|
|
44
|
+
.split("\n")
|
|
45
|
+
.map((line) => line.replace(/^ {12}/, ""))
|
|
46
|
+
.join("\n");
|
|
47
|
+
try {
|
|
48
|
+
return validateSemanticCiRequest(JSON.parse(requestJson));
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
34
53
|
const BACKEND_SDK_MARKERS = [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
54
|
+
"clue-fastapi-sdk",
|
|
55
|
+
"clue-django-sdk",
|
|
56
|
+
"clue-python-sdk-core",
|
|
57
|
+
"clue_fastapi_sdk",
|
|
58
|
+
"clue_django_sdk",
|
|
59
|
+
"clue_python_sdk_core",
|
|
41
60
|
];
|
|
42
61
|
const DEPENDENCY_FILE_CANDIDATES = [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
"package.json",
|
|
63
|
+
"pnpm-lock.yaml",
|
|
64
|
+
"package-lock.json",
|
|
65
|
+
"yarn.lock",
|
|
66
|
+
"requirements.txt",
|
|
67
|
+
"requirements-dev.txt",
|
|
68
|
+
"pyproject.toml",
|
|
69
|
+
"poetry.lock",
|
|
70
|
+
"Pipfile",
|
|
52
71
|
];
|
|
53
72
|
const exists = async (path) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
try {
|
|
74
|
+
await access(path);
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
60
79
|
};
|
|
61
80
|
|
|
62
81
|
const normalizeTarget = (target) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
82
|
+
if (typeof target !== "string" || target.trim() === "") return undefined;
|
|
83
|
+
const normalized = target
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.replace(/[\s-]+/g, "_");
|
|
87
|
+
if (!TARGET_SKILL_ROOTS[normalized]) {
|
|
88
|
+
throw new Error("target must be codex or claude_code");
|
|
89
|
+
}
|
|
90
|
+
return normalized;
|
|
72
91
|
};
|
|
73
92
|
|
|
74
93
|
const addCheck = (checks, id, passed, summary, details = {}) => {
|
|
75
|
-
|
|
94
|
+
checks.push({ id, passed, summary, ...details });
|
|
76
95
|
};
|
|
77
96
|
|
|
78
97
|
const readAllowedSourceText = async ({
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
repoRoot,
|
|
99
|
+
allowedSourcePaths,
|
|
100
|
+
excludedSourcePaths,
|
|
82
101
|
}) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
const files = await listAllowedSourceFiles({
|
|
103
|
+
repoRoot,
|
|
104
|
+
allowedSourcePaths,
|
|
105
|
+
excludedSourcePaths,
|
|
106
|
+
extensions: SOURCE_EXTENSIONS,
|
|
107
|
+
});
|
|
108
|
+
const sources = [];
|
|
109
|
+
for (const absolutePath of files) {
|
|
110
|
+
sources.push({
|
|
111
|
+
file_path: relative(resolve(repoRoot), absolutePath),
|
|
112
|
+
text: await readFile(absolutePath, "utf8"),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return sources;
|
|
97
116
|
};
|
|
98
117
|
|
|
99
118
|
const readDependencyText = async ({ repoRoot, roots }) => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
const candidatePaths = [
|
|
120
|
+
...DEPENDENCY_FILE_CANDIDATES,
|
|
121
|
+
...roots.flatMap((root) =>
|
|
122
|
+
DEPENDENCY_FILE_CANDIDATES.map((file) => join(root, file)),
|
|
123
|
+
),
|
|
124
|
+
];
|
|
125
|
+
const sources = [];
|
|
126
|
+
for (const path of [...new Set(candidatePaths)]) {
|
|
127
|
+
const absolutePath = join(repoRoot, path);
|
|
128
|
+
if (!(await exists(absolutePath))) continue;
|
|
129
|
+
sources.push({
|
|
130
|
+
file_path: path,
|
|
131
|
+
text: await readFile(absolutePath, "utf8"),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return sources;
|
|
116
135
|
};
|
|
117
136
|
|
|
118
137
|
const setupSourcePaths = async ({ repoRoot, request, includeFrontend }) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
const requested = request?.allowed_source_paths?.length
|
|
139
|
+
? request.allowed_source_paths
|
|
140
|
+
: ["."];
|
|
141
|
+
if (!includeFrontend) return requested;
|
|
142
|
+
const candidates = [
|
|
143
|
+
...requested,
|
|
144
|
+
"frontend/src",
|
|
145
|
+
"src",
|
|
146
|
+
"app",
|
|
147
|
+
"apps/web/src",
|
|
148
|
+
"packages/frontend",
|
|
149
|
+
];
|
|
150
|
+
const existing = [];
|
|
151
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
152
|
+
if (await exists(join(repoRoot, candidate))) {
|
|
153
|
+
existing.push(candidate);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return existing.length ? existing : requested;
|
|
138
157
|
};
|
|
139
158
|
|
|
140
159
|
const secretLeakPatterns = [
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
160
|
+
/pk_(live|test)_[A-Za-z0-9_-]+/,
|
|
161
|
+
/sk_(live|test)_[A-Za-z0-9_-]+/,
|
|
162
|
+
/npm_[A-Za-z0-9]{20,}/,
|
|
163
|
+
/CLUE_API_KEY\s*[:=]\s*["'][^"']+["']/,
|
|
164
|
+
/CLUE_AI_PROVIDER_API_KEY\s*[:=]\s*["'][^"']+["']/,
|
|
146
165
|
];
|
|
147
166
|
|
|
148
167
|
const findSecretLeaks = (sources) =>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
168
|
+
sources.flatMap((source) =>
|
|
169
|
+
secretLeakPatterns.some((pattern) => pattern.test(source.text))
|
|
170
|
+
? [source.file_path]
|
|
171
|
+
: [],
|
|
172
|
+
);
|
|
154
173
|
|
|
155
174
|
const startsWithRoot = (filePath, root) =>
|
|
156
|
-
|
|
175
|
+
filePath === root || filePath.startsWith(`${root.replace(/\/+$/, "")}/`);
|
|
157
176
|
|
|
158
177
|
const hasAnyMarker = (text, markers) =>
|
|
159
|
-
|
|
178
|
+
markers.some((marker) => text.includes(marker));
|
|
160
179
|
|
|
161
180
|
const checkSdkLifecycle = ({
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
181
|
+
backendRootPaths = [],
|
|
182
|
+
dependencySources = [],
|
|
183
|
+
sources,
|
|
165
184
|
}) => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
185
|
+
const combined = sources.map((source) => source.text).join("\n");
|
|
186
|
+
const backendSources = sources.filter((source) =>
|
|
187
|
+
backendRootPaths.some((root) => startsWithRoot(source.file_path, root)),
|
|
188
|
+
);
|
|
189
|
+
const backendCombined = backendSources
|
|
190
|
+
.map((source) => source.text)
|
|
191
|
+
.join("\n");
|
|
192
|
+
const dependencyCombined = dependencySources
|
|
193
|
+
.map((source) => source.text)
|
|
194
|
+
.join("\n");
|
|
195
|
+
const foundApiNames = findLifecycleCallApiNames(combined);
|
|
196
|
+
const backendFoundApiNames = findLifecycleCallApiNames(backendCombined);
|
|
197
|
+
const foundApis = REQUIRED_LIFECYCLE_APIS.filter((api) =>
|
|
198
|
+
foundApiNames.includes(api),
|
|
199
|
+
);
|
|
200
|
+
const missingApis = REQUIRED_LIFECYCLE_APIS.filter(
|
|
201
|
+
(api) => !foundApis.includes(api),
|
|
202
|
+
);
|
|
203
|
+
const noOpPattern =
|
|
204
|
+
/window\.Clue(?:Init|Identify|SetAccount|Logout)|(?:function|const|let|var)\s+Clue(?:Init|Identify|SetAccount|Logout)\b/;
|
|
205
|
+
const componentLifecycleInitFiles = sources
|
|
206
|
+
.filter((source) =>
|
|
207
|
+
/useEffect\s*\([\s\S]{0,1200}ClueInit/.test(source.text),
|
|
208
|
+
)
|
|
209
|
+
.map((source) => source.file_path);
|
|
210
|
+
const unguardedLifecycleCalls = sources.flatMap((source) =>
|
|
211
|
+
findLifecycleGuardViolations(source.text).map((violation) => ({
|
|
212
|
+
file_path: source.file_path,
|
|
213
|
+
...violation,
|
|
214
|
+
})),
|
|
215
|
+
);
|
|
216
|
+
const unguardedLifecycleFiles = [
|
|
217
|
+
...new Set(unguardedLifecycleCalls.map((violation) => violation.file_path)),
|
|
218
|
+
];
|
|
219
|
+
const backendPresent = backendSources.length > 0;
|
|
220
|
+
const backendSdkPresent =
|
|
221
|
+
!backendPresent ||
|
|
222
|
+
hasAnyMarker(
|
|
223
|
+
`${backendCombined}\n${dependencyCombined}`,
|
|
224
|
+
BACKEND_SDK_MARKERS,
|
|
225
|
+
);
|
|
226
|
+
const backendInitPresent =
|
|
227
|
+
!backendPresent ||
|
|
228
|
+
/clue_init_fastapi|clue_init_django|configure_settings|CluePythonBootstrapConfig/.test(
|
|
229
|
+
backendCombined,
|
|
230
|
+
);
|
|
231
|
+
const backendIdentityRequired =
|
|
232
|
+
backendPresent &&
|
|
233
|
+
/\b(login|signin|sign_in|auth|token|session)\b/i.test(backendCombined);
|
|
234
|
+
const backendLogoutRequired =
|
|
235
|
+
backendPresent && /\b(logout|signout|sign_out)\b/i.test(backendCombined);
|
|
236
|
+
const backendAccountRequired =
|
|
237
|
+
backendPresent &&
|
|
238
|
+
/\b(account|workspace|organization|tenant)\b/i.test(backendCombined);
|
|
239
|
+
const backendMissingApis = [
|
|
240
|
+
...(backendIdentityRequired &&
|
|
241
|
+
!backendFoundApiNames.includes("ClueIdentify")
|
|
242
|
+
? ["ClueIdentify"]
|
|
243
|
+
: []),
|
|
244
|
+
...(backendLogoutRequired && !backendFoundApiNames.includes("ClueLogout")
|
|
245
|
+
? ["ClueLogout"]
|
|
246
|
+
: []),
|
|
247
|
+
...(backendAccountRequired &&
|
|
248
|
+
!backendFoundApiNames.includes("ClueSetAccount")
|
|
249
|
+
? ["ClueSetAccount"]
|
|
250
|
+
: []),
|
|
251
|
+
];
|
|
252
|
+
return {
|
|
253
|
+
foundApis,
|
|
254
|
+
missingApis,
|
|
255
|
+
backend_lifecycle: {
|
|
256
|
+
backend_present: backendPresent,
|
|
257
|
+
backend_root_paths: backendRootPaths,
|
|
258
|
+
dependency_files: dependencySources.map((source) => source.file_path),
|
|
259
|
+
sdk_dependency_or_import_present: backendSdkPresent,
|
|
260
|
+
sdk_init_present: backendInitPresent,
|
|
261
|
+
required_apis: [
|
|
262
|
+
...(backendIdentityRequired ? ["ClueIdentify"] : []),
|
|
263
|
+
...(backendAccountRequired ? ["ClueSetAccount"] : []),
|
|
264
|
+
...(backendLogoutRequired ? ["ClueLogout"] : []),
|
|
265
|
+
],
|
|
266
|
+
missing_apis: backendMissingApis,
|
|
267
|
+
},
|
|
268
|
+
has_noop_wrapper: noOpPattern.test(combined),
|
|
269
|
+
component_lifecycle_init_files: componentLifecycleInitFiles,
|
|
270
|
+
unguarded_lifecycle_files: unguardedLifecycleFiles,
|
|
271
|
+
unguarded_lifecycle_calls: unguardedLifecycleCalls,
|
|
272
|
+
passed:
|
|
273
|
+
missingApis.length === 0 &&
|
|
274
|
+
backendSdkPresent &&
|
|
275
|
+
backendInitPresent &&
|
|
276
|
+
backendMissingApis.length === 0 &&
|
|
277
|
+
!noOpPattern.test(combined) &&
|
|
278
|
+
componentLifecycleInitFiles.length === 0 &&
|
|
279
|
+
unguardedLifecycleFiles.length === 0,
|
|
280
|
+
};
|
|
262
281
|
};
|
|
263
282
|
|
|
264
283
|
export const runSetupCheck = async ({
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
284
|
+
repoRoot,
|
|
285
|
+
target,
|
|
286
|
+
request,
|
|
287
|
+
requireSdkLifecycle = false,
|
|
269
288
|
}) => {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
289
|
+
const resolvedRepoRoot = resolve(repoRoot ?? ".");
|
|
290
|
+
const checks = [];
|
|
291
|
+
const normalizedTarget = normalizeTarget(target);
|
|
273
292
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
293
|
+
if (normalizedTarget) {
|
|
294
|
+
const skillRoot = join(
|
|
295
|
+
resolvedRepoRoot,
|
|
296
|
+
...TARGET_SKILL_ROOTS[normalizedTarget],
|
|
297
|
+
);
|
|
298
|
+
const missingSkills = [];
|
|
299
|
+
for (const skillName of SETUP_SKILLS) {
|
|
300
|
+
const skillPath = join(skillRoot, skillName, "SKILL.md");
|
|
301
|
+
if (!(await exists(skillPath))) {
|
|
302
|
+
missingSkills.push(
|
|
303
|
+
join(...TARGET_SKILL_ROOTS[normalizedTarget], skillName, "SKILL.md"),
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
addCheck(
|
|
308
|
+
checks,
|
|
309
|
+
"setup_skills",
|
|
310
|
+
missingSkills.length === 0,
|
|
311
|
+
missingSkills.length === 0
|
|
312
|
+
? "setup skills are installed"
|
|
313
|
+
: "setup skills are missing",
|
|
314
|
+
{ missing_files: missingSkills },
|
|
315
|
+
);
|
|
316
|
+
}
|
|
298
317
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
318
|
+
const workflowPath = request?.ci_workflow_path ?? DEFAULT_WORKFLOW_PATH;
|
|
319
|
+
const absoluteWorkflowPath = join(resolvedRepoRoot, workflowPath);
|
|
320
|
+
if (await exists(absoluteWorkflowPath)) {
|
|
321
|
+
const workflow = await readFile(absoluteWorkflowPath, "utf8");
|
|
322
|
+
const semanticRequest = semanticRequestFromWorkflow(workflow);
|
|
323
|
+
addCheck(
|
|
324
|
+
checks,
|
|
325
|
+
"semantic_workflow",
|
|
326
|
+
workflow.includes(
|
|
327
|
+
"npx @clue-ai/cli semantic-gen --request-env CLUE_SEMANTIC_REQUEST_JSON --repo .",
|
|
328
|
+
) &&
|
|
329
|
+
semanticRequest !== null &&
|
|
330
|
+
workflow.includes("CLUE_SEMANTIC_REQUEST_JSON: |") &&
|
|
331
|
+
workflow.includes("CLUE_API_KEY: ${{ secrets.CLUE_API_KEY }}") &&
|
|
332
|
+
workflow.includes(
|
|
333
|
+
"CLUE_AI_PROVIDER_API_KEY: ${{ secrets.CLUE_AI_PROVIDER_API_KEY }}",
|
|
334
|
+
) &&
|
|
335
|
+
workflow.includes("CLUE_AI_PROVIDER: ${{ vars.CLUE_AI_PROVIDER }}") &&
|
|
336
|
+
workflow.includes("CLUE_AI_MODEL: ${{ vars.CLUE_AI_MODEL }}") &&
|
|
337
|
+
!workflow.includes('"ai_provider": "${{ vars.CLUE_AI_PROVIDER }}"') &&
|
|
338
|
+
!workflow.includes('"ai_provider_base_url"') &&
|
|
339
|
+
!workflow.includes('"ai_model": "${{ vars.CLUE_AI_MODEL }}"') &&
|
|
340
|
+
!workflow.includes('"root_path"') &&
|
|
341
|
+
workflow.includes("permissions:\n contents: read") &&
|
|
342
|
+
workflow.includes("persist-credentials: false") &&
|
|
343
|
+
!disallowedWorkflowMetadataPattern.test(workflow),
|
|
344
|
+
"semantic workflow uses the canonical env runtime request, least-privilege checkout, and privacy-minimized GitHub metadata",
|
|
345
|
+
{ workflow_path: workflowPath },
|
|
346
|
+
);
|
|
347
|
+
} else {
|
|
348
|
+
addCheck(
|
|
349
|
+
checks,
|
|
350
|
+
"semantic_workflow",
|
|
351
|
+
false,
|
|
352
|
+
"semantic workflow is missing",
|
|
353
|
+
{
|
|
354
|
+
workflow_path: workflowPath,
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
}
|
|
331
358
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
359
|
+
addCheck(
|
|
360
|
+
checks,
|
|
361
|
+
"runtime_request_not_committed",
|
|
362
|
+
!(await exists(
|
|
363
|
+
join(resolvedRepoRoot, ".clue", "semantic-request.runtime.json"),
|
|
364
|
+
)),
|
|
365
|
+
".clue/semantic-request.runtime.json is not present in the repository",
|
|
366
|
+
);
|
|
340
367
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
+
if (request) {
|
|
369
|
+
try {
|
|
370
|
+
const inventory = await runSemanticInventory({
|
|
371
|
+
repoRoot: resolvedRepoRoot,
|
|
372
|
+
request,
|
|
373
|
+
});
|
|
374
|
+
addCheck(
|
|
375
|
+
checks,
|
|
376
|
+
"semantic_inventory",
|
|
377
|
+
true,
|
|
378
|
+
"semantic inventory discovers routes",
|
|
379
|
+
{
|
|
380
|
+
route_count: inventory.route_count,
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
addCheck(
|
|
385
|
+
checks,
|
|
386
|
+
"semantic_inventory",
|
|
387
|
+
false,
|
|
388
|
+
"semantic inventory failed",
|
|
389
|
+
{
|
|
390
|
+
error: error instanceof Error ? error.message : String(error),
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
368
395
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
396
|
+
const sourcePaths = await setupSourcePaths({
|
|
397
|
+
repoRoot: resolvedRepoRoot,
|
|
398
|
+
request,
|
|
399
|
+
includeFrontend: requireSdkLifecycle,
|
|
400
|
+
});
|
|
401
|
+
const excludedPaths = request?.excluded_source_paths ?? [];
|
|
402
|
+
const sources = await readAllowedSourceText({
|
|
403
|
+
repoRoot: resolvedRepoRoot,
|
|
404
|
+
allowedSourcePaths: sourcePaths,
|
|
405
|
+
excludedSourcePaths: excludedPaths,
|
|
406
|
+
});
|
|
407
|
+
const dependencySources = await readDependencyText({
|
|
408
|
+
repoRoot: resolvedRepoRoot,
|
|
409
|
+
roots: request?.allowed_source_paths ?? [],
|
|
410
|
+
});
|
|
411
|
+
const secretLeaks = findSecretLeaks([
|
|
412
|
+
...sources,
|
|
413
|
+
...dependencySources,
|
|
414
|
+
...((await exists(absoluteWorkflowPath))
|
|
415
|
+
? [
|
|
416
|
+
{
|
|
417
|
+
file_path: workflowPath,
|
|
418
|
+
text: await readFile(absoluteWorkflowPath, "utf8"),
|
|
419
|
+
},
|
|
420
|
+
]
|
|
421
|
+
: []),
|
|
422
|
+
]);
|
|
423
|
+
addCheck(
|
|
424
|
+
checks,
|
|
425
|
+
"no_secret_values",
|
|
426
|
+
secretLeaks.length === 0,
|
|
427
|
+
"no obvious secret values found",
|
|
428
|
+
{
|
|
429
|
+
files: secretLeaks,
|
|
430
|
+
},
|
|
431
|
+
);
|
|
405
432
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
433
|
+
if (requireSdkLifecycle) {
|
|
434
|
+
const sdkLifecycle = checkSdkLifecycle({
|
|
435
|
+
backendRootPaths: request?.allowed_source_paths ?? [],
|
|
436
|
+
dependencySources,
|
|
437
|
+
sources,
|
|
438
|
+
});
|
|
439
|
+
addCheck(
|
|
440
|
+
checks,
|
|
441
|
+
"sdk_lifecycle",
|
|
442
|
+
sdkLifecycle.passed,
|
|
443
|
+
"SDK lifecycle calls resolve to real source code instead of no-op globals",
|
|
444
|
+
{
|
|
445
|
+
found_apis: sdkLifecycle.foundApis,
|
|
446
|
+
missing_apis: sdkLifecycle.missingApis,
|
|
447
|
+
backend_lifecycle: sdkLifecycle.backend_lifecycle,
|
|
448
|
+
has_noop_wrapper: sdkLifecycle.has_noop_wrapper,
|
|
449
|
+
component_lifecycle_init_files:
|
|
450
|
+
sdkLifecycle.component_lifecycle_init_files,
|
|
451
|
+
unguarded_lifecycle_files: sdkLifecycle.unguarded_lifecycle_files,
|
|
452
|
+
unguarded_lifecycle_calls: sdkLifecycle.unguarded_lifecycle_calls,
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
}
|
|
429
456
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
457
|
+
const passed = checks.every((check) => check.passed);
|
|
458
|
+
return {
|
|
459
|
+
passed,
|
|
460
|
+
checks,
|
|
461
|
+
};
|
|
435
462
|
};
|