heyquilt 0.3.90 → 0.3.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -11
- package/RELEASES.md +135 -2
- package/bin/quilt.js +45 -12
- package/package.json +3 -3
- package/src/account.js +28 -0
- package/src/agents/claude-code.js +11 -4
- package/src/agents/cursor-transcripts.js +11 -4
- package/src/ai-vm-signin.js +14 -2
- package/src/ai-vm.js +150 -8
- package/src/api/agent-auth.js +9 -3
- package/src/api/fly-machines.js +72 -15
- package/src/api/memory-store.js +142 -6
- package/src/api/relay-cluster.js +337 -43
- package/src/api/relay-machines.js +35 -3
- package/src/api/releases.js +87 -0
- package/src/api/routes/agents.js +111 -16
- package/src/api/routes/ai-vm.js +146 -19
- package/src/api/routes/orgs.js +3 -1
- package/src/api/routes/secrets.js +448 -0
- package/src/api/routes/workspaces.js +8 -1
- package/src/api/sandbox-keys.js +57 -0
- package/src/api/secrets-access.js +93 -0
- package/src/api/server.js +40 -9
- package/src/api/supabase-store.js +121 -11
- package/src/autowork.js +3 -15
- package/src/catchup.js +1 -1
- package/src/clash.js +10 -22
- package/src/control.js +18 -15
- package/src/fsutil.js +11 -1
- package/src/gitstate.js +29 -5
- package/src/hooks.js +1 -1
- package/src/mcp-proxy.js +7 -2
- package/src/mcp.js +22 -25
- package/src/merge3.js +94 -12
- package/src/pass-source.js +9 -2
- package/src/pathrules.js +7 -0
- package/src/relay-clash.js +101 -484
- package/src/relay-githttp.js +36 -7
- package/src/relay-mcp.js +19 -7
- package/src/relay-metrics.js +66 -1
- package/src/relay-outbound.js +154 -25
- package/src/relay-sync.js +133 -136
- package/src/relay-turn-sync.js +250 -51
- package/src/relay-upstream.js +49 -179
- package/src/releases.js +16 -9
- package/src/runner.js +3 -2
- package/src/secret-import.js +55 -0
- package/src/secret-names.js +26 -0
- package/src/secret-run.js +80 -0
- package/src/secrets-client.js +56 -0
- package/src/server.js +71 -44
- package/src/session.js +347 -262
- package/src/status.js +3 -5
- package/src/sync-words.js +1 -1
- package/src/turn-sync-plan.js +13 -10
- package/src/turn-sync.js +32 -11
- package/src/ui/agent-guide.js +4 -4
- package/src/ui/app.css +208 -23
- package/src/ui/app.js +4 -0
- package/src/ui/cloud-ai-model.js +3 -2
- package/src/ui/common.js +2 -0
- package/src/ui/fileview.js +28 -1
- package/src/ui/home.js +32 -33
- package/src/ui/loom.css +18 -2
- package/src/ui/loom.js +41 -8
- package/src/ui/rail-model.js +6 -9
- package/src/ui/releases.js +1 -1
- package/src/ui/sandbox-keys-model.js +53 -0
- package/src/ui/sandbox-keys.js +132 -0
- package/src/ui/secret-rules.js +255 -0
- package/src/ui/secrets-view.js +162 -0
- package/src/ui/secrets.js +283 -0
- package/src/ui/session.js +47 -149
- package/src/ui/tree.js +23 -18
- package/src/ui/workspaces.js +5 -0
- package/src/ui-server.js +140 -28
- package/src/update-check.js +1 -1
- package/src/upstream.js +75 -38
- package/src/vault/crypto.js +96 -0
- package/src/vault/kms.js +61 -0
- package/src/vault/main.js +33 -0
- package/src/vault/server.js +279 -0
- package/src/vault/sigv4.js +38 -0
- package/src/ui/merges.js +0 -158
package/src/server.js
CHANGED
|
@@ -60,12 +60,12 @@ import { leaseKeeper, LEASE_HEARTBEAT_MS } from './relay-lease.js'
|
|
|
60
60
|
import { bringInBranch, BACKSTOP_MS, ACTIVE_BACKSTOP_MS, syncStatus, markFolderBrought } from './relay-sync.js'
|
|
61
61
|
import { noteCiResult } from './relay-ci.js'
|
|
62
62
|
import { outboundFor, replayBundle, liveSyncOn, adoptBranch, receiveAgentPush } from './relay-outbound.js'
|
|
63
|
-
import { ensureView, syncViewRefs, serveGit, parseUpdates, viewRepoOf, viewDir } from './relay-githttp.js'
|
|
63
|
+
import { ensureView, syncViewRefs, serveGit, parseUpdates, viewRepoOf, viewDir, gitHttpBackendWorks } from './relay-githttp.js'
|
|
64
64
|
import { relayFileKey } from './relay-filekey.js'
|
|
65
|
-
import { RoomMeter, roomDiskBytes } from './relay-metrics.js'
|
|
65
|
+
import { RoomMeter, roomDiskBytes, MachineMeter, machineLoad } from './relay-metrics.js'
|
|
66
66
|
import { clusterConfig, clusterProblems, Cluster, NOT_ROOMS, roomOfPath, replaySource, canReplay, displaceRoom, packRoom, unpackRoom, placeStaged, removeLocalRoom, MOVING, ELSEWHERE } from './relay-cluster.js'
|
|
67
67
|
import { joiningRoom, sessionArg } from './mcp-room.js'
|
|
68
|
-
import { clashAction,
|
|
68
|
+
import { clashAction, noteMergeChecks } from './relay-clash.js'
|
|
69
69
|
import { AGENT_COMMITS } from './github-commit.js'
|
|
70
70
|
import { UPSTREAM_CHECK_MS, SYNC_EVERY_MS, SYNC_EVERY_GIT_MS, upstreamsOf, noteFolders, relayEntry, relayLooks, dueBranches, checkBranch, folderOn, connectedOn, hostedOn, validToken, folderAble, catcherAble, noteHolds, connectedAs, describeRelayCheck, loadBranch, parseRepo, configureGithubBudget, CLASH_IDLE_MS, noteConnection } from './relay-upstream.js'
|
|
71
71
|
|
|
@@ -142,6 +142,9 @@ export function relayConfig (opts = {}) {
|
|
|
142
142
|
// GitHub as a node (relay-sync.js): commits come in through the relay's git mirror, on by default; QUILT_GIT_SYNC=0 keeps the REST path.
|
|
143
143
|
gitSync: opts.gitSync ?? env.QUILT_GIT_SYNC !== '0',
|
|
144
144
|
gitRemote: typeof opts.gitRemote === 'function' ? opts.gitRemote : null, // tests: a repository slug -> where its git is
|
|
145
|
+
// Agents' folders use git through the relay (relay-githttp.js); off when this machine's git has no http-backend.
|
|
146
|
+
gitHttp: opts.gitHttp !== false,
|
|
147
|
+
gitHttpCommand: typeof opts.gitHttpCommand === 'string' ? opts.gitHttpCommand : 'git', // tests: a git without http-backend
|
|
145
148
|
syncAuto: opts.syncAuto === true, // the relay's own commit timer: off; commits happen when someone asks for one (quilt_git_commit, a git push)
|
|
146
149
|
crashAt: typeof opts.crashAt === 'string' ? opts.crashAt : null, // tests: where to inject a crash (relay-sync.js crashPoint)
|
|
147
150
|
leaseTtlMs: num(opts.leaseTtlMs, undefined),
|
|
@@ -719,8 +722,7 @@ class Room {
|
|
|
719
722
|
mayBox (ws, rel) {
|
|
720
723
|
const a = this.access.get(ws)
|
|
721
724
|
rel = String(rel ?? '').trim().replace(/^\.\//, '')
|
|
722
|
-
|
|
723
|
-
return !(a && a.kind === 'agent' && this.cfg.gitSync && ws.branch && heldForClash(this, this.resolveKey(ws.branch), rel))
|
|
725
|
+
return !(a && !this.mayWrite(a, rel))
|
|
724
726
|
}
|
|
725
727
|
|
|
726
728
|
/**
|
|
@@ -778,8 +780,6 @@ class Room {
|
|
|
778
780
|
}
|
|
779
781
|
}
|
|
780
782
|
for (const [rel, types] of touched) {
|
|
781
|
-
// A file held for a clash with GitHub (relay-clash.js): agents propose the merge with quilt_clash; only people edit it in place.
|
|
782
|
-
if (d.key && a && a.kind === 'agent' && this.cfg.gitSync && heldForClash(this, d.key, rel)) { refused.push(`${rel} (held for a clash: propose with quilt_clash)`); for (const t of types) undo.add(t); continue }
|
|
783
783
|
if (this.mayWrite(a, rel)) continue
|
|
784
784
|
refused.push(rel)
|
|
785
785
|
for (const t of types) undo.add(t)
|
|
@@ -1603,20 +1603,6 @@ class Room {
|
|
|
1603
1603
|
* field and find it on their next look), and the relay checks GitHub itself for a branch no
|
|
1604
1604
|
* folder can bring in. Returns whether any branch follows it.
|
|
1605
1605
|
*/
|
|
1606
|
-
/** The open clashes on every branch (relay-clash.js): timeouts and hand-offs, about once a minute, one branch at a time in its queue. */
|
|
1607
|
-
async reviewClashTick () {
|
|
1608
|
-
if (!this.cfg.gitSync || !this.mirrors || !this.meta.sync) return []
|
|
1609
|
-
const out = []
|
|
1610
|
-
for (const [key, st] of Object.entries(this.meta.sync)) {
|
|
1611
|
-
if (!st || !Array.isArray(st.held) || !st.held.some((h) => h.why === 'clash')) continue
|
|
1612
|
-
const rec = upstreamsOf(this)[key]
|
|
1613
|
-
const repo = rec && parseRepo(`https://${rec.repo}`)
|
|
1614
|
-
if (!repo) continue
|
|
1615
|
-
out.push(...await this.mirrors.queue(repo.slug, rec.ref, () => reviewClashes(this, key)))
|
|
1616
|
-
}
|
|
1617
|
-
return out
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
1606
|
/** Whether session `other` has this session's owner in it (its owner or a member): then the app may offer to open it ("Open the live session", plan §8). */
|
|
1621
1607
|
sameOwner (other) {
|
|
1622
1608
|
if (!other || !this.meta.ownerSub) return false
|
|
@@ -1667,7 +1653,7 @@ class Room {
|
|
|
1667
1653
|
const want = String(repo).toLowerCase().replace(/^github\.com\//, '')
|
|
1668
1654
|
const follows = Object.values(upstreamsOf(this)).some((rec) => rec && rec.repo && String(rec.repo).toLowerCase().replace(/^github\.com\//, '') === want)
|
|
1669
1655
|
if (!follows) return false
|
|
1670
|
-
// Checks on a commit the relay pushed with merged files in it (relay-clash.js): failing
|
|
1656
|
+
// Checks on a commit the relay pushed with merged files in it (relay-clash.js): failing tells both parties; nothing is held.
|
|
1671
1657
|
if (this.cfg.gitSync && this.mirrors && /^[0-9a-f]{40}$/.test(sha)) {
|
|
1672
1658
|
for (const [key, rec] of Object.entries(upstreamsOf(this))) {
|
|
1673
1659
|
if (!rec || String(rec.repo).toLowerCase().replace(/^github\.com\//, '') !== want || (branch && rec.ref !== branch && !(rec.ref && branch === `quilt/live/${rec.ref}`))) continue
|
|
@@ -2868,16 +2854,11 @@ class Room {
|
|
|
2868
2854
|
.then((r) => { if (this.conns.has(ws)) send(ws, jsonMessage(MSG_MEMBERS, { members: this.memberList(), sessionName: this.meta.name || '', admitBy: this.admitBy, pending: this.personCanAdmit(this.access.get(ws)) ? this.pendingList() : [], reply: r })) })
|
|
2869
2855
|
return
|
|
2870
2856
|
}
|
|
2871
|
-
//
|
|
2857
|
+
// Sent by apps before 0.3.99 for offline edits that clashed with the session's: Quilt merges those on its own now
|
|
2858
|
+
// (session.js autoSettle), so there is nothing to hold. Answered ok so an older app doesn't retry.
|
|
2872
2859
|
if (req.op === 'offlineClash') {
|
|
2873
|
-
const
|
|
2874
|
-
|
|
2875
|
-
if (!a || (a.state && a.state !== 'approved') || !ws.branch || a.role === 'viewer') throw new Error('not in this session')
|
|
2876
|
-
if (!this.cfg.gitSync || !this.mirrors) throw new Error('This session doesn\'t sync live with GitHub.')
|
|
2877
|
-
const text = (v) => typeof v === 'string' && v.length <= 1024 * 1024 ? v : null
|
|
2878
|
-
openOfflineClash(this, ws.branch, { who: { name: a.name }, path: String(req.path || ''), mergeId: typeof req.mergeId === 'string' ? req.mergeId.slice(0, 40) : null, base: text(req.base), ours: text(req.ours), oursDeleted: req.oursDeleted === true, binary: req.binary === true })
|
|
2879
|
-
.then((h) => ({ id, ok: true, offlineClash: { task: h.task, merger: h.merger, reviewer: h.reviewer } }), (err) => ({ id, ok: false, error: err.message }))
|
|
2880
|
-
.then((r) => { this.broadcastBranches(); if (this.conns.has(ws)) send(ws, jsonMessage(MSG_MEMBERS, { members: this.memberList(), sessionName: this.meta.name || '', admitBy: this.admitBy, pending: this.personCanAdmit(this.access.get(ws)) ? this.pendingList() : [], reply: r })) })
|
|
2860
|
+
const r = { id: req.id, ok: true, offlineClash: { task: null, merger: null, reviewer: null } }
|
|
2861
|
+
if (this.conns.has(ws)) send(ws, jsonMessage(MSG_MEMBERS, { members: this.memberList(), sessionName: this.meta.name || '', admitBy: this.admitBy, pending: this.personCanAdmit(this.access.get(ws)) ? this.pendingList() : [], reply: r }))
|
|
2881
2862
|
return
|
|
2882
2863
|
}
|
|
2883
2864
|
// A person adopts an agent's branch into theirs (addendum 16.7; relay-outbound.js adoptBranch).
|
|
@@ -2896,7 +2877,6 @@ class Room {
|
|
|
2896
2877
|
const id = req.id
|
|
2897
2878
|
const a = this.access.get(ws)
|
|
2898
2879
|
if (!a || (a.state && a.state !== 'approved') || !ws.branch) throw new Error('not in this session')
|
|
2899
|
-
if (!this.cfg.gitSync || !this.mirrors) throw new Error('This session doesn\'t sync live with GitHub, so there are no held clashes here.')
|
|
2900
2880
|
clashAction(this, { who: { name: a.name, kind: a.kind === 'agent' ? 'agent' : 'human', access: a }, key: ws.branch, action: String(req.action || ''), path: String(req.path || ''), content: typeof req.content === 'string' ? req.content : null, checks: req.checks && typeof req.checks === 'object' ? req.checks : null, force: req.force === true })
|
|
2901
2881
|
.then((out) => out.ok ? { id, ok: true, clash: out, text: out.text } : { id, ok: false, error: out.text }, (err) => ({ id, ok: false, error: err.message }))
|
|
2902
2882
|
.then((r) => { if (this.conns.has(ws)) send(ws, jsonMessage(MSG_MEMBERS, { members: this.memberList(), sessionName: this.meta.name || '', admitBy: this.admitBy, pending: this.personCanAdmit(this.access.get(ws)) ? this.pendingList() : [], reply: r })) })
|
|
@@ -3042,6 +3022,11 @@ export function startServer ({ port = 4321, host = '0.0.0.0', dataDir = null, lo
|
|
|
3042
3022
|
// Without sign-in the limit is per address, and the message says so (as before).
|
|
3043
3023
|
const TOO_MANY = passKey ? 'too many new sessions; try again later' : 'too many new sessions from this address; try again later'
|
|
3044
3024
|
if (dataDir) fs.mkdirSync(dataDir, { recursive: true })
|
|
3025
|
+
// Without git http-backend every agent's git request would fail with a 500: agents' folders use GitHub's git instead.
|
|
3026
|
+
if (cfg.gitSync && cfg.gitHttp && passKey && !gitHttpBackendWorks(cfg.gitHttpCommand)) {
|
|
3027
|
+
cfg.gitHttp = false
|
|
3028
|
+
log('⚠️ this machine\'s git has no http-backend (on Alpine it comes with git-daemon): agents\' folders can\'t use git through the relay, so they use GitHub\'s')
|
|
3029
|
+
}
|
|
3045
3030
|
// The relay's git mirrors (relay-git.js): one bare repository per GitHub repository, one writer per branch.
|
|
3046
3031
|
const mirrors = mirrorManager({ dataDir, log, ...(cfg.pushGapMs !== undefined ? { pushGapMs: cfg.pushGapMs } : {}), ...(cfg.pushesPerHour !== undefined ? { pushesPerHour: cfg.pushesPerHour } : {}) })
|
|
3047
3032
|
// One live session per repository and branch (relay-lease.js): in the accounts API when this relay has one, else in this process.
|
|
@@ -3337,14 +3322,30 @@ export function startServer ({ port = 4321, host = '0.0.0.0', dataDir = null, lo
|
|
|
3337
3322
|
return n
|
|
3338
3323
|
}
|
|
3339
3324
|
/** How busy and how big a loaded room is (relay-metrics.js): for /stats and the accounts API's placement. */
|
|
3340
|
-
|
|
3341
|
-
const
|
|
3325
|
+
// Git pushes into a room's view (/git/<room>.git) being received now.
|
|
3326
|
+
const gitPushes = new Map() // room -> count
|
|
3327
|
+
/** Git work running or queued for a room now: pushes being received, and commits, pushes and bring-ins on its branches (relay-git.js). */
|
|
3328
|
+
const gitBusy = (room) => {
|
|
3329
|
+
let n = gitPushes.get(room.name) || 0
|
|
3330
|
+
for (const rec of Object.values((room.meta && room.meta.upstreams) || {})) {
|
|
3331
|
+
const repo = rec && rec.repo && rec.ref ? parseRepo(`https://${rec.repo}`) : null
|
|
3332
|
+
if (repo) n += mirrors.queue.pending(repo.slug, rec.ref)
|
|
3333
|
+
}
|
|
3334
|
+
return n
|
|
3335
|
+
}
|
|
3336
|
+
const roomStats = (room) => ({ room: room.name, conns: room.conns.size, hosted: room.hostedSeen.size, ...room.meter.lastMinute(), docBytes: room.totalBytes(), diskBytes: diskOf(room.name), git: gitBusy(room) })
|
|
3337
|
+
// The machine's own load (memory, event-loop lag, CPU), a minute at a time: for scaling the cluster on load.
|
|
3338
|
+
const machineMeter = new MachineMeter()
|
|
3339
|
+
/** The machine: rooms loaded, connections, memory, lag, CPU and every room's traffic (`list`: the rooms' stats, when already made). */
|
|
3340
|
+
const machineStats = (list = null) => {
|
|
3342
3341
|
const mem = process.memoryUsage()
|
|
3343
|
-
|
|
3342
|
+
const traffic = list || [...rooms.values()].filter((r) => !r.ended).map((r) => r.meter.lastMinute())
|
|
3343
|
+
return { rooms: rooms.size, conns: wss.clients.size, rss: mem.rss, heapUsed: mem.heapUsed, load: os.loadavg()[0], uptime: Math.round(process.uptime()), ...machineLoad({ meter: machineMeter, rooms: traffic, mem }) }
|
|
3344
3344
|
}
|
|
3345
3345
|
const stats = () => {
|
|
3346
3346
|
for (const name of diskBytes.keys()) if (!rooms.has(name)) diskBytes.delete(name)
|
|
3347
|
-
|
|
3347
|
+
const list = [...rooms.values()].filter((r) => !r.ended).map(roomStats)
|
|
3348
|
+
return { machine: machineStats(list), rooms: list }
|
|
3348
3349
|
}
|
|
3349
3350
|
/** Closes a loaded room on this machine without ending it: its people reconnect (to wherever it is now). */
|
|
3350
3351
|
const unloadForMove = (name) => {
|
|
@@ -3519,13 +3520,22 @@ export function startServer ({ port = 4321, host = '0.0.0.0', dataDir = null, lo
|
|
|
3519
3520
|
})
|
|
3520
3521
|
}
|
|
3521
3522
|
// An org or workspace changed what agents may commit (the accounts API): every open session asks again now.
|
|
3523
|
+
// With { room }, only that session (one just put in or taken out of a workspace), if this machine holds it.
|
|
3522
3524
|
if (url.pathname === '/github/policy' && req.method === 'POST') {
|
|
3523
3525
|
const auth = String(req.headers.authorization || '').replace(/^Bearer\s+/i, '')
|
|
3524
3526
|
const want = cfg.relayApiSecret || ''
|
|
3525
3527
|
const same = want && auth.length === want.length && crypto.timingSafeEqual(Buffer.from(auth), Buffer.from(want))
|
|
3526
3528
|
if (!same) return json(401, { error: 'only the accounts API may say this' })
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
+
return readJson(req, 4 * 1024, (err, b) => {
|
|
3530
|
+
const one = !err && b && typeof b.room === 'string' && b.room ? b.room : ''
|
|
3531
|
+
if (one) {
|
|
3532
|
+
const room = ROOM_RE.test(one) ? rooms.get(one) : null
|
|
3533
|
+
if (room) room.refreshPolicy()
|
|
3534
|
+
return json(200, { ok: true, rooms: room ? 1 : 0 })
|
|
3535
|
+
}
|
|
3536
|
+
for (const room of rooms.values()) room.refreshPolicy()
|
|
3537
|
+
json(200, { ok: true, rooms: rooms.size })
|
|
3538
|
+
})
|
|
3529
3539
|
}
|
|
3530
3540
|
// Someone connected GitHub (the API's callback): every session they are in checks its repository again now.
|
|
3531
3541
|
if (url.pathname === '/github/connected' && req.method === 'POST') {
|
|
@@ -3559,7 +3569,14 @@ export function startServer ({ port = 4321, host = '0.0.0.0', dataDir = null, lo
|
|
|
3559
3569
|
if (req.method !== 'POST') return text(405, 'method not allowed')
|
|
3560
3570
|
return readJson(req, 4096, (err, b) => {
|
|
3561
3571
|
if (err) return json(400, { error: err.message })
|
|
3562
|
-
if (action === 'release') {
|
|
3572
|
+
if (action === 'release') {
|
|
3573
|
+
// Never mid git push or commit: the API tries again later (or another room).
|
|
3574
|
+
const loaded = rooms.get(name)
|
|
3575
|
+
const busy = loaded ? gitBusy(loaded) : gitPushes.get(name) || 0
|
|
3576
|
+
if (busy) return json(409, { error: `${name} has git work running (${busy}); try again later` })
|
|
3577
|
+
letRoomGo(name)
|
|
3578
|
+
return json(200, { ok: true })
|
|
3579
|
+
}
|
|
3563
3580
|
if (action === 'restore') { cluster.restore(name); log(`cluster: ${name} stays on this machine`); return json(200, { ok: true }) }
|
|
3564
3581
|
if (action === 'forget') { forgetRoomHere(name, String((b && b.to) || '')); return json(200, { ok: true }) }
|
|
3565
3582
|
// fetch
|
|
@@ -3618,7 +3635,7 @@ export function startServer ({ port = 4321, host = '0.0.0.0', dataDir = null, lo
|
|
|
3618
3635
|
// branches at /git/<room>.git, signed in with the member's Quilt pass (git's password; quilt git-credential).
|
|
3619
3636
|
const gm = url.pathname.match(/^\/git\/([A-Za-z0-9_-]{1,64})\.git(\/info\/refs|\/git-upload-pack|\/git-receive-pack)$/)
|
|
3620
3637
|
if (gm) {
|
|
3621
|
-
if (!cfg.gitSync || !passKey || !dataDir) return text(404, 'this relay serves no git')
|
|
3638
|
+
if (!cfg.gitSync || !cfg.gitHttp || !passKey || !dataDir) return text(404, 'this relay serves no git')
|
|
3622
3639
|
const name = gm[1]
|
|
3623
3640
|
const basic = /^Basic\s+([A-Za-z0-9+/=]+)$/i.exec(String(req.headers.authorization || ''))
|
|
3624
3641
|
const decoded = basic ? Buffer.from(basic[1], 'base64').toString('utf8') : ''
|
|
@@ -3629,6 +3646,11 @@ export function startServer ({ port = 4321, host = '0.0.0.0', dataDir = null, lo
|
|
|
3629
3646
|
if (!room.letIn(pass)) return text(403, NOT_LET_IN)
|
|
3630
3647
|
const service = gm[2] === '/info/refs' ? url.searchParams.get('service') : gm[2].slice(1)
|
|
3631
3648
|
if (service === 'git-receive-pack') { const a = room.httpAccess(pass); if (a && a.role === 'viewer') return text(403, 'you can only view this session, so you can\'t push') }
|
|
3649
|
+
if (gm[2] === '/git-receive-pack') {
|
|
3650
|
+
// A push being received: the room is not moved to another machine until it is done.
|
|
3651
|
+
gitPushes.set(name, (gitPushes.get(name) || 0) + 1)
|
|
3652
|
+
res.once('close', () => { const n = (gitPushes.get(name) || 1) - 1; if (n > 0) gitPushes.set(name, n); else gitPushes.delete(name) })
|
|
3653
|
+
}
|
|
3632
3654
|
const def = viewRepoOf(room)
|
|
3633
3655
|
if (!def) { log(`[${name}] git over http: no GitHub repository (default ${room.defaultKey}: ${JSON.stringify(upstreamsOf(room)[room.defaultKey] || null)})`); return text(404, 'this session follows no GitHub repository') }
|
|
3634
3656
|
const repo = parseRepo(`https://${def.repo}`)
|
|
@@ -3637,7 +3659,7 @@ export function startServer ({ port = 4321, host = '0.0.0.0', dataDir = null, lo
|
|
|
3637
3659
|
if (!(await runGit(m.dir, ['rev-parse', '--verify', '-q', `refs/remotes/origin/${def.ref}`])).out.trim()) await room.mirrors.fetch(m.dir, def.ref, { token: await room.gitToken(repo, { access: 'read' }) }).catch(() => {})
|
|
3638
3660
|
const dir = await ensureView(dataDir, name, m.dir)
|
|
3639
3661
|
await syncViewRefs(room, dir, def.repo)
|
|
3640
|
-
serveGit(req, res, { root: path.dirname(dir), pathInfo: `/${name}.git${gm[2]}`, query: url.search.slice(1), env: { QUILT_HOOK_URL: `http://127.0.0.1:${httpServer.address().port}/git-hook`, QUILT_HOOK_TOKEN: hookToken, QUILT_ROOM: name, QUILT_WHO: `${pass.kind}:${pass.sub}`, QUILT_WHO_NAME: String(pass.name || '') } })
|
|
3662
|
+
serveGit(req, res, { root: path.dirname(dir), pathInfo: `/${name}.git${gm[2]}`, query: url.search.slice(1), env: { QUILT_HOOK_URL: `http://127.0.0.1:${httpServer.address().port}/git-hook`, QUILT_HOOK_TOKEN: hookToken, QUILT_ROOM: name, QUILT_WHO: `${pass.kind}:${pass.sub}`, QUILT_WHO_NAME: String(pass.name || '') }, git: cfg.gitHttpCommand, onError: (why) => log(`[${name}] git over http (${req.method} ${gm[2]}): ${why}`) })
|
|
3641
3663
|
})().catch((err) => { log(`[${name}] git over http: ${err.message}`); if (!res.headersSent) text(500, 'the relay could not serve git right now') })
|
|
3642
3664
|
return
|
|
3643
3665
|
}
|
|
@@ -3683,7 +3705,9 @@ export function startServer ({ port = 4321, host = '0.0.0.0', dataDir = null, lo
|
|
|
3683
3705
|
return { index: { rooms: [...idx.rooms] }, noteHosted: (o) => cluster.hostedRoom(account, o), shareWebhook: (w) => cluster.hostedWebhook(account, w || null) }
|
|
3684
3706
|
})
|
|
3685
3707
|
: Promise.resolve({})
|
|
3686
|
-
|
|
3708
|
+
// Secrets' names, through the accounts API as the agent (its pass).
|
|
3709
|
+
const secrets = cfg.apiUrl && pass.kind === 'agent' ? { apiUrl: cfg.apiUrl, pass: String(req.headers['x-quilt-pass']), fetch: apiFetch } : null
|
|
3710
|
+
return shared.then((more) => handleHostedMcp({ req, res, pass, workspaces, secrets, body: parsed, relay: { getRoom, roomEnded, refused, hosted, saveHosted, webhooks, log, endedMessage: ENDED_MESSAGE, updates, visitStart: hostedVisitStart, visitEnd: hostedVisitEnd, ...more } })
|
|
3687
3711
|
// Its first session on this machine: the webhook it set elsewhere applies here too.
|
|
3688
3712
|
.then(() => { if (more.index && cluster.hosted.get(account)) applySharedWebhook(account, cluster.hosted.get(account).webhook) }))
|
|
3689
3713
|
.catch((err) => { log(`mcp error: ${err.message}`); if (!res.headersSent) text(500, 'mcp error') })
|
|
@@ -3915,7 +3939,9 @@ export function startServer ({ port = 4321, host = '0.0.0.0', dataDir = null, lo
|
|
|
3915
3939
|
}
|
|
3916
3940
|
})
|
|
3917
3941
|
reportTicker = setInterval(() => {
|
|
3918
|
-
|
|
3942
|
+
machineMeter.roll()
|
|
3943
|
+
const list = [...rooms.values()].filter((r) => !r.ended).map(roomStats)
|
|
3944
|
+
cluster.report(machineStats(list), list).catch((err) => log(`cluster: could not report this machine's minute: ${err.message}`))
|
|
3919
3945
|
// Hosted agents in loaded rooms: a webhook set or removed on another machine reaches this one within a minute.
|
|
3920
3946
|
let n = 0
|
|
3921
3947
|
for (const [id, h] of hosted) if (n < 200 && h.rooms && Object.keys(h.rooms).some((r) => rooms.has(r))) { n++; syncHosted(id) }
|
|
@@ -3965,7 +3991,7 @@ export function startServer ({ port = 4321, host = '0.0.0.0', dataDir = null, lo
|
|
|
3965
3991
|
sweeper.unref()
|
|
3966
3992
|
// Branches only hosted agents work on, with no folder online: the relay looks for new commits on GitHub itself.
|
|
3967
3993
|
const upstreamTicker = cfg.upstreamAuto && cfg.upstreamCheckMs > 0
|
|
3968
|
-
? setInterval(() => { for (const room of rooms.values()) if (!room.ended) { room.checkUpstreams().catch((err) => log(`[${room.name}] could not look for new commits: ${err.message}`))
|
|
3994
|
+
? setInterval(() => { for (const room of rooms.values()) if (!room.ended) { room.checkUpstreams().catch((err) => log(`[${room.name}] could not look for new commits: ${err.message}`)) } }, Math.min(cfg.gitSync ? 20 * 1000 : 60 * 1000, cfg.upstreamCheckMs)) // with the mirror, often enough that each branch's minute look isn't a tick late
|
|
3969
3995
|
: null
|
|
3970
3996
|
upstreamTicker?.unref()
|
|
3971
3997
|
// Live-sync leases renewed, and let go of for sessions that ended or sat idle for a day (relay-lease.js).
|
|
@@ -4009,6 +4035,7 @@ export function startServer ({ port = 4321, host = '0.0.0.0', dataDir = null, lo
|
|
|
4009
4035
|
cluster, // exposed for tests
|
|
4010
4036
|
close: async () => {
|
|
4011
4037
|
clearInterval(reportTicker)
|
|
4038
|
+
machineMeter.stop()
|
|
4012
4039
|
if (cluster) cluster.stop()
|
|
4013
4040
|
clearInterval(leaseTicker)
|
|
4014
4041
|
clearInterval(heartbeat)
|