co-maintainer 0.4.0-beta.1
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/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +9 -0
- package/dist/mod.d.ts +4 -0
- package/dist/mod.js +2 -0
- package/dist/package.json +54 -0
- package/dist/src/ai/batch.d.ts +16 -0
- package/dist/src/ai/batch.js +117 -0
- package/dist/src/ai/fake.d.ts +7 -0
- package/dist/src/ai/fake.js +40 -0
- package/dist/src/ai/hetzner.d.ts +16 -0
- package/dist/src/ai/hetzner.js +69 -0
- package/dist/src/ai/mermaid.d.ts +27 -0
- package/dist/src/ai/mermaid.js +457 -0
- package/dist/src/ai/mermaid_loop.d.ts +12 -0
- package/dist/src/ai/mermaid_loop.js +100 -0
- package/dist/src/ai/openrouter.d.ts +9 -0
- package/dist/src/ai/openrouter.js +29 -0
- package/dist/src/ai/provider.d.ts +4 -0
- package/dist/src/ai/provider.js +63 -0
- package/dist/src/cli/args.d.ts +5 -0
- package/dist/src/cli/args.js +252 -0
- package/dist/src/cli/commands/probe.d.ts +2 -0
- package/dist/src/cli/commands/probe.js +122 -0
- package/dist/src/cli/commands/review.d.ts +3 -0
- package/dist/src/cli/commands/review.js +84 -0
- package/dist/src/cli/commands/serve.d.ts +18 -0
- package/dist/src/cli/commands/serve.js +183 -0
- package/dist/src/cli/commands/set.d.ts +7 -0
- package/dist/src/cli/commands/set.js +171 -0
- package/dist/src/cli/main.d.ts +1 -0
- package/dist/src/cli/main.js +27 -0
- package/dist/src/cli/prompt.d.ts +4 -0
- package/dist/src/cli/prompt.js +26 -0
- package/dist/src/cli/review_args.d.ts +28 -0
- package/dist/src/cli/review_args.js +84 -0
- package/dist/src/cli/review_output.d.ts +2 -0
- package/dist/src/cli/review_output.js +10 -0
- package/dist/src/cli/review_result.d.ts +81 -0
- package/dist/src/cli/review_result.js +281 -0
- package/dist/src/config.d.ts +108 -0
- package/dist/src/config.js +138 -0
- package/dist/src/git/default_branch.d.ts +5 -0
- package/dist/src/git/default_branch.js +22 -0
- package/dist/src/github/app.d.ts +43 -0
- package/dist/src/github/app.js +120 -0
- package/dist/src/github/client.d.ts +17 -0
- package/dist/src/github/client.js +66 -0
- package/dist/src/github/collect.d.ts +7 -0
- package/dist/src/github/collect.js +368 -0
- package/dist/src/github/gh.d.ts +8 -0
- package/dist/src/github/gh.js +63 -0
- package/dist/src/github/jwt.d.ts +12 -0
- package/dist/src/github/jwt.js +84 -0
- package/dist/src/github/oauth.d.ts +9 -0
- package/dist/src/github/oauth.js +51 -0
- package/dist/src/github/pat.d.ts +10 -0
- package/dist/src/github/pat.js +60 -0
- package/dist/src/knowledge/facts.d.ts +3 -0
- package/dist/src/knowledge/facts.js +325 -0
- package/dist/src/knowledge/guide.d.ts +6 -0
- package/dist/src/knowledge/guide.js +82 -0
- package/dist/src/knowledge/probe.d.ts +12 -0
- package/dist/src/knowledge/probe.js +144 -0
- package/dist/src/knowledge/sections.d.ts +2 -0
- package/dist/src/knowledge/sections.js +13 -0
- package/dist/src/knowledge/skill.d.ts +10 -0
- package/dist/src/knowledge/skill.js +106 -0
- package/dist/src/knowledge/synthesis.d.ts +7 -0
- package/dist/src/knowledge/synthesis.js +315 -0
- package/dist/src/knowledge/types.d.ts +49 -0
- package/dist/src/knowledge/types.js +1 -0
- package/dist/src/knowledge/validate.d.ts +6 -0
- package/dist/src/knowledge/validate.js +134 -0
- package/dist/src/local/carry_over_store.d.ts +18 -0
- package/dist/src/local/carry_over_store.js +40 -0
- package/dist/src/local/codegraph_exclude.d.ts +3 -0
- package/dist/src/local/codegraph_exclude.js +26 -0
- package/dist/src/local/codegraph_prepare.d.ts +17 -0
- package/dist/src/local/codegraph_prepare.js +51 -0
- package/dist/src/local/git_ops.d.ts +12 -0
- package/dist/src/local/git_ops.js +98 -0
- package/dist/src/local/git_parse.d.ts +27 -0
- package/dist/src/local/git_parse.js +131 -0
- package/dist/src/local/git_revision.d.ts +15 -0
- package/dist/src/local/git_revision.js +141 -0
- package/dist/src/local/git_untracked.d.ts +21 -0
- package/dist/src/local/git_untracked.js +79 -0
- package/dist/src/local/review_local.d.ts +4 -0
- package/dist/src/local/review_local.js +273 -0
- package/dist/src/local/review_lock.d.ts +7 -0
- package/dist/src/local/review_lock.js +45 -0
- package/dist/src/pr/checkout.d.ts +25 -0
- package/dist/src/pr/checkout.js +136 -0
- package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
- package/dist/src/pr/codegraph_tool_args.js +34 -0
- package/dist/src/pr/codegraph_tools.d.ts +15 -0
- package/dist/src/pr/codegraph_tools.js +408 -0
- package/dist/src/pr/diff_summary.d.ts +6 -0
- package/dist/src/pr/diff_summary.js +52 -0
- package/dist/src/pr/findings.d.ts +14 -0
- package/dist/src/pr/findings.js +94 -0
- package/dist/src/pr/github_revision.d.ts +4 -0
- package/dist/src/pr/github_revision.js +39 -0
- package/dist/src/pr/hunks.d.ts +18 -0
- package/dist/src/pr/hunks.js +73 -0
- package/dist/src/pr/reviewer.d.ts +40 -0
- package/dist/src/pr/reviewer.js +582 -0
- package/dist/src/pr/rounds.d.ts +17 -0
- package/dist/src/pr/rounds.js +43 -0
- package/dist/src/pr/scope.d.ts +35 -0
- package/dist/src/pr/scope.js +84 -0
- package/dist/src/pr/snapshot.d.ts +11 -0
- package/dist/src/pr/snapshot.js +1 -0
- package/dist/src/pr/suggestion.d.ts +17 -0
- package/dist/src/pr/suggestion.js +70 -0
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +199 -0
- package/dist/src/remote/client_tools.d.ts +13 -0
- package/dist/src/remote/client_tools.js +30 -0
- package/dist/src/remote/fixtures.d.ts +1 -0
- package/dist/src/remote/fixtures.js +30 -0
- package/dist/src/remote/revision_from_submit.d.ts +2 -0
- package/dist/src/remote/revision_from_submit.js +26 -0
- package/dist/src/remote/schema.d.ts +14 -0
- package/dist/src/remote/schema.js +30 -0
- package/dist/src/remote/server/body.d.ts +18 -0
- package/dist/src/remote/server/body.js +73 -0
- package/dist/src/remote/server/routes.d.ts +1 -0
- package/dist/src/remote/server/routes.js +150 -0
- package/dist/src/remote/server/sessions.d.ts +16 -0
- package/dist/src/remote/server/sessions.js +48 -0
- package/dist/src/remote/server/sync.d.ts +3 -0
- package/dist/src/remote/server/sync.js +90 -0
- package/dist/src/remote/tool_bridge.d.ts +15 -0
- package/dist/src/remote/tool_bridge.js +84 -0
- package/dist/src/remote/validate.d.ts +8 -0
- package/dist/src/remote/validate.js +248 -0
- package/dist/src/review/carry_over.d.ts +63 -0
- package/dist/src/review/carry_over.js +372 -0
- package/dist/src/review/engine.d.ts +13 -0
- package/dist/src/review/engine.js +4 -0
- package/dist/src/review/guides.d.ts +12 -0
- package/dist/src/review/guides.js +44 -0
- package/dist/src/review/revision.d.ts +26 -0
- package/dist/src/review/revision.js +57 -0
- package/dist/src/server/api/activity.d.ts +1 -0
- package/dist/src/server/api/activity.js +12 -0
- package/dist/src/server/api/analytics.d.ts +1 -0
- package/dist/src/server/api/analytics.js +9 -0
- package/dist/src/server/api/installations.d.ts +4 -0
- package/dist/src/server/api/installations.js +24 -0
- package/dist/src/server/api/jobs.d.ts +1 -0
- package/dist/src/server/api/jobs.js +86 -0
- package/dist/src/server/api/json_body.d.ts +1 -0
- package/dist/src/server/api/json_body.js +14 -0
- package/dist/src/server/api/remote_tokens.d.ts +1 -0
- package/dist/src/server/api/remote_tokens.js +71 -0
- package/dist/src/server/api/repos.d.ts +4 -0
- package/dist/src/server/api/repos.js +136 -0
- package/dist/src/server/api/settings.d.ts +1 -0
- package/dist/src/server/api/settings.js +210 -0
- package/dist/src/server/app.d.ts +27 -0
- package/dist/src/server/app.js +147 -0
- package/dist/src/server/assets.d.ts +3 -0
- package/dist/src/server/assets.js +21 -0
- package/dist/src/server/auth.d.ts +44 -0
- package/dist/src/server/auth.js +188 -0
- package/dist/src/server/errors.d.ts +8 -0
- package/dist/src/server/errors.js +10 -0
- package/dist/src/server/http.d.ts +8 -0
- package/dist/src/server/http.js +117 -0
- package/dist/src/server/logo.d.ts +1 -0
- package/dist/src/server/logo.js +2 -0
- package/dist/src/server/pages/activity.d.ts +4 -0
- package/dist/src/server/pages/activity.js +200 -0
- package/dist/src/server/pages/add_repo.d.ts +9 -0
- package/dist/src/server/pages/add_repo.js +56 -0
- package/dist/src/server/pages/analytics.d.ts +2 -0
- package/dist/src/server/pages/analytics.js +193 -0
- package/dist/src/server/pages/client.d.ts +2 -0
- package/dist/src/server/pages/client.js +2 -0
- package/dist/src/server/pages/home.d.ts +16 -0
- package/dist/src/server/pages/home.js +87 -0
- package/dist/src/server/pages/knowledge.d.ts +2 -0
- package/dist/src/server/pages/knowledge.js +64 -0
- package/dist/src/server/pages/layout.d.ts +32 -0
- package/dist/src/server/pages/layout.js +230 -0
- package/dist/src/server/pages/pr.d.ts +2 -0
- package/dist/src/server/pages/pr.js +109 -0
- package/dist/src/server/pages/repo.d.ts +2 -0
- package/dist/src/server/pages/repo.js +92 -0
- package/dist/src/server/pages/repo_prs.d.ts +2 -0
- package/dist/src/server/pages/repo_prs.js +75 -0
- package/dist/src/server/pages/repo_remote.d.ts +2 -0
- package/dist/src/server/pages/repo_remote.js +38 -0
- package/dist/src/server/pages/repo_settings.d.ts +3 -0
- package/dist/src/server/pages/repo_settings.js +127 -0
- package/dist/src/server/pages/router.d.ts +17 -0
- package/dist/src/server/pages/router.js +312 -0
- package/dist/src/server/pages/settings.d.ts +2 -0
- package/dist/src/server/pages/settings.js +337 -0
- package/dist/src/server/pages/setup.d.ts +5 -0
- package/dist/src/server/pages/setup.js +38 -0
- package/dist/src/server/pages/styles.d.ts +2 -0
- package/dist/src/server/pages/styles.js +2 -0
- package/dist/src/server/webhook/index.d.ts +1 -0
- package/dist/src/server/webhook/index.js +64 -0
- package/dist/src/server/webhook/signature.d.ts +7 -0
- package/dist/src/server/webhook/signature.js +41 -0
- package/dist/src/services/credentials.d.ts +19 -0
- package/dist/src/services/credentials.js +146 -0
- package/dist/src/services/dashboard.d.ts +165 -0
- package/dist/src/services/dashboard.js +200 -0
- package/dist/src/services/drift.d.ts +13 -0
- package/dist/src/services/drift.js +76 -0
- package/dist/src/services/jobs.d.ts +34 -0
- package/dist/src/services/jobs.js +222 -0
- package/dist/src/services/remote_review.d.ts +10 -0
- package/dist/src/services/remote_review.js +274 -0
- package/dist/src/services/remote_review_abort.d.ts +3 -0
- package/dist/src/services/remote_review_abort.js +11 -0
- package/dist/src/services/remote_token_jobs.d.ts +1 -0
- package/dist/src/services/remote_token_jobs.js +20 -0
- package/dist/src/services/remote_tokens.d.ts +13 -0
- package/dist/src/services/remote_tokens.js +57 -0
- package/dist/src/services/replies.d.ts +7 -0
- package/dist/src/services/replies.js +248 -0
- package/dist/src/services/review.d.ts +35 -0
- package/dist/src/services/review.js +671 -0
- package/dist/src/services/setup.d.ts +29 -0
- package/dist/src/services/setup.js +364 -0
- package/dist/src/services/webhook.d.ts +22 -0
- package/dist/src/services/webhook.js +401 -0
- package/dist/src/store/app_db.d.ts +10 -0
- package/dist/src/store/app_db.js +85 -0
- package/dist/src/store/cache_db.d.ts +4 -0
- package/dist/src/store/cache_db.js +63 -0
- package/dist/src/store/deliveries.d.ts +12 -0
- package/dist/src/store/deliveries.js +23 -0
- package/dist/src/store/drift.d.ts +4 -0
- package/dist/src/store/drift.js +22 -0
- package/dist/src/store/findings.d.ts +31 -0
- package/dist/src/store/findings.js +64 -0
- package/dist/src/store/installations.d.ts +9 -0
- package/dist/src/store/installations.js +35 -0
- package/dist/src/store/job_logs.d.ts +5 -0
- package/dist/src/store/job_logs.js +17 -0
- package/dist/src/store/jobs.d.ts +27 -0
- package/dist/src/store/jobs.js +86 -0
- package/dist/src/store/migrations.d.ts +5 -0
- package/dist/src/store/migrations.js +258 -0
- package/dist/src/store/remote_review_inputs.d.ts +11 -0
- package/dist/src/store/remote_review_inputs.js +25 -0
- package/dist/src/store/remote_tokens.d.ts +10 -0
- package/dist/src/store/remote_tokens.js +45 -0
- package/dist/src/store/replies.d.ts +24 -0
- package/dist/src/store/replies.js +79 -0
- package/dist/src/store/repos.d.ts +11 -0
- package/dist/src/store/repos.js +59 -0
- package/dist/src/store/reviews.d.ts +86 -0
- package/dist/src/store/reviews.js +220 -0
- package/dist/src/store/rows.d.ts +179 -0
- package/dist/src/store/rows.js +4 -0
- package/dist/src/store/sessions.d.ts +8 -0
- package/dist/src/store/sessions.js +34 -0
- package/dist/src/store/skill_state.d.ts +3 -0
- package/dist/src/store/skill_state.js +24 -0
- package/dist/src/store/sqlite.d.ts +39 -0
- package/dist/src/store/sqlite.js +77 -0
- package/dist/src/store/subjects.d.ts +15 -0
- package/dist/src/store/subjects.js +78 -0
- package/dist/src/tools/codegraph.d.ts +58 -0
- package/dist/src/tools/codegraph.js +165 -0
- package/dist/src/tools/codegraph_exec.d.ts +13 -0
- package/dist/src/tools/codegraph_exec.js +66 -0
- package/dist/src/types.d.ts +89 -0
- package/dist/src/types.js +1 -0
- package/dist/src/util/atomic.d.ts +4 -0
- package/dist/src/util/atomic.js +78 -0
- package/dist/src/util/keyed_lock.d.ts +3 -0
- package/dist/src/util/keyed_lock.js +33 -0
- package/dist/src/util/log.d.ts +11 -0
- package/dist/src/util/log.js +42 -0
- package/dist/src/util/redact.d.ts +5 -0
- package/dist/src/util/redact.js +30 -0
- package/dist/src/util/runtime.d.ts +90 -0
- package/dist/src/util/runtime.js +206 -0
- package/dist/src/util/time.d.ts +9 -0
- package/dist/src/util/time.js +36 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +5 -0
- package/package.json +54 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { VERSION } from "../../version.js";
|
|
2
|
+
import { cloneDir, readConfig } from "../../config.js";
|
|
3
|
+
import { resolveDefaultBranchName } from "../../git/default_branch.js";
|
|
4
|
+
import { runCommand } from "../../pr/checkout.js";
|
|
5
|
+
import { loadGuides } from "../../review/guides.js";
|
|
6
|
+
import { isLockedOut, readBearerToken, recordAuthFailure, } from "../../server/auth.js";
|
|
7
|
+
import { errorResponse } from "../../server/errors.js";
|
|
8
|
+
import { lookupRemoteBearer } from "../../services/remote_tokens.js";
|
|
9
|
+
import { findRepoByFullName } from "../../store/repos.js";
|
|
10
|
+
import { MIN_CLIENT_SCHEMA, REMOTE_CLI_UPGRADE_COMMAND, REMOTE_MAX_BODY_BYTES, REMOTE_SCHEMA_VERSION, REMOTE_SYNC_INTERVAL_SECONDS, } from "../schema.js";
|
|
11
|
+
import { readBoundedJson } from "./body.js";
|
|
12
|
+
import { submitRemoteReview } from "../../services/remote_review.js";
|
|
13
|
+
import { handleRemoteCancel, handleRemoteSync } from "./sync.js";
|
|
14
|
+
import { validateHandshakeRequest, validateSubmitRequest, } from "../validate.js";
|
|
15
|
+
import { stat } from "../../util/runtime.js";
|
|
16
|
+
const REPO_UNAVAILABLE = "Sorry, we could not access this repository.";
|
|
17
|
+
async function authenticateRemote(request, ip) {
|
|
18
|
+
if (isLockedOut(ip)) {
|
|
19
|
+
return errorResponse(429, "locked_out", "too many failed attempts");
|
|
20
|
+
}
|
|
21
|
+
const bearer = readBearerToken(request);
|
|
22
|
+
if (!bearer) {
|
|
23
|
+
recordAuthFailure(ip);
|
|
24
|
+
return errorResponse(401, "token_invalid", "invalid or missing token");
|
|
25
|
+
}
|
|
26
|
+
const status = await lookupRemoteBearer(bearer);
|
|
27
|
+
if (status === "invalid") {
|
|
28
|
+
recordAuthFailure(ip);
|
|
29
|
+
return errorResponse(401, "token_invalid", "invalid or missing token");
|
|
30
|
+
}
|
|
31
|
+
if (status === "inactive") {
|
|
32
|
+
return errorResponse(403, "token_inactive", "token is inactive");
|
|
33
|
+
}
|
|
34
|
+
return status;
|
|
35
|
+
}
|
|
36
|
+
async function defaultBranchForRepo(fullName) {
|
|
37
|
+
try {
|
|
38
|
+
const dir = cloneDir(fullName);
|
|
39
|
+
await stat(dir);
|
|
40
|
+
const name = await resolveDefaultBranchName(dir, "origin", runCommand);
|
|
41
|
+
return name ?? null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function handleHandshake(request, ip) {
|
|
48
|
+
const parsed = await readBoundedJson(request, REMOTE_MAX_BODY_BYTES);
|
|
49
|
+
if (!parsed.ok) {
|
|
50
|
+
return errorResponse(parsed.status, parsed.code, parsed.message);
|
|
51
|
+
}
|
|
52
|
+
const validation = validateHandshakeRequest(parsed.value);
|
|
53
|
+
if (validation) {
|
|
54
|
+
return errorResponse(400, "bad_request", validation);
|
|
55
|
+
}
|
|
56
|
+
const record = parsed.value;
|
|
57
|
+
const schemaVersion = record.schemaVersion;
|
|
58
|
+
if (schemaVersion < MIN_CLIENT_SCHEMA) {
|
|
59
|
+
return errorResponse(426, "upgrade_required", "This co-maintainer CLI is too old for the server.", {
|
|
60
|
+
minClientSchema: MIN_CLIENT_SCHEMA,
|
|
61
|
+
serverSchema: REMOTE_SCHEMA_VERSION,
|
|
62
|
+
upgradeCommand: REMOTE_CLI_UPGRADE_COMMAND,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const auth = await authenticateRemote(request, ip);
|
|
66
|
+
if (auth instanceof Response)
|
|
67
|
+
return auth;
|
|
68
|
+
const repoRow = findRepoByFullName(String(record.repo));
|
|
69
|
+
if (!repoRow || repoRow.active !== 1) {
|
|
70
|
+
return errorResponse(404, "repo_unavailable", REPO_UNAVAILABLE);
|
|
71
|
+
}
|
|
72
|
+
const guides = await loadGuides(repoRow.full_name);
|
|
73
|
+
if (!guides.shortGuide && !guides.skill) {
|
|
74
|
+
return errorResponse(409, "not_initialized", REPO_UNAVAILABLE);
|
|
75
|
+
}
|
|
76
|
+
const config = readConfig();
|
|
77
|
+
const timeoutSeconds = config.remoteSyncTimeoutSeconds ?? 10;
|
|
78
|
+
const toolLimit = config.remoteToolOutputMaxChars ?? null;
|
|
79
|
+
return Response.json({
|
|
80
|
+
schemaVersion: REMOTE_SCHEMA_VERSION,
|
|
81
|
+
minClientSchema: MIN_CLIENT_SCHEMA,
|
|
82
|
+
serverVersion: VERSION,
|
|
83
|
+
token: { name: auth.name },
|
|
84
|
+
repo: {
|
|
85
|
+
fullName: repoRow.full_name,
|
|
86
|
+
defaultBranch: await defaultBranchForRepo(repoRow.full_name),
|
|
87
|
+
guideBuiltAt: guides.guideBuiltAt,
|
|
88
|
+
},
|
|
89
|
+
sync: {
|
|
90
|
+
intervalSeconds: REMOTE_SYNC_INTERVAL_SECONDS,
|
|
91
|
+
timeoutSeconds,
|
|
92
|
+
},
|
|
93
|
+
limits: {
|
|
94
|
+
maxBodyBytes: REMOTE_MAX_BODY_BYTES,
|
|
95
|
+
toolOutputMaxChars: toolLimit,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async function handleSync(request, ip, jobId) {
|
|
100
|
+
const auth = await authenticateRemote(request, ip);
|
|
101
|
+
if (auth instanceof Response)
|
|
102
|
+
return auth;
|
|
103
|
+
const parsed = await readBoundedJson(request, REMOTE_MAX_BODY_BYTES);
|
|
104
|
+
if (!parsed.ok) {
|
|
105
|
+
return errorResponse(parsed.status, parsed.code, parsed.message);
|
|
106
|
+
}
|
|
107
|
+
return handleRemoteSync(jobId, auth, parsed.value);
|
|
108
|
+
}
|
|
109
|
+
async function handleSubmit(request, ip) {
|
|
110
|
+
const auth = await authenticateRemote(request, ip);
|
|
111
|
+
if (auth instanceof Response)
|
|
112
|
+
return auth;
|
|
113
|
+
const parsed = await readBoundedJson(request, REMOTE_MAX_BODY_BYTES);
|
|
114
|
+
if (!parsed.ok) {
|
|
115
|
+
return errorResponse(parsed.status, parsed.code, parsed.message);
|
|
116
|
+
}
|
|
117
|
+
const validation = validateSubmitRequest(parsed.value);
|
|
118
|
+
if (validation) {
|
|
119
|
+
return errorResponse(400, "bad_request", validation);
|
|
120
|
+
}
|
|
121
|
+
const result = submitRemoteReview(auth, parsed.value);
|
|
122
|
+
if ("error" in result) {
|
|
123
|
+
return errorResponse(result.status, result.code, result.error);
|
|
124
|
+
}
|
|
125
|
+
return Response.json({
|
|
126
|
+
schemaVersion: REMOTE_SCHEMA_VERSION,
|
|
127
|
+
jobId: result.jobId,
|
|
128
|
+
reviewId: result.reviewId,
|
|
129
|
+
}, { status: 202 });
|
|
130
|
+
}
|
|
131
|
+
export async function handleRemoteRoute(request, url, remoteAddr) {
|
|
132
|
+
if (url.pathname === "/api/remote/handshake" && request.method === "POST") {
|
|
133
|
+
return await handleHandshake(request, remoteAddr);
|
|
134
|
+
}
|
|
135
|
+
if (url.pathname === "/api/remote/reviews" && request.method === "POST") {
|
|
136
|
+
return await handleSubmit(request, remoteAddr);
|
|
137
|
+
}
|
|
138
|
+
const syncMatch = /^\/api\/remote\/reviews\/([^/]+)\/sync$/.exec(url.pathname);
|
|
139
|
+
if (syncMatch && request.method === "POST") {
|
|
140
|
+
return await handleSync(request, remoteAddr, syncMatch[1]);
|
|
141
|
+
}
|
|
142
|
+
const reviewMatch = /^\/api\/remote\/reviews\/([^/]+)$/.exec(url.pathname);
|
|
143
|
+
if (reviewMatch && request.method === "DELETE") {
|
|
144
|
+
const auth = await authenticateRemote(request, remoteAddr);
|
|
145
|
+
if (auth instanceof Response)
|
|
146
|
+
return auth;
|
|
147
|
+
return handleRemoteCancel(reviewMatch[1], auth);
|
|
148
|
+
}
|
|
149
|
+
return errorResponse(404, "not_found", `no route for ${url.pathname}`);
|
|
150
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type RemoteSession = {
|
|
2
|
+
jobId: string;
|
|
3
|
+
reviewId: string;
|
|
4
|
+
tokenId: string;
|
|
5
|
+
subjectId: string;
|
|
6
|
+
lastSyncAt: number;
|
|
7
|
+
};
|
|
8
|
+
export type RemoteSyncResult = Record<string, unknown>;
|
|
9
|
+
export declare function openRemoteSession(session: Omit<RemoteSession, "lastSyncAt">): void;
|
|
10
|
+
export declare function touchRemoteSession(jobId: string): void;
|
|
11
|
+
export declare function getRemoteSession(jobId: string): RemoteSession | undefined;
|
|
12
|
+
export declare function closeRemoteSession(jobId: string): void;
|
|
13
|
+
export declare function setRemoteSyncResult(jobId: string, result: RemoteSyncResult): void;
|
|
14
|
+
export declare function getRemoteSyncResult(jobId: string): RemoteSyncResult | undefined;
|
|
15
|
+
export declare function startRemoteWatchdog(): void;
|
|
16
|
+
export declare function stopRemoteWatchdog(): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readConfig } from "../../config.js";
|
|
2
|
+
import { cancel } from "../../services/jobs.js";
|
|
3
|
+
import { clearRemoteToolBridge } from "../tool_bridge.js";
|
|
4
|
+
const sessions = new Map();
|
|
5
|
+
const finishedResults = new Map();
|
|
6
|
+
let watchdogTimer;
|
|
7
|
+
export function openRemoteSession(session) {
|
|
8
|
+
sessions.set(session.jobId, { ...session, lastSyncAt: Date.now() });
|
|
9
|
+
}
|
|
10
|
+
export function touchRemoteSession(jobId) {
|
|
11
|
+
const session = sessions.get(jobId);
|
|
12
|
+
if (session)
|
|
13
|
+
session.lastSyncAt = Date.now();
|
|
14
|
+
}
|
|
15
|
+
export function getRemoteSession(jobId) {
|
|
16
|
+
return sessions.get(jobId);
|
|
17
|
+
}
|
|
18
|
+
export function closeRemoteSession(jobId) {
|
|
19
|
+
sessions.delete(jobId);
|
|
20
|
+
clearRemoteToolBridge(jobId);
|
|
21
|
+
setTimeout(() => finishedResults.delete(jobId), 60_000);
|
|
22
|
+
}
|
|
23
|
+
export function setRemoteSyncResult(jobId, result) {
|
|
24
|
+
finishedResults.set(jobId, result);
|
|
25
|
+
}
|
|
26
|
+
export function getRemoteSyncResult(jobId) {
|
|
27
|
+
return finishedResults.get(jobId);
|
|
28
|
+
}
|
|
29
|
+
export function startRemoteWatchdog() {
|
|
30
|
+
if (watchdogTimer !== undefined)
|
|
31
|
+
return;
|
|
32
|
+
watchdogTimer = setInterval(() => {
|
|
33
|
+
const timeoutMs = (readConfig().remoteSyncTimeoutSeconds ?? 10) * 1000;
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
for (const [jobId, session] of sessions) {
|
|
36
|
+
if (now - session.lastSyncAt > timeoutMs) {
|
|
37
|
+
cancel(jobId, "client_timeout");
|
|
38
|
+
closeRemoteSession(jobId);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}, 1000);
|
|
42
|
+
}
|
|
43
|
+
export function stopRemoteWatchdog() {
|
|
44
|
+
if (watchdogTimer !== undefined) {
|
|
45
|
+
clearInterval(watchdogTimer);
|
|
46
|
+
watchdogTimer = undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { cancel, getLogsSince } from "../../services/jobs.js";
|
|
2
|
+
import { errorResponse } from "../../server/errors.js";
|
|
3
|
+
import { redact } from "../../util/redact.js";
|
|
4
|
+
import { getJob } from "../../store/jobs.js";
|
|
5
|
+
import { getReviewByJobId } from "../../store/reviews.js";
|
|
6
|
+
import { REMOTE_SCHEMA_VERSION } from "../schema.js";
|
|
7
|
+
import { validateSyncRequest, validateSyncResponseStatus, } from "../validate.js";
|
|
8
|
+
import { getRemoteSyncResult, touchRemoteSession, } from "./sessions.js";
|
|
9
|
+
import { applyToolResults, drainPendingToolCalls } from "../tool_bridge.js";
|
|
10
|
+
function jobToSyncStatus(jobStatus) {
|
|
11
|
+
switch (jobStatus) {
|
|
12
|
+
case "queued":
|
|
13
|
+
return "queued";
|
|
14
|
+
case "running":
|
|
15
|
+
return "running";
|
|
16
|
+
case "done":
|
|
17
|
+
return "done";
|
|
18
|
+
case "failed":
|
|
19
|
+
return "failed";
|
|
20
|
+
case "canceled":
|
|
21
|
+
return "canceled";
|
|
22
|
+
default:
|
|
23
|
+
return "failed";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function handleRemoteSync(jobId, token, body) {
|
|
27
|
+
const validation = validateSyncRequest(body);
|
|
28
|
+
if (validation) {
|
|
29
|
+
return errorResponse(400, "bad_request", validation);
|
|
30
|
+
}
|
|
31
|
+
const job = getJob(jobId);
|
|
32
|
+
const review = getReviewByJobId(jobId);
|
|
33
|
+
if (!job || !review || review.token_id !== token.id) {
|
|
34
|
+
return errorResponse(404, "not_found", "review not found");
|
|
35
|
+
}
|
|
36
|
+
touchRemoteSession(jobId);
|
|
37
|
+
const toolResults = body.toolResults;
|
|
38
|
+
if (Array.isArray(toolResults)) {
|
|
39
|
+
applyToolResults(jobId, toolResults);
|
|
40
|
+
}
|
|
41
|
+
const afterLogSeq = Number(body.afterLogSeq);
|
|
42
|
+
const logs = getLogsSince(jobId, afterLogSeq).map((row) => ({
|
|
43
|
+
seq: row.seq,
|
|
44
|
+
at: row.at,
|
|
45
|
+
level: row.level,
|
|
46
|
+
message: row.message,
|
|
47
|
+
}));
|
|
48
|
+
const status = jobToSyncStatus(job.status);
|
|
49
|
+
const statusErr = validateSyncResponseStatus(status);
|
|
50
|
+
if (statusErr) {
|
|
51
|
+
return errorResponse(500, "internal", statusErr);
|
|
52
|
+
}
|
|
53
|
+
let abort = null;
|
|
54
|
+
let result = null;
|
|
55
|
+
if (status === "done") {
|
|
56
|
+
result = getRemoteSyncResult(jobId) ?? null;
|
|
57
|
+
}
|
|
58
|
+
else if (status === "canceled") {
|
|
59
|
+
const reason = job.cancel_reason ?? "canceled";
|
|
60
|
+
abort = {
|
|
61
|
+
reason,
|
|
62
|
+
message: reason === "client_timeout"
|
|
63
|
+
? "No sync received for the configured timeout."
|
|
64
|
+
: "Review was canceled.",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
else if (status === "failed") {
|
|
68
|
+
abort = {
|
|
69
|
+
reason: "failed",
|
|
70
|
+
message: redact(job.error ?? "Review failed."),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const toolCalls = drainPendingToolCalls(jobId);
|
|
74
|
+
return Response.json({
|
|
75
|
+
schemaVersion: REMOTE_SCHEMA_VERSION,
|
|
76
|
+
status,
|
|
77
|
+
toolCalls,
|
|
78
|
+
logs,
|
|
79
|
+
result,
|
|
80
|
+
abort,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export function handleRemoteCancel(jobId, token) {
|
|
84
|
+
const review = getReviewByJobId(jobId);
|
|
85
|
+
if (!review || review.token_id !== token.id) {
|
|
86
|
+
return errorResponse(404, "not_found", "review not found");
|
|
87
|
+
}
|
|
88
|
+
cancel(jobId, "client_canceled");
|
|
89
|
+
return new Response(null, { status: 204 });
|
|
90
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ToolHandler } from "../ai/mermaid_loop.ts";
|
|
2
|
+
export type PendingToolCall = {
|
|
3
|
+
callId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
arguments: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
export declare function clearRemoteToolBridge(jobId: string): void;
|
|
8
|
+
export declare function drainPendingToolCalls(jobId: string): PendingToolCall[];
|
|
9
|
+
export declare function applyToolResults(jobId: string, results: Array<{
|
|
10
|
+
callId: string;
|
|
11
|
+
output?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}>): void;
|
|
14
|
+
/** Server-side handlers: same schemas as local tools, execution on the CLI. */
|
|
15
|
+
export declare function remoteBridgeToolHandlers(jobId: string, signal: AbortSignal, allowedNames: ReadonlySet<string>, sourceHandlers: ToolHandler[]): ToolHandler[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readConfig } from "../config.js";
|
|
2
|
+
import { REMOTE_KNOWN_TOOL_NAMES } from "./schema.js";
|
|
3
|
+
const pendingByJob = new Map();
|
|
4
|
+
const queuedCallsByJob = new Map();
|
|
5
|
+
export function clearRemoteToolBridge(jobId) {
|
|
6
|
+
const pending = pendingByJob.get(jobId);
|
|
7
|
+
if (pending) {
|
|
8
|
+
for (const [, entry] of pending) {
|
|
9
|
+
entry.reject(new Error("review ended"));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
pendingByJob.delete(jobId);
|
|
13
|
+
queuedCallsByJob.delete(jobId);
|
|
14
|
+
}
|
|
15
|
+
export function drainPendingToolCalls(jobId) {
|
|
16
|
+
const queue = queuedCallsByJob.get(jobId) ?? [];
|
|
17
|
+
queuedCallsByJob.set(jobId, []);
|
|
18
|
+
return queue;
|
|
19
|
+
}
|
|
20
|
+
export function applyToolResults(jobId, results) {
|
|
21
|
+
const pending = pendingByJob.get(jobId);
|
|
22
|
+
if (!pending)
|
|
23
|
+
return;
|
|
24
|
+
const maxChars = readConfig().remoteToolOutputMaxChars ?? 500_000;
|
|
25
|
+
for (const row of results) {
|
|
26
|
+
const entry = pending.get(row.callId);
|
|
27
|
+
if (!entry)
|
|
28
|
+
continue;
|
|
29
|
+
pending.delete(row.callId);
|
|
30
|
+
if (row.error !== undefined) {
|
|
31
|
+
entry.reject(new Error(row.error));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const out = row.output ?? "";
|
|
35
|
+
entry.resolve(out.length > maxChars ? `${out.slice(0, maxChars)}\n...[truncated]` : out);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function queueToolCall(jobId, signal, name, args) {
|
|
39
|
+
if (signal.aborted)
|
|
40
|
+
throw new Error("aborted");
|
|
41
|
+
const callId = crypto.randomUUID();
|
|
42
|
+
const argsObj = typeof args === "object" && args !== null && !Array.isArray(args)
|
|
43
|
+
? args
|
|
44
|
+
: {};
|
|
45
|
+
const call = { callId, name, arguments: argsObj };
|
|
46
|
+
let queue = queuedCallsByJob.get(jobId);
|
|
47
|
+
if (!queue) {
|
|
48
|
+
queue = [];
|
|
49
|
+
queuedCallsByJob.set(jobId, queue);
|
|
50
|
+
}
|
|
51
|
+
queue.push(call);
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
let pending = pendingByJob.get(jobId);
|
|
54
|
+
if (!pending) {
|
|
55
|
+
pending = new Map();
|
|
56
|
+
pendingByJob.set(jobId, pending);
|
|
57
|
+
}
|
|
58
|
+
pending.set(callId, { resolve, reject });
|
|
59
|
+
const onAbort = () => {
|
|
60
|
+
pending.delete(callId);
|
|
61
|
+
reject(new Error("aborted"));
|
|
62
|
+
};
|
|
63
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** Server-side handlers: same schemas as local tools, execution on the CLI. */
|
|
67
|
+
export function remoteBridgeToolHandlers(jobId, signal, allowedNames, sourceHandlers) {
|
|
68
|
+
const byName = new Map(sourceHandlers.map((handler) => [handler.name, handler]));
|
|
69
|
+
const handlers = [];
|
|
70
|
+
for (const name of allowedNames) {
|
|
71
|
+
if (name === "read-full-diff" || !REMOTE_KNOWN_TOOL_NAMES.has(name)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const src = byName.get(name);
|
|
75
|
+
if (!src)
|
|
76
|
+
continue;
|
|
77
|
+
handlers.push({
|
|
78
|
+
name: src.name,
|
|
79
|
+
tool: src.tool,
|
|
80
|
+
run: (args) => queueToolCall(jobId, signal, src.name, args),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return handlers;
|
|
84
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function parseSchemaVersion(body: Record<string, unknown>): number | string;
|
|
2
|
+
export declare function assertClientSchema(version: number): string | null;
|
|
3
|
+
export declare function validateRemotePath(field: string, path: string): string | null;
|
|
4
|
+
export declare function validateRevisionPayload(revision: unknown): string | null;
|
|
5
|
+
export declare function validateHandshakeRequest(body: unknown): string | null;
|
|
6
|
+
export declare function validateSubmitRequest(body: unknown): string | null;
|
|
7
|
+
export declare function validateSyncRequest(body: unknown): string | null;
|
|
8
|
+
export declare function validateSyncResponseStatus(status: unknown): string | null;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { rejectEscapingPath } from "../pr/codegraph_tool_args.js";
|
|
2
|
+
import { MIN_CLIENT_SCHEMA, REMOTE_KNOWN_TOOL_NAMES, REMOTE_SCHEMA_VERSION, REMOTE_SYNC_STATUSES, } from "./schema.js";
|
|
3
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4
|
+
const REVISION_STATUSES = new Set([
|
|
5
|
+
"added",
|
|
6
|
+
"modified",
|
|
7
|
+
"removed",
|
|
8
|
+
"renamed",
|
|
9
|
+
]);
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function bad(field, reason) {
|
|
14
|
+
return `${field}: ${reason}`;
|
|
15
|
+
}
|
|
16
|
+
export function parseSchemaVersion(body) {
|
|
17
|
+
const v = body.schemaVersion;
|
|
18
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 1) {
|
|
19
|
+
return bad("schemaVersion", "must be a positive integer");
|
|
20
|
+
}
|
|
21
|
+
return v;
|
|
22
|
+
}
|
|
23
|
+
export function assertClientSchema(version) {
|
|
24
|
+
if (version < MIN_CLIENT_SCHEMA) {
|
|
25
|
+
return bad("schemaVersion", `must be at least ${MIN_CLIENT_SCHEMA}`);
|
|
26
|
+
}
|
|
27
|
+
if (version > REMOTE_SCHEMA_VERSION) {
|
|
28
|
+
return bad("schemaVersion", `must be at most ${REMOTE_SCHEMA_VERSION}`);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function validateBranch(name, value) {
|
|
33
|
+
if (!value)
|
|
34
|
+
return bad(name, "must not be empty");
|
|
35
|
+
if (value.length > 255)
|
|
36
|
+
return bad(name, "too long");
|
|
37
|
+
if (/[\0\r\n]/.test(value))
|
|
38
|
+
return bad(name, "invalid characters");
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
export function validateRemotePath(field, path) {
|
|
42
|
+
if (!path)
|
|
43
|
+
return bad(field, "must not be empty");
|
|
44
|
+
if (path.includes("\\"))
|
|
45
|
+
return bad(field, "must use forward slashes");
|
|
46
|
+
if (/[\0]/.test(path))
|
|
47
|
+
return bad(field, "invalid characters");
|
|
48
|
+
const norm = path.replace(/\\/g, "/");
|
|
49
|
+
if (norm.startsWith("/") ||
|
|
50
|
+
norm.startsWith("//") ||
|
|
51
|
+
/^[A-Za-z]:\//.test(norm) ||
|
|
52
|
+
/^[A-Za-z]:$/.test(norm) ||
|
|
53
|
+
norm.split("/").includes("..")) {
|
|
54
|
+
return bad(field, "must stay inside the repository");
|
|
55
|
+
}
|
|
56
|
+
const escape = rejectEscapingPath(path);
|
|
57
|
+
if (escape)
|
|
58
|
+
return bad(field, escape.replace(/^Tool error: /, ""));
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function validateRevisionFile(file, index, paths) {
|
|
62
|
+
if (!isRecord(file)) {
|
|
63
|
+
return bad(`revision.files[${index}]`, "must be an object");
|
|
64
|
+
}
|
|
65
|
+
const path = file.path;
|
|
66
|
+
if (typeof path !== "string") {
|
|
67
|
+
return bad(`revision.files[${index}].path`, "must be a string");
|
|
68
|
+
}
|
|
69
|
+
const pathErr = validateRemotePath(`revision.files[${index}].path`, path);
|
|
70
|
+
if (pathErr)
|
|
71
|
+
return pathErr;
|
|
72
|
+
if (paths.has(path)) {
|
|
73
|
+
return bad(`revision.files[${index}].path`, "duplicate path");
|
|
74
|
+
}
|
|
75
|
+
paths.add(path);
|
|
76
|
+
const status = file.status;
|
|
77
|
+
if (typeof status !== "string" ||
|
|
78
|
+
!REVISION_STATUSES.has(status)) {
|
|
79
|
+
return bad(`revision.files[${index}].status`, "invalid status");
|
|
80
|
+
}
|
|
81
|
+
const previousPath = file.previousPath;
|
|
82
|
+
if (previousPath !== null && previousPath !== undefined) {
|
|
83
|
+
if (typeof previousPath !== "string") {
|
|
84
|
+
return bad(`revision.files[${index}].previousPath`, "must be a string or null");
|
|
85
|
+
}
|
|
86
|
+
const prevErr = validateRemotePath(`revision.files[${index}].previousPath`, previousPath);
|
|
87
|
+
if (prevErr)
|
|
88
|
+
return prevErr;
|
|
89
|
+
}
|
|
90
|
+
if (status === "renamed") {
|
|
91
|
+
if (typeof previousPath !== "string" || !previousPath) {
|
|
92
|
+
return bad(`revision.files[${index}].previousPath`, "required for renamed");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const binary = file.binary;
|
|
96
|
+
if (typeof binary !== "boolean") {
|
|
97
|
+
return bad(`revision.files[${index}].binary`, "must be a boolean");
|
|
98
|
+
}
|
|
99
|
+
const patch = file.patch;
|
|
100
|
+
if (typeof patch !== "string") {
|
|
101
|
+
return bad(`revision.files[${index}].patch`, "must be a string");
|
|
102
|
+
}
|
|
103
|
+
if (binary && patch.length > 0) {
|
|
104
|
+
return bad(`revision.files[${index}].patch`, "must be empty for binary files");
|
|
105
|
+
}
|
|
106
|
+
for (const key of ["additions", "deletions"]) {
|
|
107
|
+
const n = file[key];
|
|
108
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 0) {
|
|
109
|
+
return bad(`revision.files[${index}].${key}`, "must be a non-negative integer");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
export function validateRevisionPayload(revision) {
|
|
115
|
+
if (!isRecord(revision))
|
|
116
|
+
return bad("revision", "required");
|
|
117
|
+
const files = revision.files;
|
|
118
|
+
if (!Array.isArray(files) || files.length < 1) {
|
|
119
|
+
return bad("revision.files", "must have at least one file");
|
|
120
|
+
}
|
|
121
|
+
const paths = new Set();
|
|
122
|
+
for (let i = 0; i < files.length; i++) {
|
|
123
|
+
const err = validateRevisionFile(files[i], i, paths);
|
|
124
|
+
if (err)
|
|
125
|
+
return err;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
export function validateHandshakeRequest(body) {
|
|
130
|
+
if (!isRecord(body))
|
|
131
|
+
return "body must be an object";
|
|
132
|
+
const version = parseSchemaVersion(body);
|
|
133
|
+
if (typeof version === "string")
|
|
134
|
+
return version;
|
|
135
|
+
const schemaErr = assertClientSchema(version);
|
|
136
|
+
if (schemaErr)
|
|
137
|
+
return schemaErr;
|
|
138
|
+
if (typeof body.clientVersion !== "string" || !body.clientVersion) {
|
|
139
|
+
return bad("clientVersion", "required");
|
|
140
|
+
}
|
|
141
|
+
if (typeof body.repo !== "string" || !body.repo.includes("/")) {
|
|
142
|
+
return bad("repo", "must be owner/repo");
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
export function validateSubmitRequest(body) {
|
|
147
|
+
if (!isRecord(body))
|
|
148
|
+
return "body must be an object";
|
|
149
|
+
const version = parseSchemaVersion(body);
|
|
150
|
+
if (typeof version === "string")
|
|
151
|
+
return version;
|
|
152
|
+
const schemaErr = assertClientSchema(version);
|
|
153
|
+
if (schemaErr)
|
|
154
|
+
return schemaErr;
|
|
155
|
+
const requestId = body.requestId;
|
|
156
|
+
if (typeof requestId !== "string" || !UUID_RE.test(requestId)) {
|
|
157
|
+
return bad("requestId", "must be a UUID");
|
|
158
|
+
}
|
|
159
|
+
if (typeof body.repo !== "string" || !body.repo) {
|
|
160
|
+
return bad("repo", "required");
|
|
161
|
+
}
|
|
162
|
+
if (typeof body.branch !== "string") {
|
|
163
|
+
return bad("branch", "must be a string");
|
|
164
|
+
}
|
|
165
|
+
const branchErr = validateBranch("branch", body.branch);
|
|
166
|
+
if (branchErr)
|
|
167
|
+
return branchErr;
|
|
168
|
+
if (body.toBranch !== undefined && typeof body.toBranch !== "string") {
|
|
169
|
+
return bad("toBranch", "must be a string");
|
|
170
|
+
}
|
|
171
|
+
if (body.fresh !== undefined && typeof body.fresh !== "boolean") {
|
|
172
|
+
return bad("fresh", "must be a boolean");
|
|
173
|
+
}
|
|
174
|
+
const revisionErr = validateRevisionPayload(body.revision);
|
|
175
|
+
if (revisionErr)
|
|
176
|
+
return revisionErr;
|
|
177
|
+
if (body.capabilities !== undefined) {
|
|
178
|
+
if (!isRecord(body.capabilities)) {
|
|
179
|
+
return bad("capabilities", "must be an object");
|
|
180
|
+
}
|
|
181
|
+
const tools = body.capabilities.tools;
|
|
182
|
+
if (tools !== undefined) {
|
|
183
|
+
if (!Array.isArray(tools))
|
|
184
|
+
return bad("capabilities.tools", "must be an array");
|
|
185
|
+
for (let i = 0; i < tools.length; i++) {
|
|
186
|
+
const tool = tools[i];
|
|
187
|
+
if (!isRecord(tool)) {
|
|
188
|
+
return bad(`capabilities.tools[${i}]`, "must be an object");
|
|
189
|
+
}
|
|
190
|
+
if (typeof tool.name !== "string" || !tool.name) {
|
|
191
|
+
return bad(`capabilities.tools[${i}].name`, "required");
|
|
192
|
+
}
|
|
193
|
+
// Unknown tools are ignored at runtime; no error here (plan §14.5).
|
|
194
|
+
if (REMOTE_KNOWN_TOOL_NAMES.has(tool.name) &&
|
|
195
|
+
tool.version !== undefined &&
|
|
196
|
+
typeof tool.version !== "string") {
|
|
197
|
+
return bad(`capabilities.tools[${i}].version`, "must be a string");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
export function validateSyncRequest(body) {
|
|
205
|
+
if (!isRecord(body))
|
|
206
|
+
return "body must be an object";
|
|
207
|
+
const version = parseSchemaVersion(body);
|
|
208
|
+
if (typeof version === "string")
|
|
209
|
+
return version;
|
|
210
|
+
const schemaErr = assertClientSchema(version);
|
|
211
|
+
if (schemaErr)
|
|
212
|
+
return schemaErr;
|
|
213
|
+
const after = body.afterLogSeq;
|
|
214
|
+
if (typeof after !== "number" || !Number.isInteger(after) || after < 0) {
|
|
215
|
+
return bad("afterLogSeq", "must be a non-negative integer");
|
|
216
|
+
}
|
|
217
|
+
const results = body.toolResults;
|
|
218
|
+
if (results !== undefined) {
|
|
219
|
+
if (!Array.isArray(results))
|
|
220
|
+
return bad("toolResults", "must be an array");
|
|
221
|
+
for (let i = 0; i < results.length; i++) {
|
|
222
|
+
const row = results[i];
|
|
223
|
+
if (!isRecord(row)) {
|
|
224
|
+
return bad(`toolResults[${i}]`, "must be an object");
|
|
225
|
+
}
|
|
226
|
+
if (typeof row.callId !== "string" || !row.callId) {
|
|
227
|
+
return bad(`toolResults[${i}].callId`, "required");
|
|
228
|
+
}
|
|
229
|
+
const hasOut = row.output !== undefined;
|
|
230
|
+
const hasErr = row.error !== undefined;
|
|
231
|
+
if (hasOut && typeof row.output !== "string") {
|
|
232
|
+
return bad(`toolResults[${i}].output`, "must be a string");
|
|
233
|
+
}
|
|
234
|
+
if (hasErr && typeof row.error !== "string") {
|
|
235
|
+
return bad(`toolResults[${i}].error`, "must be a string");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
export function validateSyncResponseStatus(status) {
|
|
242
|
+
if (typeof status !== "string")
|
|
243
|
+
return bad("status", "required");
|
|
244
|
+
if (!REMOTE_SYNC_STATUSES.includes(status)) {
|
|
245
|
+
return bad("status", "unexpected value");
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|