feat(lanes): per-lane database, Redis, and .env isolation (A1+A2)
Gives each lane its own slot-derived runtime (ports, detached process lifecycle, profile-driven hooks) and its own database/Redis logical index/.env file, so two lanes running the same repo's stack at once no longer share state. Machine-level DB/Redis credentials live at ~/.ccam/secrets.env (mode 0600, never returned by any route); a hook's output is redacted of that password (raw and URL-encoded forms) before it reaches a log file or the lane_hook_output websocket broadcast. Wired into provision/up/reset/remove; reset accepts --keep-db to skip the drop/recreate/migrate/reseed block entirely.
This commit is contained in:
+155
@@ -8820,6 +8820,161 @@ paths:
|
||||
description: available:true with {branch, head, subject, dirty, untracked}, or available:false alone.
|
||||
'404':
|
||||
description: Lane not found.
|
||||
/api/lanes/{id}/runtime:
|
||||
get:
|
||||
tags:
|
||||
- Lanes
|
||||
summary: A lane's own application stack
|
||||
description: Slot, ports, per-service liveness, log paths and the last boot error. Recomputed on every call from pid files and port probes rather than cached, because a process can die to OOM or a stray kill without telling anyone. Read-only, so no same-origin guard. Kept out of GET /api/lanes because it opens a socket per declared port and stats every pid file. A lane whose repository declares no .ccam/profile returns available:false with HTTP 200 — most lanes never run a stack, which is a normal state, not a fault.
|
||||
operationId: getLaneRuntime
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: available:false (no profile); available:true with provisioned:false (no slot yet); or the full facts with slot, ports, services, up, healthy, steppedAside, logs and lastError.
|
||||
'404':
|
||||
description: Lane not found.
|
||||
/api/lanes/{id}/up:
|
||||
post:
|
||||
tags:
|
||||
- Lanes
|
||||
summary: Boot a lane's stack
|
||||
description: 'Runs the profile''s boot then health hooks. Returns 202 and finishes in the background because a build can take minutes; progress streams as lane_hook_output and the attempt ends with a lane_runtime message. Does NOT run bootstrap — installing dependencies on every boot would make a routine restart minutes long. A failing health check leaves the processes running, because their logs are what identify the service that never came up. Writes only slot and ports on the lane row: never stage, status or notes.'
|
||||
operationId: upLane
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
build:
|
||||
type: boolean
|
||||
default: true
|
||||
description: false passes --no-build to the boot hook, reusing an existing build.
|
||||
responses:
|
||||
'202':
|
||||
description: Accepted; the boot runs in the background.
|
||||
'400':
|
||||
description: ENOPROFILE — no .ccam/profile, with the paths searched.
|
||||
'403':
|
||||
description: The browser request was not same-origin/loopback.
|
||||
'404':
|
||||
description: Lane not found.
|
||||
'409':
|
||||
description: ESLOTS (every slot taken) or EPORTBUSY (with the occupying pids).
|
||||
/api/lanes/{id}/down:
|
||||
post:
|
||||
tags:
|
||||
- Lanes
|
||||
summary: Stop a lane's stack
|
||||
description: Kills each recorded pid tree bottom-up (killing a parent first reparents its children to init, where nothing knows to look for them), then sweeps listeners on the lane's ports ONLY when a pid file existed — a lane whose stack is already down still owns its port numbers, and an unconditional sweep would kill a server the user started there. Idempotent, and a no-op for a lane that was never up.
|
||||
operationId: downLane
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: '{ok, killed: number[], runtime}.'
|
||||
'403':
|
||||
description: The browser request was not same-origin/loopback.
|
||||
'404':
|
||||
description: Lane not found.
|
||||
/api/lanes/{id}/hook/{name}:
|
||||
post:
|
||||
tags:
|
||||
- Lanes
|
||||
summary: Run one of the lane profile's hooks
|
||||
description: The surface a driving session uses for ci-gate, e2e, migrate and friends. Returns 202; output streams as lane_hook_output and completion arrives as lane_hook_result with the exit code. The name is checked against a fixed allowlist BEFORE anything is spawned, and args travels as an array of strings straight into argv — neither is ever joined into a command string.
|
||||
operationId: runLaneHook
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- bootstrap
|
||||
- boot
|
||||
- health
|
||||
- migrate
|
||||
- seed
|
||||
- ci-gate
|
||||
- e2e
|
||||
- regen
|
||||
- db-create
|
||||
- db-drop
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
args:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
'202':
|
||||
description: Accepted; the hook runs in the background.
|
||||
'400':
|
||||
description: ENOHOOK (name outside the allowlist) or ENOPROFILE.
|
||||
'403':
|
||||
description: The browser request was not same-origin/loopback.
|
||||
'404':
|
||||
description: Lane not found.
|
||||
'409':
|
||||
description: ENOSLOT — the lane has no runtime yet; bring it up first.
|
||||
/api/lanes/{id}/logs/{svc}:
|
||||
get:
|
||||
tags:
|
||||
- Lanes
|
||||
summary: Tail a lane's hook or service log
|
||||
description: The resolved path is confined to the lane's log directory after realpath, so a name from the request can never escape it. Read-only, so no same-origin guard.
|
||||
operationId: getLaneLog
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: svc
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: tail
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 65536
|
||||
maximum: 1048576
|
||||
description: Trailing bytes to return; capped at 1 MiB.
|
||||
responses:
|
||||
'200':
|
||||
description: '{available, svc, size, truncated, text}, or {available:false} for a lane with no slot.'
|
||||
'404':
|
||||
description: Lane not found, or ENOLOG — no such log for this lane.
|
||||
/api/lanes/branches:
|
||||
get:
|
||||
tags:
|
||||
|
||||
Reference in New Issue
Block a user