@mmerterden/multi-agent-pipeline 20.30.0 → 20.34.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/CHANGELOG.md +123 -0
- package/README.md +47 -1
- package/README.tr.md +61 -1
- package/docs/adr/0018-cross-repo-links-not-a-global-graph.md +43 -0
- package/docs/adr/README.md +2 -1
- package/docs/facts.json +6 -5
- package/docs/features.md +1 -1
- package/install/_dev-only-files.mjs +1 -0
- package/install/_unattended-profile.mjs +61 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +16 -2
- package/install/index.mjs +10 -1
- package/install/templates/claude-hooks.json +23 -1
- package/manifest.json +198 -134
- package/package.json +4 -1
- package/pipeline/agents/code-reviewer.md +9 -0
- package/pipeline/agents/explorer.md +4 -0
- package/pipeline/agents/plan-critic.md +4 -2
- package/pipeline/commands/multi-agent/SKILL.md +17 -136
- package/pipeline/commands/multi-agent/analysis/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/channels/SKILL.md +15 -297
- package/pipeline/commands/multi-agent/graph/SKILL.md +5 -2
- package/pipeline/commands/multi-agent/language/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/office-on/SKILL.md +8 -7
- package/pipeline/commands/multi-agent/office-status/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +4 -1
- package/pipeline/commands/multi-agent/resume/SKILL.md +8 -22
- package/pipeline/commands/multi-agent/review/SKILL.md +11 -134
- package/pipeline/commands/multi-agent/scenario-audit/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/setup/SKILL.md +19 -224
- package/pipeline/commands/multi-agent/sync/SKILL.md +12 -0
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +29 -3
- package/pipeline/contract/CHANGELOG.md +46 -0
- package/pipeline/contract/README.md +27 -10
- package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
- package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
- package/pipeline/contract/fixtures/error-unsigned.json +1 -1
- package/pipeline/contract/fixtures/launch-plan.json +1 -1
- package/pipeline/contract/fixtures/runs-awaiting-question.json +6 -1
- package/pipeline/contract/fixtures/runs-empty.json +1 -1
- package/pipeline/contract/fixtures/runs-failed.json +6 -1
- package/pipeline/contract/fixtures/runs-old-schema.json +6 -1
- package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +5 -1
- package/pipeline/contract/fixtures/runs-pr-opened.json +6 -1
- package/pipeline/contract/fixtures/runs-running.json +6 -1
- package/pipeline/contract/manifest.json +153 -19
- package/pipeline/contract/types/index.d.ts +304 -6
- package/pipeline/lib/bg-launch.mjs +106 -0
- package/pipeline/lib/code-digest.mjs +49 -0
- package/pipeline/lib/credential-resolve.mjs +42 -2
- package/pipeline/multi-agent-refs/channels/generator.md +146 -0
- package/pipeline/multi-agent-refs/channels/menu.md +55 -0
- package/pipeline/multi-agent-refs/channels/multi-repo.md +6 -0
- package/pipeline/multi-agent-refs/channels/preview.md +25 -0
- package/pipeline/multi-agent-refs/channels/reference.md +98 -0
- package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +1 -1
- package/pipeline/multi-agent-refs/features/code-graph.md +27 -0
- package/pipeline/multi-agent-refs/features/office-threat-model.md +24 -6
- package/pipeline/multi-agent-refs/features/plan-critic.md +5 -2
- package/pipeline/multi-agent-refs/features/review-delta.md +4 -1
- package/pipeline/multi-agent-refs/features/scope-check.md +10 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +1 -1
- package/pipeline/multi-agent-refs/help/en.md +2 -2
- package/pipeline/multi-agent-refs/help/tr.md +2 -2
- package/pipeline/multi-agent-refs/knowledge.md +9 -0
- package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +163 -0
- package/pipeline/multi-agent-refs/phases/modes.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +4 -4
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +9 -8
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +12 -7
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +7 -3
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +19 -10
- package/pipeline/multi-agent-refs/picker-contract.md +1 -1
- package/pipeline/multi-agent-refs/resume/details.md +35 -0
- package/pipeline/multi-agent-refs/review/inputs.md +87 -0
- package/pipeline/multi-agent-refs/review/module-guides.md +25 -0
- package/pipeline/multi-agent-refs/review/valven.md +42 -0
- package/pipeline/multi-agent-refs/review-standalone-tracker.md +37 -0
- package/pipeline/multi-agent-refs/setup/accounts.md +27 -0
- package/pipeline/multi-agent-refs/setup/app-store-connect.md +5 -0
- package/pipeline/multi-agent-refs/setup/firebase.md +5 -0
- package/pipeline/multi-agent-refs/setup/google-play.md +4 -0
- package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +21 -0
- package/pipeline/multi-agent-refs/setup/language.md +46 -0
- package/pipeline/multi-agent-refs/setup/multi-repo-group.md +51 -0
- package/pipeline/multi-agent-refs/setup/summaries.md +63 -0
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +51 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +8 -3
- package/pipeline/office/NOTICE.md +4 -1
- package/pipeline/office/assets/ARCHIVE_BOXES.png +0 -0
- package/pipeline/office/assets/COFFEE_MACHINE.png +0 -0
- package/pipeline/office/assets/COUNTER_SINK.png +0 -0
- package/pipeline/office/assets/DEVICE_STAND.png +0 -0
- package/pipeline/office/assets/FILING_CABINET.png +0 -0
- package/pipeline/office/assets/FRIDGE.png +0 -0
- package/pipeline/office/assets/MAILBOX.png +0 -0
- package/pipeline/office/assets/MOP_BUCKET.png +0 -0
- package/pipeline/office/assets/PRINTER.png +0 -0
- package/pipeline/office/assets/RECEPTION_DESK.png +0 -0
- package/pipeline/office/assets/SERVER_RACK.png +0 -0
- package/pipeline/office/assets/SERVER_RACK_2.png +0 -0
- package/pipeline/office/assets/TOILET_STALL.png +0 -0
- package/pipeline/office/assets/VENDING.png +0 -0
- package/pipeline/office/assets/WASHBASIN.png +0 -0
- package/pipeline/office/assets/WATER_COOLER.png +0 -0
- package/pipeline/office/index.html +3936 -819
- package/pipeline/office/toolkit.json +3 -0
- package/pipeline/rules/response-shape.md +6 -7
- package/pipeline/schemas/activity.schema.json +188 -4
- package/pipeline/schemas/agent-state.schema.json +511 -115
- package/pipeline/schemas/contract-error.schema.json +12 -1
- package/pipeline/schemas/job-change.schema.json +72 -0
- package/pipeline/schemas/job-request.schema.json +38 -0
- package/pipeline/schemas/launch.json +4 -4
- package/pipeline/schemas/office-credentials.schema.json +71 -0
- package/pipeline/schemas/office-jobs.json +253 -0
- package/pipeline/schemas/office-jobs.schema.json +62 -0
- package/pipeline/schemas/office-memory.schema.json +105 -0
- package/pipeline/schemas/office-settings-request.schema.json +30 -0
- package/pipeline/schemas/office-settings.schema.json +67 -0
- package/pipeline/schemas/phases.json +14 -6
- package/pipeline/schemas/phases.schema.json +86 -19
- package/pipeline/schemas/plan-critique.schema.json +9 -2
- package/pipeline/schemas/planning-output.schema.json +25 -2
- package/pipeline/schemas/prefs.schema.json +24 -1
- package/pipeline/schemas/prs.schema.json +5 -0
- package/pipeline/schemas/prune-logs-request.schema.json +26 -0
- package/pipeline/schemas/prune-logs.schema.json +52 -0
- package/pipeline/schemas/repo-change.schema.json +54 -0
- package/pipeline/schemas/repo-request.schema.json +29 -0
- package/pipeline/schemas/repos.schema.json +49 -1
- package/pipeline/schemas/reviewer-output.schema.json +20 -2
- package/pipeline/schemas/runs-index.schema.json +257 -46
- package/pipeline/schemas/system-status.schema.json +111 -0
- package/pipeline/schemas/token-budget.json +4 -4
- package/pipeline/schemas/tools.schema.json +5 -0
- package/pipeline/schemas/triage-output.schema.json +20 -2
- package/pipeline/schemas/unattended-profile.json +7 -2
- package/pipeline/scripts/_active-run.mjs +2 -1
- package/pipeline/scripts/_graph-analysis.mjs +270 -0
- package/pipeline/scripts/_memory-policy.mjs +165 -0
- package/pipeline/scripts/_secret-scan.mjs +105 -0
- package/pipeline/scripts/_spawn-async.mjs +9 -5
- package/pipeline/scripts/_unified-diff.mjs +2 -2
- package/pipeline/scripts/activity-snapshot.mjs +219 -23
- package/pipeline/scripts/autopilot-runner.mjs +7 -5
- package/pipeline/scripts/capture-flush.sh +12 -2
- package/pipeline/scripts/check-read-size.sh +9 -0
- package/pipeline/scripts/code-graph-rules/android.json +8 -3
- package/pipeline/scripts/contract-server.mjs +575 -28
- package/pipeline/scripts/debt-ledger.mjs +323 -0
- package/pipeline/scripts/file-context.mjs +261 -0
- package/pipeline/scripts/file-context.sh +20 -0
- package/pipeline/scripts/gen-mode-dispatch.mjs +3 -1
- package/pipeline/scripts/graph-links.mjs +230 -0
- package/pipeline/scripts/graph-query.mjs +133 -1
- package/pipeline/scripts/graph-report.mjs +65 -0
- package/pipeline/scripts/job-launch.mjs +477 -0
- package/pipeline/scripts/launch-request.mjs +94 -9
- package/pipeline/scripts/learn-from-transcripts.mjs +12 -48
- package/pipeline/scripts/learnings-ledger.mjs +143 -24
- package/pipeline/scripts/office-build.mjs +148 -11
- package/pipeline/scripts/office-control.mjs +34 -7
- package/pipeline/scripts/office-credentials.mjs +106 -0
- package/pipeline/scripts/office-memory.mjs +200 -0
- package/pipeline/scripts/office-prune.mjs +125 -0
- package/pipeline/scripts/office-settings.mjs +145 -0
- package/pipeline/scripts/phase-tracker.sh +12 -5
- package/pipeline/scripts/plan-critique-gate.mjs +11 -2
- package/pipeline/scripts/prune-logs.sh +53 -2
- package/pipeline/scripts/rework-rulings.mjs +106 -0
- package/pipeline/scripts/runs-index.mjs +62 -2
- package/pipeline/scripts/scope-check-gate.mjs +149 -2
- package/pipeline/scripts/system-status.mjs +196 -0
- package/pipeline/scripts/task-mirror.mjs +150 -0
- package/pipeline/scripts/task-mirror.sh +20 -0
- package/pipeline/scripts/toolkit-tools.mjs +33 -7
- package/pipeline/scripts/transcript-index.mjs +4 -1
- package/pipeline/scripts/triage-memory.mjs +64 -37
- package/pipeline/scripts/validate-planning.mjs +68 -4
- package/pipeline/scripts/validate-reviewer.mjs +20 -0
- package/pipeline/scripts/validate-triage.mjs +107 -2
- package/pipeline/scripts/work-items.mjs +191 -22
- package/pipeline/skills/.skill-manifest.json +9 -9
- package/pipeline/skills/.skills-index.json +1 -1
- package/pipeline/skills/shared/README.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +5 -2
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +3 -4
- package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +4 -1
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +21 -0
- package/pipeline/skills/shared/core/multi-agent-scenario-audit/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +26 -3
- package/pipeline/skills/skills-index.md +1 -1
|
@@ -20,13 +20,19 @@
|
|
|
20
20
|
* GET /v1/issues issues.mjs --json [--source S] [--project K]... [--max N]
|
|
21
21
|
* GET /v1/worktrees worktrees.mjs --json [--measure] [--redact]
|
|
22
22
|
* GET /v1/launch-spec launch-request.mjs spec
|
|
23
|
-
* GET /v1/repos launch-request.mjs repos [--redact]
|
|
23
|
+
* GET /v1/repos launch-request.mjs repos [--redact]; ?all=1 adds
|
|
24
|
+
* the suggested and the missing registered ones
|
|
25
|
+
* POST /v1/repos launch-request.mjs register-repo (two steps)
|
|
26
|
+
* DELETE /v1/repos/{id} remove a registered repository (two steps)
|
|
24
27
|
* POST /v1/launch?repo=P write a launch request, return launch-request.mjs plan
|
|
25
28
|
* POST /v1/runs/{id}/answer answer-question.mjs --question Q --answer IDS
|
|
26
29
|
* GET /v1/runs/{id}/log run-log.mjs {id} --tail N
|
|
27
30
|
* POST /v1/runs/{id}/resume launch-request.mjs resume: the plan that resumes it
|
|
28
31
|
* POST /v1/runs/{id}/kill run-kill.mjs preview, then apply (two steps)
|
|
29
32
|
* POST /v1/gc gc-plan.mjs preview, then apply (two steps)
|
|
33
|
+
* POST /v1/prune-logs office-prune.mjs preview, then apply the listed folders (two steps)
|
|
34
|
+
* GET /v1/settings office-settings.mjs --json
|
|
35
|
+
* POST /v1/settings office-settings.mjs set S V, after a preview (two steps)
|
|
30
36
|
* GET /v1/autopilot autopilot-control.mjs status
|
|
31
37
|
* POST /v1/autopilot/off autopilot-control.mjs off [--now]
|
|
32
38
|
* POST /v1/autopilot/on autopilot-control.mjs on-preview, then on (two steps)
|
|
@@ -35,6 +41,9 @@
|
|
|
35
41
|
* GET /v1/activity/stream the same document as server-sent events, on change
|
|
36
42
|
* GET /v1/agents/{sessionId}/{agentId}/chat activity-snapshot.mjs chat
|
|
37
43
|
* GET /v1/ledger office-ledger.mjs --json
|
|
44
|
+
* GET /v1/system system-status.mjs --json
|
|
45
|
+
* GET /v1/credentials office-credentials.mjs --json [--probe]
|
|
46
|
+
* GET /v1/memory office-memory.mjs --json
|
|
38
47
|
* GET /v1/runs/{id}/results run-results.mjs: diff, findings, tests, evidence, analysis
|
|
39
48
|
* GET /v1/runs/{id}/evidence/{name} run-results.mjs --evidence: one screenshot
|
|
40
49
|
* POST /v1/runs/{id}/review-verdict work-items.mjs verdict, then --post (two steps)
|
|
@@ -48,6 +57,8 @@
|
|
|
48
57
|
* DELETE /v1/routines/{name} routine-registry.mjs remove (two steps)
|
|
49
58
|
* GET /v1/stack?repo=R stack-toggle.mjs status
|
|
50
59
|
* POST /v1/stack stack-toggle.mjs set (two steps)
|
|
60
|
+
* GET /v1/jobs job-launch.mjs list: the jobs the office can start
|
|
61
|
+
* POST /v1/jobs start one in the background (two steps)
|
|
51
62
|
*
|
|
52
63
|
* Every route carries two tags for a remote carrier to enforce later, and
|
|
53
64
|
* manifest.json repeats them: `remote` is the scope it would need (read,
|
|
@@ -60,7 +71,8 @@
|
|
|
60
71
|
* prefs.global.office.page, else the installed copy (office/index.html next
|
|
61
72
|
* to this scripts directory), and is 404 when none exists. The page carries
|
|
62
73
|
* no token: it trades a one-time code from its URL fragment for one. It and
|
|
63
|
-
* its sprites under /office/assets/ are the only routes that need none
|
|
74
|
+
* its sprites under /office/assets/ are the only routes that need none (with
|
|
75
|
+
* /favicon.ico, an empty 204, because a browser asks for it on its own), and
|
|
64
76
|
* its response carries a per-response CSP nonce for its inline script and
|
|
65
77
|
* style.
|
|
66
78
|
*
|
|
@@ -72,12 +84,16 @@
|
|
|
72
84
|
* POST /v1/phone/runs/{id}/answer scope answer
|
|
73
85
|
* POST /v1/phone/launch?repo=P scope launch, and only after `phone-devices.mjs launch on`
|
|
74
86
|
*
|
|
75
|
-
* There is deliberately no route that merges, marks ready, runs a
|
|
76
|
-
*
|
|
77
|
-
* and environment launch.json declares
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
87
|
+
* There is deliberately no route that merges, marks ready, runs a shell
|
|
88
|
+
* command or takes a prompt. POST /v1/launch and POST /v1/runs/{id}/resume
|
|
89
|
+
* return the argv and environment launch.json declares for a client to start.
|
|
90
|
+
* POST /v1/jobs is the one route that starts a session itself, for the office:
|
|
91
|
+
* a job from schemas/office-jobs.json with typed arguments, two requests, the
|
|
92
|
+
* unattended profile, workspace trust and the autopilot runner's sandbox check
|
|
93
|
+
* first, then `claude --bg` without a shell (lib/bg-launch.mjs), never from a
|
|
94
|
+
* phone. Besides that the only `claude` this process runs is `claude agents
|
|
95
|
+
* --json` (read, for processAlive) and `claude stop <session>`, for a kill or
|
|
96
|
+
* `autopilot/off` with now.
|
|
81
97
|
*
|
|
82
98
|
* Kill, gc, autopilot on, stack and routine removal change what this machine
|
|
83
99
|
* keeps or runs, so each takes two requests. The first returns a
|
|
@@ -124,16 +140,20 @@
|
|
|
124
140
|
* body or a token) to ~/.claude/logs/multi-agent/office-actions.jsonl.
|
|
125
141
|
*
|
|
126
142
|
* Lifetime. The server exits after --idle-minutes without a request (15 by
|
|
127
|
-
* default), after --max-hours however busy it is (8 by default
|
|
128
|
-
* stream does not extend it), and on SIGTERM, SIGINT or
|
|
129
|
-
* token file either way.
|
|
143
|
+
* default, 0 never), after --max-hours however busy it is (8 by default, 0 no
|
|
144
|
+
* limit; an open event stream does not extend it), and on SIGTERM, SIGINT or
|
|
145
|
+
* SIGHUP, removing its token file either way. office-on starts it with both at
|
|
146
|
+
* 0, so the panel runs until office-off.
|
|
130
147
|
*
|
|
131
148
|
* Panel bootstrap. `POST /office/code` (bearer, no Origin) mints a code that
|
|
132
149
|
* is valid for 10 seconds and one use; office-control.mjs opens the browser
|
|
133
150
|
* with it in the URL fragment. The page trades it for the token with
|
|
134
151
|
* `POST /office/exchange` (no bearer, own Origin only); a second use, an
|
|
135
152
|
* expired code or an unknown one is 401. The token itself never appears in a
|
|
136
|
-
* URL.
|
|
153
|
+
* URL. `GET /office/version` gives the installed page's build id and whether
|
|
154
|
+
* this server's code changed on disk since it started; `POST /office/reload`
|
|
155
|
+
* mints a fresh code for the page itself, so it can reload onto a newly
|
|
156
|
+
* installed page. It is started on demand by `/multi-agent:serve` or by a client, and is
|
|
137
157
|
* never installed as a service.
|
|
138
158
|
*
|
|
139
159
|
* Zero runtime dependencies (ADR-0004): node:http, node:crypto and the
|
|
@@ -164,7 +184,7 @@ import {
|
|
|
164
184
|
} from "node:fs";
|
|
165
185
|
import { createServer } from "node:http";
|
|
166
186
|
import { homedir } from "node:os";
|
|
167
|
-
import { dirname, isAbsolute, join, sep } from "node:path";
|
|
187
|
+
import { basename, dirname, isAbsolute, join, sep } from "node:path";
|
|
168
188
|
import { fileURLToPath } from "node:url";
|
|
169
189
|
import { clearInterval, clearTimeout, setInterval, setTimeout } from "node:timers";
|
|
170
190
|
import { answerStateFile } from "./answer-question.mjs";
|
|
@@ -185,6 +205,17 @@ import {
|
|
|
185
205
|
} from "./stack-toggle.mjs";
|
|
186
206
|
import { CONTRACT_VERSION as STEER_VERSION, SteerError, queueSteer } from "./steer-run.mjs";
|
|
187
207
|
import { officeLedger } from "./office-ledger.mjs";
|
|
208
|
+
import { systemStatus } from "./system-status.mjs";
|
|
209
|
+
import { officeCredentials } from "./office-credentials.mjs";
|
|
210
|
+
import { officeMemory } from "./office-memory.mjs";
|
|
211
|
+
import { AGES as PRUNE_AGES, pruneApply, prunePreview } from "./office-prune.mjs";
|
|
212
|
+
import {
|
|
213
|
+
CHOICES,
|
|
214
|
+
SettingsError,
|
|
215
|
+
applySetting,
|
|
216
|
+
freeChainOf,
|
|
217
|
+
readSettings,
|
|
218
|
+
} from "./office-settings.mjs";
|
|
188
219
|
import { EVIDENCE_NAME, ResultsError, runEvidence, runResults } from "./run-results.mjs";
|
|
189
220
|
import {
|
|
190
221
|
WorkError,
|
|
@@ -195,6 +226,7 @@ import {
|
|
|
195
226
|
} from "./work-items.mjs";
|
|
196
227
|
import {
|
|
197
228
|
CONTRACT_VERSION as TOOLS_VERSION,
|
|
229
|
+
activeToolkitVersion,
|
|
198
230
|
TOOL_NAME,
|
|
199
231
|
ToolError,
|
|
200
232
|
createToolkitClient,
|
|
@@ -224,12 +256,26 @@ import {
|
|
|
224
256
|
planLaunch,
|
|
225
257
|
planResume,
|
|
226
258
|
questionsDocument,
|
|
259
|
+
registerRepo,
|
|
227
260
|
reposDocument,
|
|
261
|
+
repoId,
|
|
228
262
|
requestsDir,
|
|
263
|
+
suggestedRepos,
|
|
264
|
+
unregisterRepo,
|
|
265
|
+
missingRepos,
|
|
229
266
|
workingTreeState,
|
|
230
267
|
writeRequestFile,
|
|
231
268
|
} from "./launch-request.mjs";
|
|
232
269
|
import { GROUP_NAMES, envelope, selectRuns } from "./runs-index.mjs";
|
|
270
|
+
import {
|
|
271
|
+
CONTRACT_VERSION as JOBS_VERSION,
|
|
272
|
+
JobError,
|
|
273
|
+
jobsDocument,
|
|
274
|
+
previewJob,
|
|
275
|
+
startJob,
|
|
276
|
+
} from "./job-launch.mjs";
|
|
277
|
+
import { guardRegistration, sandboxPrecondition } from "./autopilot-runner.mjs";
|
|
278
|
+
import { codeDigest } from "../lib/code-digest.mjs";
|
|
233
279
|
import { CONTRACT_VERSION as KILL_VERSION, KillError, killPreview, killRun } from "./run-kill.mjs";
|
|
234
280
|
import {
|
|
235
281
|
CONTRACT_VERSION as LOG_VERSION,
|
|
@@ -259,7 +305,7 @@ import { runMain } from "../lib/fatal.mjs";
|
|
|
259
305
|
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
260
306
|
|
|
261
307
|
/** The contract as a whole; pipeline/contract/manifest.json carries the same. */
|
|
262
|
-
export const CONTRACT_VERSION = "1.
|
|
308
|
+
export const CONTRACT_VERSION = "1.6.0";
|
|
263
309
|
/** The majors `?v=` accepts. A new major keeps the previous one here for one minor. */
|
|
264
310
|
export const SUPPORTED_MAJORS = [1];
|
|
265
311
|
export const BIND_HOST = "127.0.0.1";
|
|
@@ -290,6 +336,7 @@ const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
|
290
336
|
const AGENT_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
291
337
|
const ROUTINE_NAME = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
292
338
|
const REPO_NAME = /^[A-Za-z0-9._-]{1,100}(?:\/[A-Za-z0-9._-]{1,100})?$/;
|
|
339
|
+
const REPO_ID = /^[0-9a-f]{16}$/;
|
|
293
340
|
/** Server-sent event streams open at once; one more is refused. */
|
|
294
341
|
export const MAX_STREAMS = 4;
|
|
295
342
|
const STREAM_POLL_MS = 2000;
|
|
@@ -344,6 +391,7 @@ const Q = {
|
|
|
344
391
|
redact: { check: (v) => v === "0" || v === "1" },
|
|
345
392
|
group: { check: (v) => GROUP_NAMES.includes(v) },
|
|
346
393
|
measure: { check: (v) => v === "0" || v === "1" },
|
|
394
|
+
all: { check: (v) => v === "0" || v === "1" },
|
|
347
395
|
source: { check: (v) => ["jira", "github", "all"].includes(v) },
|
|
348
396
|
project: { repeat: true, check: (v) => PROJECT_KEY.test(v) },
|
|
349
397
|
max: { check: (v) => /^[0-9]{1,3}$/.test(v) && Number(v) >= 1 && Number(v) <= 200 },
|
|
@@ -716,6 +764,87 @@ async function killRoute({ params, body, redact }) {
|
|
|
716
764
|
}
|
|
717
765
|
}
|
|
718
766
|
|
|
767
|
+
/**
|
|
768
|
+
* POST /v1/prune-logs: what prune-logs.sh would remove for an age, and a token
|
|
769
|
+
* bound to that age and folder list; then a prune of exactly that list
|
|
770
|
+
* (pruneApply), refused with 409 changed when the folders moved. The age is
|
|
771
|
+
* one of a fixed set.
|
|
772
|
+
*/
|
|
773
|
+
async function pruneRoute({ body }) {
|
|
774
|
+
requireBody(
|
|
775
|
+
body,
|
|
776
|
+
{ confirmToken: isToken, olderThanDays: (v) => PRUNE_AGES.includes(v) },
|
|
777
|
+
"prune-logs-request.schema.json",
|
|
778
|
+
);
|
|
779
|
+
if (body.confirmToken === undefined) {
|
|
780
|
+
if (body.olderThanDays === undefined)
|
|
781
|
+
throw new HttpError(400, "invalid-body", "olderThanDays is required for the preview");
|
|
782
|
+
const { all, ...doc } = await prunePreview(body.olderThanDays, process.env, {
|
|
783
|
+
keepAll: true,
|
|
784
|
+
});
|
|
785
|
+
return {
|
|
786
|
+
...doc,
|
|
787
|
+
...mintConfirm("prune-logs", "", { days: body.olderThanDays, all, count: doc.count }),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
if (body.olderThanDays !== undefined)
|
|
791
|
+
throw new HttpError(
|
|
792
|
+
400,
|
|
793
|
+
"invalid-body",
|
|
794
|
+
"olderThanDays belongs to the preview, not the confirmation",
|
|
795
|
+
);
|
|
796
|
+
const { days, all, count } = redeemConfirm(body.confirmToken, "prune-logs", "");
|
|
797
|
+
try {
|
|
798
|
+
return await pruneApply(days, process.env, { all, count });
|
|
799
|
+
} catch (e) {
|
|
800
|
+
if (e.code === "changed") throw new HttpError(409, "changed", e.message);
|
|
801
|
+
throw e;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* POST /v1/settings: the change a setting would make and a token, then the
|
|
807
|
+
* same script the terminal command runs. The token is bound to that setting
|
|
808
|
+
* and value.
|
|
809
|
+
*/
|
|
810
|
+
async function settingsRoute({ body }) {
|
|
811
|
+
requireBody(
|
|
812
|
+
body,
|
|
813
|
+
{
|
|
814
|
+
confirmToken: isToken,
|
|
815
|
+
setting: (v) => Object.hasOwn(CHOICES, v),
|
|
816
|
+
value: (v) => typeof v === "string",
|
|
817
|
+
},
|
|
818
|
+
"office-settings-request.schema.json",
|
|
819
|
+
);
|
|
820
|
+
if (body.confirmToken === undefined) {
|
|
821
|
+
if (!body.setting || !CHOICES[body.setting].includes(body.value))
|
|
822
|
+
throw new HttpError(400, "invalid-body", "setting and one of its values are required");
|
|
823
|
+
const now = readSettings();
|
|
824
|
+
return {
|
|
825
|
+
...now,
|
|
826
|
+
change: {
|
|
827
|
+
setting: body.setting,
|
|
828
|
+
from: now[body.setting],
|
|
829
|
+
to: body.value,
|
|
830
|
+
...(body.setting === "route" && body.value === "on" && freeChainOf()
|
|
831
|
+
? { freeChain: freeChainOf() }
|
|
832
|
+
: {}),
|
|
833
|
+
},
|
|
834
|
+
...mintConfirm("settings", "", { setting: body.setting, value: body.value }),
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
if (body.setting !== undefined || body.value !== undefined)
|
|
838
|
+
throw new HttpError(400, "invalid-body", "the confirmation carries only its token");
|
|
839
|
+
const { setting, value } = redeemConfirm(body.confirmToken, "settings", "");
|
|
840
|
+
try {
|
|
841
|
+
return await applySetting(setting, value);
|
|
842
|
+
} catch (e) {
|
|
843
|
+
if (e instanceof SettingsError) throw new HttpError(409, "not-applied", e.message);
|
|
844
|
+
throw e;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
719
848
|
function gcRoute({ body, redact }) {
|
|
720
849
|
requireBody(
|
|
721
850
|
body,
|
|
@@ -775,6 +904,39 @@ async function control(args) {
|
|
|
775
904
|
return doc;
|
|
776
905
|
}
|
|
777
906
|
|
|
907
|
+
/**
|
|
908
|
+
* For a read whose answer is the same for every caller: a request that
|
|
909
|
+
* arrives while one runs gets that one's answer instead of starting another.
|
|
910
|
+
* With `limit`, at most `limit.max` distinct keys of `limit.group` run at
|
|
911
|
+
* once; one more is 429, like `oneAtATime`.
|
|
912
|
+
*/
|
|
913
|
+
const inFlight = new Map();
|
|
914
|
+
const groupRunning = new Map();
|
|
915
|
+
const shared =
|
|
916
|
+
(keyOf, fn, limit = null) =>
|
|
917
|
+
(ctx) => {
|
|
918
|
+
const key = keyOf(ctx);
|
|
919
|
+
if (!inFlight.has(key)) {
|
|
920
|
+
if (limit) {
|
|
921
|
+
const n = groupRunning.get(limit.group) ?? 0;
|
|
922
|
+
if (n >= limit.max) {
|
|
923
|
+
throw new HttpError(429, "busy", "too many of these are running; try again shortly");
|
|
924
|
+
}
|
|
925
|
+
groupRunning.set(limit.group, n + 1);
|
|
926
|
+
}
|
|
927
|
+
inFlight.set(
|
|
928
|
+
key,
|
|
929
|
+
Promise.resolve()
|
|
930
|
+
.then(() => fn(ctx))
|
|
931
|
+
.finally(() => {
|
|
932
|
+
inFlight.delete(key);
|
|
933
|
+
if (limit) groupRunning.set(limit.group, groupRunning.get(limit.group) - 1);
|
|
934
|
+
}),
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
return inFlight.get(key);
|
|
938
|
+
};
|
|
939
|
+
|
|
778
940
|
/** One request at a time per key; a second one while the first runs is 429. */
|
|
779
941
|
const running = new Set();
|
|
780
942
|
const oneAtATime = (key, fn) => async (ctx) => {
|
|
@@ -1033,6 +1195,252 @@ function resolveRepo(ref) {
|
|
|
1033
1195
|
return hits[0].path;
|
|
1034
1196
|
}
|
|
1035
1197
|
|
|
1198
|
+
/**
|
|
1199
|
+
* POST /v1/repos: add a checkout launch accepts. The first request names a
|
|
1200
|
+
* suggestion (an id from GET /v1/repos?all=1) or an absolute path; the
|
|
1201
|
+
* preview says what would be stored and mints a token; the second request
|
|
1202
|
+
* sends only the token. Only a git checkout is accepted from here.
|
|
1203
|
+
*/
|
|
1204
|
+
function repoAddRoute({ body }) {
|
|
1205
|
+
requireBody(
|
|
1206
|
+
body,
|
|
1207
|
+
{
|
|
1208
|
+
confirmToken: isToken,
|
|
1209
|
+
suggestion: (v) => typeof v === "string" && REPO_ID.test(v),
|
|
1210
|
+
path: (v) => typeof v === "string" && v.length <= 4096 && isAbsolute(v),
|
|
1211
|
+
},
|
|
1212
|
+
"repo-request.schema.json",
|
|
1213
|
+
);
|
|
1214
|
+
if (body.confirmToken === undefined) {
|
|
1215
|
+
let path;
|
|
1216
|
+
if (body.suggestion !== undefined && body.path !== undefined) {
|
|
1217
|
+
throw new HttpError(400, "invalid-body", "name a suggestion or a path, not both");
|
|
1218
|
+
}
|
|
1219
|
+
if (body.suggestion !== undefined) {
|
|
1220
|
+
const hit = suggestedRepos().find((r) => r.id === body.suggestion);
|
|
1221
|
+
if (!hit) throw new HttpError(404, "not-found", "no such suggestion");
|
|
1222
|
+
path = hit.path;
|
|
1223
|
+
} else if (body.path !== undefined) {
|
|
1224
|
+
path = realOr(body.path);
|
|
1225
|
+
} else {
|
|
1226
|
+
throw new HttpError(400, "invalid-body", "suggestion or path is required");
|
|
1227
|
+
}
|
|
1228
|
+
if (launchRepoAllowed(path)) {
|
|
1229
|
+
throw new HttpError(409, "already-registered", "launch already accepts that repository");
|
|
1230
|
+
}
|
|
1231
|
+
let real;
|
|
1232
|
+
try {
|
|
1233
|
+
real = registerRepoCheck(path);
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
throw new HttpError(400, "invalid-body", err.message);
|
|
1236
|
+
}
|
|
1237
|
+
// The page picks a repository by folder name; a second one with the same
|
|
1238
|
+
// name could never be told apart there.
|
|
1239
|
+
if (launchRepos().some((r) => r.name.split("/").pop() === basename(real))) {
|
|
1240
|
+
throw new HttpError(
|
|
1241
|
+
409,
|
|
1242
|
+
"name-taken",
|
|
1243
|
+
"a repository with that folder name is already on the list",
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
return {
|
|
1247
|
+
contractVersion: ACTIVITY_VERSION,
|
|
1248
|
+
preview: { name: basename(real) },
|
|
1249
|
+
...mintConfirm("repo-add", "", { path: real }),
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
if (body.suggestion !== undefined || body.path !== undefined) {
|
|
1253
|
+
throw new HttpError(
|
|
1254
|
+
400,
|
|
1255
|
+
"invalid-body",
|
|
1256
|
+
"the selection belongs to the preview, not the confirmation",
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
const { path } = redeemConfirm(body.confirmToken, "repo-add", "");
|
|
1260
|
+
try {
|
|
1261
|
+
const stored = registerRepo(path, process.env, { requireCheckout: true });
|
|
1262
|
+
return { contractVersion: ACTIVITY_VERSION, registered: { name: basename(stored) } };
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
// registerRepo's own refusals name no path; anything else (a lock timeout,
|
|
1265
|
+
// a disk error) can, so it stays in the server's log.
|
|
1266
|
+
if (REGISTER_REFUSALS.has(err.message)) throw new HttpError(409, "not-registered", err.message);
|
|
1267
|
+
process.stderr.write(`contract-server: repo-add: ${err.message}\n`);
|
|
1268
|
+
throw new HttpError(409, "not-registered", "the repository could not be stored");
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const REGISTER_REFUSALS = new Set([
|
|
1272
|
+
"the path must be absolute",
|
|
1273
|
+
"the path is not a directory",
|
|
1274
|
+
"the home directory or the root is not a repository",
|
|
1275
|
+
"the path is not a git checkout",
|
|
1276
|
+
]);
|
|
1277
|
+
|
|
1278
|
+
// What registerRepo would refuse, checked without writing.
|
|
1279
|
+
function registerRepoCheck(path) {
|
|
1280
|
+
const real = realOr(path);
|
|
1281
|
+
let st;
|
|
1282
|
+
try {
|
|
1283
|
+
st = statSync(real);
|
|
1284
|
+
} catch {
|
|
1285
|
+
throw new Error("the path is not a directory");
|
|
1286
|
+
}
|
|
1287
|
+
if (!st.isDirectory()) throw new Error("the path is not a directory");
|
|
1288
|
+
if (real === "/" || real === realOr(homedir())) {
|
|
1289
|
+
throw new Error("the home directory or the root is not a repository");
|
|
1290
|
+
}
|
|
1291
|
+
if (!existsSync(join(real, ".git"))) throw new Error("the path is not a git checkout");
|
|
1292
|
+
return real;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** DELETE /v1/repos/{id}: a registered entry, preview then ?confirm=<token>. */
|
|
1296
|
+
function repoRemoveRoute({ params, q }) {
|
|
1297
|
+
const id = params.id;
|
|
1298
|
+
if (!REPO_ID.test(id)) throw new HttpError(404, "not-found", "no such repository");
|
|
1299
|
+
const entry = [
|
|
1300
|
+
...launchRepos()
|
|
1301
|
+
.filter((r) => r.source === "registered")
|
|
1302
|
+
.map((r) => ({ id: repoId(r.path), name: r.name })),
|
|
1303
|
+
...missingRepos().map((r) => ({ id: r.id, name: r.name })),
|
|
1304
|
+
].find((r) => r.id === id);
|
|
1305
|
+
const token = one(q, "confirm");
|
|
1306
|
+
if (token === null) {
|
|
1307
|
+
if (!entry) throw new HttpError(404, "not-found", "no registered repository has that id");
|
|
1308
|
+
return {
|
|
1309
|
+
contractVersion: ACTIVITY_VERSION,
|
|
1310
|
+
preview: { name: entry.name },
|
|
1311
|
+
...mintConfirm("repo-remove", id, null),
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
redeemConfirm(token, "repo-remove", id);
|
|
1315
|
+
const removed = unregisterRepo(id);
|
|
1316
|
+
if (!removed) throw new HttpError(404, "not-found", "no registered repository has that id");
|
|
1317
|
+
return { contractVersion: ACTIVITY_VERSION, removed: { name: basename(removed) } };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const JOB_STATUS = {
|
|
1321
|
+
"unknown-job": 404,
|
|
1322
|
+
"command-not-installed": 409,
|
|
1323
|
+
"invalid-repo": 400,
|
|
1324
|
+
"invalid-args": 400,
|
|
1325
|
+
"invalid-request": 400,
|
|
1326
|
+
"no-launch-shape": 422,
|
|
1327
|
+
"room-busy": 409,
|
|
1328
|
+
"sandbox-unavailable": 409,
|
|
1329
|
+
"launch-failed": 502,
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
function jobError(err) {
|
|
1333
|
+
if (!(err instanceof JobError)) return err;
|
|
1334
|
+
return new HttpError(JOB_STATUS[err.code] ?? 400, err.code, err.message, {
|
|
1335
|
+
...(err.extra.errors ? { errors: err.extra.errors } : {}),
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const knownRun = (id) => selectRuns({ taskId: id }).found;
|
|
1340
|
+
|
|
1341
|
+
/** A session started without agent-guard has no second layer; the office starts none. */
|
|
1342
|
+
function requireGuard(repoPath) {
|
|
1343
|
+
if (guardRegistration({ cwd: repoPath }).ok) return;
|
|
1344
|
+
throw new HttpError(
|
|
1345
|
+
409,
|
|
1346
|
+
"guard-missing",
|
|
1347
|
+
"agent-guard.sh is not registered for every tool, so nothing starts unattended",
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* POST /v1/jobs: hand a job to someone in the office. The first request names
|
|
1353
|
+
* jobId, repo and args (and answers for a pipeline run); the preview says
|
|
1354
|
+
* which command starts, with what, and what it writes outside this machine,
|
|
1355
|
+
* and mints a token. The second request sends only the token: the server then
|
|
1356
|
+
* checks the unattended profile, the workspace trust and the unattended
|
|
1357
|
+
* sandbox, starts the session in the background and records it for the floor.
|
|
1358
|
+
*/
|
|
1359
|
+
async function jobsPost({ body }) {
|
|
1360
|
+
requireBody(
|
|
1361
|
+
body,
|
|
1362
|
+
{
|
|
1363
|
+
confirmToken: isToken,
|
|
1364
|
+
jobId: (v) => typeof v === "string" && /^[a-z][a-z0-9-]{1,40}$/.test(v),
|
|
1365
|
+
repo: (v) =>
|
|
1366
|
+
typeof v === "string" && v.length <= 4096 && (isAbsolute(v) || REPO_NAME.test(v)),
|
|
1367
|
+
args: (v) => v !== null && typeof v === "object" && !Array.isArray(v),
|
|
1368
|
+
answers: (v) => v !== null && typeof v === "object" && !Array.isArray(v),
|
|
1369
|
+
},
|
|
1370
|
+
"job-request.schema.json",
|
|
1371
|
+
);
|
|
1372
|
+
try {
|
|
1373
|
+
if (body.confirmToken === undefined) {
|
|
1374
|
+
if (!body.jobId || !body.repo) {
|
|
1375
|
+
throw new HttpError(400, "invalid-body", "jobId and repo are required");
|
|
1376
|
+
}
|
|
1377
|
+
const repoPath = resolveRepo(body.repo);
|
|
1378
|
+
requireUnattendedProfile();
|
|
1379
|
+
requireGuard(repoPath);
|
|
1380
|
+
const sessions = cachedSessions();
|
|
1381
|
+
const { job, prompt } = previewJob({
|
|
1382
|
+
jobId: body.jobId,
|
|
1383
|
+
args: body.args ?? {},
|
|
1384
|
+
answers: body.answers,
|
|
1385
|
+
repoPath,
|
|
1386
|
+
knownRun,
|
|
1387
|
+
sessions,
|
|
1388
|
+
});
|
|
1389
|
+
return {
|
|
1390
|
+
contractVersion: JOBS_VERSION,
|
|
1391
|
+
preview: {
|
|
1392
|
+
jobId: job.id,
|
|
1393
|
+
role: job.role,
|
|
1394
|
+
team: job.team,
|
|
1395
|
+
repo: basename(repoPath),
|
|
1396
|
+
prompt,
|
|
1397
|
+
writes: job.writes,
|
|
1398
|
+
},
|
|
1399
|
+
...mintConfirm("job", "", {
|
|
1400
|
+
jobId: job.id,
|
|
1401
|
+
args: body.args ?? {},
|
|
1402
|
+
answers: body.answers,
|
|
1403
|
+
repoPath,
|
|
1404
|
+
}),
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
if (["jobId", "repo", "args", "answers"].some((k) => body[k] !== undefined)) {
|
|
1408
|
+
throw new HttpError(
|
|
1409
|
+
400,
|
|
1410
|
+
"invalid-body",
|
|
1411
|
+
"the job belongs to the preview, not the confirmation",
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
const p = redeemConfirm(body.confirmToken, "job", "");
|
|
1415
|
+
if (!launchRepoAllowed(p.repoPath)) throw notAllowed();
|
|
1416
|
+
requireUnattendedProfile();
|
|
1417
|
+
requireWorkspaceTrust(p.repoPath, true);
|
|
1418
|
+
requireGuard(p.repoPath);
|
|
1419
|
+
const record = await startJob({
|
|
1420
|
+
jobId: p.jobId,
|
|
1421
|
+
args: p.args,
|
|
1422
|
+
answers: p.answers,
|
|
1423
|
+
repoPath: p.repoPath,
|
|
1424
|
+
repoName: basename(p.repoPath),
|
|
1425
|
+
knownRun,
|
|
1426
|
+
sessions: cachedSessions(),
|
|
1427
|
+
precondition: sandboxPrecondition,
|
|
1428
|
+
});
|
|
1429
|
+
return {
|
|
1430
|
+
contractVersion: JOBS_VERSION,
|
|
1431
|
+
started: {
|
|
1432
|
+
sessionId: record.sessionId,
|
|
1433
|
+
jobId: record.jobId,
|
|
1434
|
+
role: record.role,
|
|
1435
|
+
team: record.team,
|
|
1436
|
+
repo: record.repo,
|
|
1437
|
+
},
|
|
1438
|
+
};
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
throw jobError(err);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1036
1444
|
function stackError(err) {
|
|
1037
1445
|
if (!(err instanceof StackError)) return err;
|
|
1038
1446
|
if (err.code === "unreadable-settings") {
|
|
@@ -1187,7 +1595,8 @@ function toolError(err) {
|
|
|
1187
1595
|
|
|
1188
1596
|
async function toolsRoute() {
|
|
1189
1597
|
try {
|
|
1190
|
-
|
|
1598
|
+
const tools = await toolkitClient().listTools();
|
|
1599
|
+
return { contractVersion: TOOLS_VERSION, version: activeToolkitVersion(), tools };
|
|
1191
1600
|
} catch (err) {
|
|
1192
1601
|
throw toolError(err);
|
|
1193
1602
|
}
|
|
@@ -1292,8 +1701,25 @@ export const ROUTES = [
|
|
|
1292
1701
|
{
|
|
1293
1702
|
method: "GET",
|
|
1294
1703
|
path: "/v1/repos",
|
|
1704
|
+
query: ["all"],
|
|
1705
|
+
handle: ({ q, redact }) => reposDocument({ redact, extras: flag(one(q, "all")) }),
|
|
1706
|
+
},
|
|
1707
|
+
{
|
|
1708
|
+
method: "POST",
|
|
1709
|
+
path: "/v1/repos",
|
|
1295
1710
|
query: [],
|
|
1296
|
-
|
|
1711
|
+
body: true,
|
|
1712
|
+
remote: "local-only",
|
|
1713
|
+
risk: "high",
|
|
1714
|
+
handle: repoAddRoute,
|
|
1715
|
+
},
|
|
1716
|
+
{
|
|
1717
|
+
method: "DELETE",
|
|
1718
|
+
path: "/v1/repos/{id}",
|
|
1719
|
+
query: ["confirm"],
|
|
1720
|
+
remote: "local-only",
|
|
1721
|
+
risk: "high",
|
|
1722
|
+
handle: repoRemoveRoute,
|
|
1297
1723
|
},
|
|
1298
1724
|
{
|
|
1299
1725
|
method: "POST",
|
|
@@ -1350,11 +1776,36 @@ export const ROUTES = [
|
|
|
1350
1776
|
risk: "high",
|
|
1351
1777
|
handle: gcRoute,
|
|
1352
1778
|
},
|
|
1779
|
+
{
|
|
1780
|
+
method: "POST",
|
|
1781
|
+
path: "/v1/prune-logs",
|
|
1782
|
+
query: [],
|
|
1783
|
+
body: true,
|
|
1784
|
+
remote: "local-only",
|
|
1785
|
+
risk: "high",
|
|
1786
|
+
handle: oneAtATime("prune-logs", pruneRoute),
|
|
1787
|
+
},
|
|
1788
|
+
{
|
|
1789
|
+
method: "GET",
|
|
1790
|
+
path: "/v1/settings",
|
|
1791
|
+
query: [],
|
|
1792
|
+
remote: "local-only",
|
|
1793
|
+
handle: () => readSettings(),
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
method: "POST",
|
|
1797
|
+
path: "/v1/settings",
|
|
1798
|
+
query: [],
|
|
1799
|
+
body: true,
|
|
1800
|
+
remote: "local-only",
|
|
1801
|
+
risk: "high",
|
|
1802
|
+
handle: oneAtATime("settings", settingsRoute),
|
|
1803
|
+
},
|
|
1353
1804
|
{
|
|
1354
1805
|
method: "GET",
|
|
1355
1806
|
path: "/v1/autopilot",
|
|
1356
1807
|
query: [],
|
|
1357
|
-
handle:
|
|
1808
|
+
handle: shared(() => "autopilot-status", autopilotGet),
|
|
1358
1809
|
},
|
|
1359
1810
|
{
|
|
1360
1811
|
method: "POST",
|
|
@@ -1390,6 +1841,35 @@ export const ROUTES = [
|
|
|
1390
1841
|
handle: chatRoute,
|
|
1391
1842
|
},
|
|
1392
1843
|
{ method: "GET", path: "/v1/ledger", query: [], handle: () => officeLedger() },
|
|
1844
|
+
{
|
|
1845
|
+
method: "GET",
|
|
1846
|
+
path: "/v1/system",
|
|
1847
|
+
query: [],
|
|
1848
|
+
handle: shared(
|
|
1849
|
+
() => "system",
|
|
1850
|
+
() => systemStatus(),
|
|
1851
|
+
),
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
method: "GET",
|
|
1855
|
+
path: "/v1/credentials",
|
|
1856
|
+
query: ["probe"],
|
|
1857
|
+
remote: "local-only",
|
|
1858
|
+
handle: shared(
|
|
1859
|
+
({ q }) => "credentials:" + (one(q, "probe") === "1"),
|
|
1860
|
+
({ q }) => officeCredentials({ probe: one(q, "probe") === "1" }),
|
|
1861
|
+
),
|
|
1862
|
+
},
|
|
1863
|
+
{
|
|
1864
|
+
method: "GET",
|
|
1865
|
+
path: "/v1/memory",
|
|
1866
|
+
query: [],
|
|
1867
|
+
remote: "local-only",
|
|
1868
|
+
handle: shared(
|
|
1869
|
+
() => "memory",
|
|
1870
|
+
() => officeMemory(),
|
|
1871
|
+
),
|
|
1872
|
+
},
|
|
1393
1873
|
{ method: "GET", path: "/v1/runs/{id}/results", query: [], handle: resultsRoute },
|
|
1394
1874
|
{ method: "GET", path: "/v1/runs/{id}/evidence/{name}", query: [], handle: evidenceRoute },
|
|
1395
1875
|
{
|
|
@@ -1405,13 +1885,19 @@ export const ROUTES = [
|
|
|
1405
1885
|
method: "GET",
|
|
1406
1886
|
path: "/v1/issues/{key}/maturity",
|
|
1407
1887
|
query: [],
|
|
1408
|
-
handle:
|
|
1888
|
+
handle: shared(({ params }) => `maturity:${params.key}`, maturityRoute, {
|
|
1889
|
+
group: "maturity",
|
|
1890
|
+
max: 2,
|
|
1891
|
+
}),
|
|
1409
1892
|
},
|
|
1410
1893
|
{
|
|
1411
1894
|
method: "GET",
|
|
1412
1895
|
path: "/v1/prs",
|
|
1413
1896
|
query: [],
|
|
1414
|
-
handle:
|
|
1897
|
+
handle: shared(
|
|
1898
|
+
({ redact }) => `prs:${redact ? 1 : 0}`,
|
|
1899
|
+
({ redact }) => pullRequests({ redact }),
|
|
1900
|
+
),
|
|
1415
1901
|
},
|
|
1416
1902
|
{ method: "GET", path: "/v1/tools", query: [], handle: toolsRoute },
|
|
1417
1903
|
{
|
|
@@ -1463,6 +1949,16 @@ export const ROUTES = [
|
|
|
1463
1949
|
handle: routineDeleteRoute,
|
|
1464
1950
|
},
|
|
1465
1951
|
{ method: "GET", path: "/v1/stack", query: ["repo"], handle: stackGet },
|
|
1952
|
+
{ method: "GET", path: "/v1/jobs", query: [], handle: () => jobsDocument() },
|
|
1953
|
+
{
|
|
1954
|
+
method: "POST",
|
|
1955
|
+
path: "/v1/jobs",
|
|
1956
|
+
query: [],
|
|
1957
|
+
body: true,
|
|
1958
|
+
remote: "local-only",
|
|
1959
|
+
risk: "high",
|
|
1960
|
+
handle: jobsPost,
|
|
1961
|
+
},
|
|
1466
1962
|
{
|
|
1467
1963
|
method: "POST",
|
|
1468
1964
|
path: "/v1/stack",
|
|
@@ -1665,14 +2161,14 @@ function parseArgs(argv) {
|
|
|
1665
2161
|
opts.port = Number(p);
|
|
1666
2162
|
} else if (a === "--idle-minutes") {
|
|
1667
2163
|
const n = Number(value());
|
|
1668
|
-
if (!Number.isFinite(n) || n
|
|
1669
|
-
throw new Error(`--idle-minutes is
|
|
2164
|
+
if (!Number.isFinite(n) || n < 0 || n > MAX_IDLE_MINUTES) {
|
|
2165
|
+
throw new Error(`--idle-minutes is 0 (never) to ${MAX_IDLE_MINUTES}`);
|
|
1670
2166
|
}
|
|
1671
2167
|
opts.idleMinutes = n;
|
|
1672
2168
|
} else if (a === "--max-hours") {
|
|
1673
2169
|
const n = Number(value());
|
|
1674
|
-
if (!Number.isFinite(n) || n
|
|
1675
|
-
throw new Error(`--max-hours is
|
|
2170
|
+
if (!Number.isFinite(n) || n < 0 || n > MAX_HOURS) {
|
|
2171
|
+
throw new Error(`--max-hours is 0 (no limit) to ${MAX_HOURS}`);
|
|
1676
2172
|
}
|
|
1677
2173
|
opts.maxHours = n;
|
|
1678
2174
|
} else if (a === "--allow-origin") {
|
|
@@ -1717,6 +2213,25 @@ export const OFFICE_PATH = "/office";
|
|
|
1717
2213
|
export const OFFICE_CODE_PATH = "/office/code";
|
|
1718
2214
|
/** No bearer, own Origin only: trades a panel code for the token, once. */
|
|
1719
2215
|
export const OFFICE_EXCHANGE_PATH = "/office/exchange";
|
|
2216
|
+
/** Bearer, own Origin or none: the installed page's build and whether this server is stale. */
|
|
2217
|
+
export const OFFICE_VERSION_PATH = "/office/version";
|
|
2218
|
+
/** Bearer, own Origin or none: a fresh panel code, so the page can reload itself. */
|
|
2219
|
+
export const OFFICE_RELOAD_PATH = "/office/reload";
|
|
2220
|
+
|
|
2221
|
+
const serverDigest = () => codeDigest(dirname(fileURLToPath(import.meta.url)));
|
|
2222
|
+
/** The server's code (scripts/ and lib/) as it was at start; a reinstall changes the files, not the process. */
|
|
2223
|
+
const SERVER_DIGEST = serverDigest();
|
|
2224
|
+
|
|
2225
|
+
/** The build id office-build.mjs stamps into the installed page, or null. */
|
|
2226
|
+
export function officeBuild(env = process.env) {
|
|
2227
|
+
const file = officePage(env);
|
|
2228
|
+
if (!file) return null;
|
|
2229
|
+
try {
|
|
2230
|
+
return /const BUILD = "([0-9a-f]{12})";/.exec(readFileSync(file, "utf8"))?.[1] ?? null;
|
|
2231
|
+
} catch {
|
|
2232
|
+
return null;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
1720
2235
|
|
|
1721
2236
|
function readPrefs(env) {
|
|
1722
2237
|
try {
|
|
@@ -1949,7 +2464,7 @@ export function serve(opts) {
|
|
|
1949
2464
|
};
|
|
1950
2465
|
const arm = () => {
|
|
1951
2466
|
clearTimeout(idleTimer);
|
|
1952
|
-
idleTimer = setTimeout(() => stop("idle"), idleMs);
|
|
2467
|
+
if (idleMs > 0) idleTimer = setTimeout(() => stop("idle"), idleMs);
|
|
1953
2468
|
};
|
|
1954
2469
|
|
|
1955
2470
|
/**
|
|
@@ -2049,6 +2564,15 @@ export function serve(opts) {
|
|
|
2049
2564
|
serveOffice(res, req.method === "HEAD");
|
|
2050
2565
|
return;
|
|
2051
2566
|
}
|
|
2567
|
+
if (url.pathname === "/favicon.ico" && (req.method === "GET" || req.method === "HEAD")) {
|
|
2568
|
+
drain(req);
|
|
2569
|
+
res.writeHead(204, {
|
|
2570
|
+
"Cache-Control": "max-age=86400",
|
|
2571
|
+
"X-Content-Type-Options": "nosniff",
|
|
2572
|
+
});
|
|
2573
|
+
res.end();
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2052
2576
|
if (url.pathname.startsWith(OFFICE_ASSET_PREFIX)) {
|
|
2053
2577
|
drain(req);
|
|
2054
2578
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
@@ -2095,6 +2619,30 @@ export function serve(opts) {
|
|
|
2095
2619
|
if (!authorized(req.headers.authorization, tokenDigest)) {
|
|
2096
2620
|
throw new HttpError(401, "unauthorized", "a bearer token is required");
|
|
2097
2621
|
}
|
|
2622
|
+
if (url.pathname === OFFICE_VERSION_PATH || url.pathname === OFFICE_RELOAD_PATH) {
|
|
2623
|
+
drain(req);
|
|
2624
|
+
if (origin !== undefined && !own) {
|
|
2625
|
+
throw new HttpError(403, "forbidden-origin", "only the panel page asks this");
|
|
2626
|
+
}
|
|
2627
|
+
if (url.pathname === OFFICE_VERSION_PATH) {
|
|
2628
|
+
if (req.method !== "GET") {
|
|
2629
|
+
throw new HttpError(405, "method-not-allowed", "use GET", { allow: "GET" });
|
|
2630
|
+
}
|
|
2631
|
+
send(res, 200, {
|
|
2632
|
+
contractVersion: CONTRACT_VERSION,
|
|
2633
|
+
build: officeBuild(),
|
|
2634
|
+
serverStale: serverDigest() !== SERVER_DIGEST,
|
|
2635
|
+
});
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
if (req.method !== "POST") {
|
|
2639
|
+
throw new HttpError(405, "method-not-allowed", "use POST", { allow: "POST" });
|
|
2640
|
+
}
|
|
2641
|
+
// The page already holds the token; a code lets it reload itself onto
|
|
2642
|
+
// the page now installed without the token ever entering a URL.
|
|
2643
|
+
send(res, 200, { contractVersion: CONTRACT_VERSION, ...mintCode() });
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2098
2646
|
if (url.pathname === OFFICE_CODE_PATH) {
|
|
2099
2647
|
drain(req);
|
|
2100
2648
|
if (req.method !== "POST") {
|
|
@@ -2227,10 +2775,9 @@ export function serve(opts) {
|
|
|
2227
2775
|
tokenFile = writeTokenFile({ ...record, token, startedAt: started.toISOString() });
|
|
2228
2776
|
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) process.on(sig, () => stop(sig));
|
|
2229
2777
|
arm();
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
Math.round(
|
|
2233
|
-
);
|
|
2778
|
+
const maxHours = opts.maxHours ?? DEFAULT_MAX_HOURS;
|
|
2779
|
+
if (maxHours > 0)
|
|
2780
|
+
lifeTimer = setTimeout(() => stop("lifetime"), Math.round(maxHours * 3600000));
|
|
2234
2781
|
resolve({ ...record, tokenFile, ...(opts.printToken ? { token } : {}) });
|
|
2235
2782
|
});
|
|
2236
2783
|
});
|