causly-server 0.0.0-stage → 1.5.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/.env.example +120 -0
- package/CHANGELOG.md +121 -0
- package/LICENSE +21 -0
- package/README.md +205 -3
- package/bin/causly.js +192 -0
- package/index.js +229 -0
- package/package.json +59 -6
- package/packs/calendar.js +141 -0
- package/packs/command.js +22 -0
- package/packs/database.js +95 -0
- package/packs/directory.js +63 -0
- package/packs/discord.js +63 -0
- package/packs/docker.js +202 -0
- package/packs/files.js +106 -0
- package/packs/git.js +211 -0
- package/packs/github.js +321 -0
- package/packs/gmail.js +98 -0
- package/packs/leads.js +46 -0
- package/packs/linear.js +104 -0
- package/packs/loader.js +247 -0
- package/packs/manifest.json +1152 -0
- package/packs/meetings.js +141 -0
- package/packs/notion.js +185 -0
- package/packs/pipeline.js +151 -0
- package/packs/project.js +70 -0
- package/packs/registry.js +149 -0
- package/packs/reminders.js +57 -0
- package/packs/research.js +34 -0
- package/packs/secrets.js +77 -0
- package/packs/sentry.js +97 -0
- package/packs/seo.js +70 -0
- package/packs/slack.js +95 -0
- package/packs/stripe.js +127 -0
- package/packs/supabase.js +73 -0
- package/packs/telegram.js +70 -0
- package/packs/terraform.js +247 -0
- package/packs/vercel.js +114 -0
- package/packs/workflow.js +84 -0
- package/prompts/cofounder.js +66 -0
- package/prompts/playbooks.js +75 -0
- package/tools/backupOps.js +87 -0
- package/tools/boundaryAttest.js +184 -0
- package/tools/calendarOps.js +563 -0
- package/tools/cliOps.js +233 -0
- package/tools/commandOps.js +198 -0
- package/tools/dbOps.js +117 -0
- package/tools/directoryOps.js +96 -0
- package/tools/discordOps.js +133 -0
- package/tools/dockerOps.js +259 -0
- package/tools/envFile.js +89 -0
- package/tools/envLoader.js +85 -0
- package/tools/fileOps.js +125 -0
- package/tools/gitOps.js +170 -0
- package/tools/githubOps.js +357 -0
- package/tools/gmailOps.js +221 -0
- package/tools/googleAuth.js +51 -0
- package/tools/googleLogin.js +230 -0
- package/tools/leadOps.js +447 -0
- package/tools/linearOps.js +304 -0
- package/tools/logger.js +47 -0
- package/tools/meetingOps.js +697 -0
- package/tools/notionOps.js +215 -0
- package/tools/paths.js +36 -0
- package/tools/pipelineOps.js +511 -0
- package/tools/projectOps.js +192 -0
- package/tools/reminderAlerts.js +48 -0
- package/tools/reminderOps.js +129 -0
- package/tools/scheduler.js +46 -0
- package/tools/secretsOps.js +133 -0
- package/tools/security.js +488 -0
- package/tools/sentryOps.js +130 -0
- package/tools/seoOps.js +561 -0
- package/tools/services.js +295 -0
- package/tools/setupOps.js +245 -0
- package/tools/slackOps.js +151 -0
- package/tools/statusOps.js +104 -0
- package/tools/store.js +111 -0
- package/tools/stripeOps.js +299 -0
- package/tools/supabaseOps.js +94 -0
- package/tools/telegramOps.js +153 -0
- package/tools/terminal.js +124 -0
- package/tools/terraformOps.js +171 -0
- package/tools/toolWrapper.js +60 -0
- package/tools/toolsetOps.js +123 -0
- package/tools/transcribeOps.js +221 -0
- package/tools/updateCheck.js +58 -0
- package/tools/vercelOps.js +198 -0
- package/tools/verifyBoundaryAttest.js +25 -0
- package/tools/webOps.js +474 -0
- package/tools/workflowOps.js +371 -0
- package/tools/workspace.js +85 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import * as gitOps from "./gitOps.js";
|
|
2
|
+
import * as githubOps from "./githubOps.js";
|
|
3
|
+
import * as projectOps from "./projectOps.js";
|
|
4
|
+
import * as vercelOps from "./vercelOps.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract "owner" and "repo" from a git remote URL like
|
|
8
|
+
* https://github.com/owner/repo.git or git@github.com:owner/repo.git
|
|
9
|
+
*/
|
|
10
|
+
function parseOwnerRepoFromRemoteUrl(url) {
|
|
11
|
+
if (!url) return null;
|
|
12
|
+
const httpsMatch = url.match(/github\.com[/:]([^/]+)\/([^/.]+)(\.git)?$/);
|
|
13
|
+
if (httpsMatch) return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* fix_ci — investigate a failed GitHub Actions run and prepare the fix loop.
|
|
19
|
+
*
|
|
20
|
+
* This does NOT write code fixes itself (that requires the AI's own
|
|
21
|
+
* reasoning + file tools). Instead it does the mechanical investigation
|
|
22
|
+
* work end-to-end — find the failing run, find the failing job(s), pull
|
|
23
|
+
* the actual log text — so the AI can read the logs, make the fix with
|
|
24
|
+
* file tools, then call `verify_ci_fix` (below) to commit/push and confirm
|
|
25
|
+
* the run goes green.
|
|
26
|
+
*
|
|
27
|
+
* Flow:
|
|
28
|
+
* find latest failing run on branch -> list its jobs -> find failed jobs
|
|
29
|
+
* -> pull logs for each failed job -> return structured diagnosis
|
|
30
|
+
*/
|
|
31
|
+
export async function fixCi({ owner, repo, branch, run_id }) {
|
|
32
|
+
const steps = [];
|
|
33
|
+
|
|
34
|
+
// 1. Resolve the run to investigate
|
|
35
|
+
let run;
|
|
36
|
+
if (run_id) {
|
|
37
|
+
run = await githubOps.githubGetWorkflowRun({ owner, repo, run_id });
|
|
38
|
+
steps.push({ step: "get_workflow_run", run });
|
|
39
|
+
} else {
|
|
40
|
+
const runs = await githubOps.githubListWorkflowRuns({ owner, repo, branch, status: "failure", per_page: 5 });
|
|
41
|
+
steps.push({ step: "list_workflow_runs", found: runs.runs?.length || 0 });
|
|
42
|
+
if (!runs.runs || runs.runs.length === 0) {
|
|
43
|
+
return { ok: false, reason: `No failing workflow runs found${branch ? ` on branch '${branch}'` : ""}.`, steps };
|
|
44
|
+
}
|
|
45
|
+
run = runs.runs[0]; // most recent
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (run.conclusion && run.conclusion !== "failure" && run.status !== "in_progress") {
|
|
49
|
+
return { ok: false, reason: `Run ${run.id} is not currently failing (conclusion: ${run.conclusion}).`, steps, run };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. Get jobs for that run
|
|
53
|
+
const jobsResult = await githubOps.githubGetWorkflowRunJobs({ owner, repo, run_id: run.id });
|
|
54
|
+
steps.push({ step: "get_workflow_run_jobs", job_count: jobsResult.jobs?.length || 0 });
|
|
55
|
+
|
|
56
|
+
const failedJobs = (jobsResult.jobs || []).filter((j) => j.conclusion === "failure");
|
|
57
|
+
if (failedJobs.length === 0) {
|
|
58
|
+
return { ok: false, reason: `No failed jobs found in run ${run.id} — it may still be in progress.`, steps, run, jobs: jobsResult.jobs };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. Pull logs for each failed job
|
|
62
|
+
const diagnosis = [];
|
|
63
|
+
for (const job of failedJobs) {
|
|
64
|
+
try {
|
|
65
|
+
const logResult = await githubOps.githubGetJobLogs({ owner, repo, job_id: job.id });
|
|
66
|
+
const failedSteps = (job.steps || []).filter((s) => s.conclusion === "failure");
|
|
67
|
+
diagnosis.push({
|
|
68
|
+
job_id: job.id,
|
|
69
|
+
job_name: job.name,
|
|
70
|
+
failed_steps: failedSteps.map((s) => s.name),
|
|
71
|
+
html_url: job.html_url,
|
|
72
|
+
// Trim to the tail — that's where the actual error usually is, and
|
|
73
|
+
// full CI logs can be enormous.
|
|
74
|
+
logs_tail: logResult.logs.slice(-8000),
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
diagnosis.push({ job_id: job.id, job_name: job.name, error: `Could not fetch logs: ${err.message}` });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
steps.push({ step: "fetch_job_logs", jobs_diagnosed: diagnosis.length });
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
ok: true,
|
|
84
|
+
run: { id: run.id, branch: run.branch || run.head_branch, html_url: run.html_url },
|
|
85
|
+
failed_jobs: diagnosis,
|
|
86
|
+
steps,
|
|
87
|
+
next_step:
|
|
88
|
+
"Read logs_tail for each failed job, fix the code with file tools, then call verify_ci_fix to commit, push, and confirm CI passes.",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* verify_ci_fix — after the AI has made a code fix on disk, commit + push
|
|
94
|
+
* it and poll the new workflow run until it finishes, reporting the result.
|
|
95
|
+
*
|
|
96
|
+
* Flow:
|
|
97
|
+
* commit -> push -> poll new run on branch until complete -> report pass/fail
|
|
98
|
+
*/
|
|
99
|
+
export async function verifyCiFix({
|
|
100
|
+
repo_path,
|
|
101
|
+
owner,
|
|
102
|
+
repo,
|
|
103
|
+
branch,
|
|
104
|
+
commit_message,
|
|
105
|
+
poll_interval_ms = 15_000,
|
|
106
|
+
max_polls = 20,
|
|
107
|
+
}) {
|
|
108
|
+
const steps = [];
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await gitOps.gitAdd({ repo_path, files: "." });
|
|
112
|
+
const commitResult = await gitOps.gitCommit({ repo_path, message: commit_message });
|
|
113
|
+
steps.push({ step: "commit", ...commitResult });
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return { ok: false, failed_step: "commit", reason: err.message, steps };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const pushResult = await gitOps.gitPush({ repo_path, branch });
|
|
120
|
+
steps.push({ step: "push", ...pushResult });
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return { ok: false, failed_step: "push", reason: err.message, steps };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Poll for the new run to appear and finish
|
|
126
|
+
let latestRun = null;
|
|
127
|
+
for (let i = 0; i < max_polls; i++) {
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, poll_interval_ms));
|
|
129
|
+
const runs = await githubOps.githubListWorkflowRuns({ owner, repo, branch, per_page: 1 });
|
|
130
|
+
latestRun = runs.runs?.[0] || null;
|
|
131
|
+
if (latestRun && latestRun.status === "completed") break;
|
|
132
|
+
}
|
|
133
|
+
steps.push({ step: "poll_workflow_run", final_status: latestRun?.status, conclusion: latestRun?.conclusion });
|
|
134
|
+
|
|
135
|
+
if (!latestRun) {
|
|
136
|
+
return { ok: false, failed_step: "poll_workflow_run", reason: "No workflow run found after push.", steps };
|
|
137
|
+
}
|
|
138
|
+
if (latestRun.status !== "completed") {
|
|
139
|
+
return { ok: null, reason: "Run still in progress after max_polls — check back later.", run: latestRun, steps };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
ok: latestRun.conclusion === "success",
|
|
144
|
+
run: latestRun,
|
|
145
|
+
steps,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* ship_change — take a set of already-made file edits from request to PR.
|
|
151
|
+
*
|
|
152
|
+
* Assumes the caller (the AI, using file/directory tools) has already made
|
|
153
|
+
* the code changes on disk. This workflow does NOT edit files; it takes the
|
|
154
|
+
* project from "dirty working tree" to "open pull request", running checks
|
|
155
|
+
* along the way and stopping early if anything fails.
|
|
156
|
+
*
|
|
157
|
+
* Flow:
|
|
158
|
+
* inspect changes -> create branch -> run checks -> commit -> push -> open PR
|
|
159
|
+
*/
|
|
160
|
+
export async function shipChange({
|
|
161
|
+
repo_path,
|
|
162
|
+
branch_name,
|
|
163
|
+
commit_message,
|
|
164
|
+
pr_title,
|
|
165
|
+
pr_body = "",
|
|
166
|
+
base = "main",
|
|
167
|
+
owner,
|
|
168
|
+
repo,
|
|
169
|
+
run_checks = true,
|
|
170
|
+
skip_if_no_changes = true,
|
|
171
|
+
}) {
|
|
172
|
+
const steps = [];
|
|
173
|
+
const fail = (step, reason, extra = {}) => ({
|
|
174
|
+
ok: false,
|
|
175
|
+
failed_step: step,
|
|
176
|
+
reason,
|
|
177
|
+
steps,
|
|
178
|
+
...extra,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// 1. Inspect current changes
|
|
182
|
+
const changed = await gitOps.gitChangedFiles({ repo_path });
|
|
183
|
+
steps.push({ step: "inspect_changes", changed_files: changed.changed_files });
|
|
184
|
+
if (skip_if_no_changes && changed.changed_files.length === 0) {
|
|
185
|
+
return fail("inspect_changes", "No changes detected in the working tree — nothing to ship.");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 2. Create + checkout branch (fall back to checkout if it already exists)
|
|
189
|
+
try {
|
|
190
|
+
const branchResult = await gitOps.gitCreateBranch({ repo_path, branch_name, checkout: true });
|
|
191
|
+
steps.push({ step: "create_branch", ...branchResult });
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (/already exists/i.test(err.message)) {
|
|
194
|
+
try {
|
|
195
|
+
const checkoutResult = await gitOps.gitCheckout({ repo_path, branch: branch_name });
|
|
196
|
+
steps.push({ step: "checkout_existing_branch", ...checkoutResult });
|
|
197
|
+
} catch (checkoutErr) {
|
|
198
|
+
return fail("create_branch", checkoutErr.message);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
return fail("create_branch", err.message);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 3. Run checks (tests/lint/typecheck/build) — best-effort, non-fatal per check
|
|
206
|
+
const checks = {};
|
|
207
|
+
if (run_checks) {
|
|
208
|
+
for (const [name, fn] of Object.entries({
|
|
209
|
+
tests: projectOps.runTests,
|
|
210
|
+
lint: projectOps.runLint,
|
|
211
|
+
typecheck: projectOps.runTypecheck,
|
|
212
|
+
build: projectOps.runBuild,
|
|
213
|
+
})) {
|
|
214
|
+
const result = await fn({ repo_path });
|
|
215
|
+
checks[name] = result;
|
|
216
|
+
if (result.ran && result.exit_code !== 0) {
|
|
217
|
+
steps.push({ step: `check_${name}`, ...result });
|
|
218
|
+
return fail(`check_${name}`, `${name} failed (exit code ${result.exit_code}). Fix the issue and retry.`, { checks });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
steps.push({ step: "run_checks", checks });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 4. Stage + commit
|
|
225
|
+
try {
|
|
226
|
+
await gitOps.gitAdd({ repo_path, files: "." });
|
|
227
|
+
const commitResult = await gitOps.gitCommit({ repo_path, message: commit_message });
|
|
228
|
+
steps.push({ step: "commit", ...commitResult });
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return fail("commit", err.message);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 5. Push
|
|
234
|
+
try {
|
|
235
|
+
const pushResult = await gitOps.gitPush({ repo_path, branch: branch_name });
|
|
236
|
+
steps.push({ step: "push", ...pushResult });
|
|
237
|
+
} catch (err) {
|
|
238
|
+
return fail("push", err.message);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 6. Resolve owner/repo if not given, from the git remote
|
|
242
|
+
let resolvedOwner = owner;
|
|
243
|
+
let resolvedRepo = repo;
|
|
244
|
+
if (!resolvedOwner || !resolvedRepo) {
|
|
245
|
+
const remotes = await gitOps.gitRemote({ repo_path, action: "list" });
|
|
246
|
+
const origin = remotes.result?.find((r) => r.name === "origin");
|
|
247
|
+
const parsed = parseOwnerRepoFromRemoteUrl(origin?.refs?.fetch);
|
|
248
|
+
if (parsed) {
|
|
249
|
+
resolvedOwner = resolvedOwner || parsed.owner;
|
|
250
|
+
resolvedRepo = resolvedRepo || parsed.repo;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!resolvedOwner || !resolvedRepo) {
|
|
254
|
+
return fail("create_pull_request", "Could not resolve owner/repo — pass them explicitly.", { checks });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 7. Open PR
|
|
258
|
+
try {
|
|
259
|
+
const pr = await githubOps.githubCreatePullRequest({
|
|
260
|
+
owner: resolvedOwner,
|
|
261
|
+
repo: resolvedRepo,
|
|
262
|
+
title: pr_title || commit_message,
|
|
263
|
+
head: branch_name,
|
|
264
|
+
base,
|
|
265
|
+
body: pr_body,
|
|
266
|
+
});
|
|
267
|
+
steps.push({ step: "create_pull_request", ...pr });
|
|
268
|
+
return { ok: true, pull_request: pr, checks, steps };
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return fail("create_pull_request", err.message, { checks });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* deploy_project — safely deploy a project and verify it actually works.
|
|
276
|
+
*
|
|
277
|
+
* Flow:
|
|
278
|
+
* project health -> tests -> build -> trigger deployment -> poll until
|
|
279
|
+
* ready -> if ready, HTTP health check the live URL -> verified result
|
|
280
|
+
*
|
|
281
|
+
* Stops early (without deploying) if health/tests/build fail, so broken
|
|
282
|
+
* code never reaches a deployment.
|
|
283
|
+
*/
|
|
284
|
+
export async function deployProject({
|
|
285
|
+
repo_path,
|
|
286
|
+
project,
|
|
287
|
+
git_source_repo,
|
|
288
|
+
git_source_ref = "main",
|
|
289
|
+
deployment_name,
|
|
290
|
+
health_check_url,
|
|
291
|
+
run_checks = true,
|
|
292
|
+
poll_interval_ms = 10_000,
|
|
293
|
+
max_polls = 30,
|
|
294
|
+
}) {
|
|
295
|
+
const steps = [];
|
|
296
|
+
const fail = (step, reason, extra = {}) => ({ ok: false, failed_step: step, reason, steps, ...extra });
|
|
297
|
+
|
|
298
|
+
// 1. Project health
|
|
299
|
+
const health = await projectOps.projectHealth({ repo_path });
|
|
300
|
+
steps.push({ step: "project_health", ...health });
|
|
301
|
+
if (health.git_status && health.git_status.is_clean === false) {
|
|
302
|
+
return fail("project_health", "Working tree has uncommitted changes — commit or stash before deploying.", { health });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 2. Tests + build (best-effort skip if project has no such script)
|
|
306
|
+
const checks = {};
|
|
307
|
+
if (run_checks) {
|
|
308
|
+
for (const [name, fn] of Object.entries({ tests: projectOps.runTests, build: projectOps.runBuild })) {
|
|
309
|
+
const result = await fn({ repo_path });
|
|
310
|
+
checks[name] = result;
|
|
311
|
+
if (result.ran && result.exit_code !== 0) {
|
|
312
|
+
steps.push({ step: `check_${name}`, ...result });
|
|
313
|
+
return fail(`check_${name}`, `${name} failed (exit code ${result.exit_code}) — deployment aborted.`, { checks });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
steps.push({ step: "run_checks", checks });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 3. Trigger deployment
|
|
320
|
+
let deployment;
|
|
321
|
+
try {
|
|
322
|
+
deployment = await vercelOps.vercelCreateDeployment({
|
|
323
|
+
name: deployment_name || project,
|
|
324
|
+
project,
|
|
325
|
+
git_source_repo,
|
|
326
|
+
git_source_ref,
|
|
327
|
+
});
|
|
328
|
+
steps.push({ step: "trigger_deployment", ...deployment });
|
|
329
|
+
} catch (err) {
|
|
330
|
+
return fail("trigger_deployment", err.message, { checks });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 4. Poll until ready or errored
|
|
334
|
+
let finalState = null;
|
|
335
|
+
for (let i = 0; i < max_polls; i++) {
|
|
336
|
+
await new Promise((resolve) => setTimeout(resolve, poll_interval_ms));
|
|
337
|
+
const events = await vercelOps.vercelGetDeploymentEvents({ deployment_id: deployment.uid });
|
|
338
|
+
finalState = events;
|
|
339
|
+
if (events.state === "READY" || events.state === "ERROR" || events.state === "CANCELED") break;
|
|
340
|
+
}
|
|
341
|
+
steps.push({ step: "poll_deployment", final_state: finalState?.state });
|
|
342
|
+
|
|
343
|
+
if (!finalState || finalState.state !== "READY") {
|
|
344
|
+
// Pull build logs to help diagnose the failure
|
|
345
|
+
let logs = null;
|
|
346
|
+
try {
|
|
347
|
+
logs = await vercelOps.vercelGetDeploymentLogs({ deployment_id: deployment.uid, limit: 200 });
|
|
348
|
+
} catch {
|
|
349
|
+
// best-effort — logs are a bonus, not required for the failure report
|
|
350
|
+
}
|
|
351
|
+
return fail("poll_deployment", `Deployment did not become ready (state: ${finalState?.state || "unknown"}).`, {
|
|
352
|
+
checks,
|
|
353
|
+
deployment,
|
|
354
|
+
build_errors: finalState?.build_errors,
|
|
355
|
+
logs,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 5. HTTP health check the live URL
|
|
360
|
+
const url = health_check_url || `https://${deployment.url}`;
|
|
361
|
+
const httpResult = await vercelOps.httpCheck({ url });
|
|
362
|
+
steps.push({ step: "http_check", ...httpResult });
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
ok: httpResult.healthy,
|
|
366
|
+
deployment,
|
|
367
|
+
checks,
|
|
368
|
+
http_check: httpResult,
|
|
369
|
+
steps,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Workspace jail: optionally restrict file/path arguments to approved folders.
|
|
2
|
+
//
|
|
3
|
+
// Off by default. Turn on by setting CAUSLY_WORKSPACE to one or more folders,
|
|
4
|
+
// separated by ";" on Windows or ":" on macOS/Linux, e.g.
|
|
5
|
+
// CAUSLY_WORKSPACE=D:\causly-server;D:\causly-site
|
|
6
|
+
//
|
|
7
|
+
// This guards the path arguments of tool calls (files, directories, cwd,
|
|
8
|
+
// repo_path, terraform dir, ...). It is a guardrail, not a sandbox: shell
|
|
9
|
+
// command strings and docker volume mounts are not inspected.
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
|
|
13
|
+
// Tool input fields that hold a filesystem path (or an array of paths).
|
|
14
|
+
export const PATH_FIELDS = [
|
|
15
|
+
"path",
|
|
16
|
+
"paths",
|
|
17
|
+
"root_path",
|
|
18
|
+
"cwd",
|
|
19
|
+
"repo_path",
|
|
20
|
+
"dir",
|
|
21
|
+
"project_dir",
|
|
22
|
+
"context_dir",
|
|
23
|
+
"dockerfile",
|
|
24
|
+
"source",
|
|
25
|
+
"destination",
|
|
26
|
+
"store_path",
|
|
27
|
+
"plan_file",
|
|
28
|
+
"var_file",
|
|
29
|
+
"transcript_file",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Resolve symlinks in the deepest existing part of a path, then re-attach the
|
|
33
|
+
// parts that don't exist yet (so new files are checked correctly and a symlink
|
|
34
|
+
// inside the workspace can't point outside it).
|
|
35
|
+
function realish(p) {
|
|
36
|
+
let current = path.resolve(p);
|
|
37
|
+
const missing = [];
|
|
38
|
+
for (;;) {
|
|
39
|
+
try {
|
|
40
|
+
const real = fs.realpathSync.native(current);
|
|
41
|
+
return missing.length ? path.join(real, ...missing.reverse()) : real;
|
|
42
|
+
} catch {
|
|
43
|
+
const parent = path.dirname(current);
|
|
44
|
+
if (parent === current) return path.resolve(p);
|
|
45
|
+
missing.push(path.basename(current));
|
|
46
|
+
current = parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const normalize = (p) => (process.platform === "win32" ? p.toLowerCase() : p);
|
|
52
|
+
|
|
53
|
+
export function isInsideRoot(root, target) {
|
|
54
|
+
const rel = path.relative(normalize(realish(root)), normalize(realish(target)));
|
|
55
|
+
if (rel === "") return true;
|
|
56
|
+
if (path.isAbsolute(rel)) return false; // e.g. a different drive
|
|
57
|
+
return rel !== ".." && !rel.startsWith(".." + path.sep);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function workspaceRoots(value = process.env.CAUSLY_WORKSPACE) {
|
|
61
|
+
return (value || "")
|
|
62
|
+
.split(path.delimiter)
|
|
63
|
+
.map((s) => s.trim())
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.map((s) => path.resolve(s));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Returns { allowed: true } or { allowed: false, reason }. */
|
|
69
|
+
export function checkWorkspace(toolName, input, value = process.env.CAUSLY_WORKSPACE) {
|
|
70
|
+
const roots = workspaceRoots(value);
|
|
71
|
+
if (!roots.length) return { allowed: true };
|
|
72
|
+
for (const field of PATH_FIELDS) {
|
|
73
|
+
const raw = input?.[field];
|
|
74
|
+
for (const v of Array.isArray(raw) ? raw : [raw]) {
|
|
75
|
+
if (typeof v !== "string" || !v) continue;
|
|
76
|
+
if (!roots.some((root) => isInsideRoot(root, v))) {
|
|
77
|
+
return {
|
|
78
|
+
allowed: false,
|
|
79
|
+
reason: `'${toolName}' ${field} "${v}" is outside the allowed workspace (CAUSLY_WORKSPACE).`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { allowed: true };
|
|
85
|
+
}
|