bareloop 0.30.0 → 0.32.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 +154 -0
- package/bareloop.context.md +205 -10
- package/bin/bareloop.mjs +0 -1
- package/package.json +1 -1
- package/src/authorflow.js +22 -42
- package/src/authorjob.js +5 -8
- package/src/authorrun.js +42 -16
- package/src/cli.js +12 -6
- package/src/config.js +135 -0
- package/src/interviewrun.js +10 -2
- package/src/keysfile.js +156 -0
- package/src/ledger.js +26 -0
- package/src/monthly.js +352 -0
- package/src/panel/authorroutes.js +328 -0
- package/src/panel/authorsession.js +555 -0
- package/src/panel/index.html +1247 -50
- package/src/panel/server.js +449 -51
- package/src/panel/settingsroutes.js +230 -0
- package/src/planrun.js +61 -5
- package/src/promptregisters.js +3 -0
- package/src/providerrows.js +217 -0
- package/src/providers.js +84 -0
- package/src/replay.js +80 -2
- package/src/replayio.js +14 -3
- package/src/run.js +48 -2
- package/src/runlist.js +13 -0
- package/src/userrun.js +134 -13
- package/types/authorflow.d.ts +4 -21
- package/types/authorrun.d.ts +5 -0
- package/types/cli.d.ts +2 -1
- package/types/config.d.ts +48 -0
- package/types/interviewrun.d.ts +5 -0
- package/types/keysfile.d.ts +90 -0
- package/types/ledger.d.ts +14 -0
- package/types/monthly.d.ts +192 -0
- package/types/panel/authorroutes.d.ts +67 -0
- package/types/panel/authorsession.d.ts +53 -0
- package/types/panel/server.d.ts +42 -5
- package/types/panel/settingsroutes.d.ts +14 -0
- package/types/providerrows.d.ts +164 -0
- package/types/providers.d.ts +34 -0
- package/types/replay.d.ts +31 -0
- package/types/replayio.d.ts +12 -1
- package/types/run.d.ts +26 -1
- package/types/runlist.d.ts +1 -0
- package/types/userrun.d.ts +22 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} [home] test seam; production never passes it
|
|
3
|
+
* @returns {string} `<home>/config.json`
|
|
4
|
+
*/
|
|
5
|
+
export function configPath(home?: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} ConfigRead
|
|
8
|
+
* @property {Record<string, any>} config the parsed document ({} when missing/unreadable)
|
|
9
|
+
* @property {boolean} exists
|
|
10
|
+
* @property {string|null} problem plain words when the file is there but unreadable
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* @param {{ home?: string }} [opts]
|
|
14
|
+
* @returns {ConfigRead}
|
|
15
|
+
*/
|
|
16
|
+
export function readConfig(opts?: {
|
|
17
|
+
home?: string;
|
|
18
|
+
}): ConfigRead;
|
|
19
|
+
/**
|
|
20
|
+
* Apply a patch and write. `null` deletes a field. Top-level fields shallow-merge;
|
|
21
|
+
* `keys` merges per key variable name, then per field. Validated before anything is
|
|
22
|
+
* written; nothing is written on a refusal.
|
|
23
|
+
*
|
|
24
|
+
* Patch fields understood: `monthlyLimitUsd` (finite > 0), `anthropicBalanceNote`
|
|
25
|
+
* (finite >= 0), `keys.<ENV NAME>.{name,shape,baseUrl}` (Name = a model id, shape = a provider table name, baseUrl = blank or http(s)). Any other
|
|
26
|
+
* field is passed through untouched (kept), but the whole document is swept for
|
|
27
|
+
* secret-shaped strings first.
|
|
28
|
+
* @param {Record<string, any>} patch
|
|
29
|
+
* @param {{ home?: string }} [opts]
|
|
30
|
+
* @returns {Record<string, any>} the document as written
|
|
31
|
+
*/
|
|
32
|
+
export function updateConfig(patch: Record<string, any>, opts?: {
|
|
33
|
+
home?: string;
|
|
34
|
+
}): Record<string, any>;
|
|
35
|
+
/** A plain-words config failure (bad value, secret-shaped string, unreadable file). */
|
|
36
|
+
export class ConfigError extends Error {
|
|
37
|
+
}
|
|
38
|
+
export type ConfigRead = {
|
|
39
|
+
/**
|
|
40
|
+
* the parsed document ({} when missing/unreadable)
|
|
41
|
+
*/
|
|
42
|
+
config: Record<string, any>;
|
|
43
|
+
exists: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* plain words when the file is there but unreadable
|
|
46
|
+
*/
|
|
47
|
+
problem: string | null;
|
|
48
|
+
};
|
package/types/interviewrun.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {object} Deps
|
|
3
3
|
* @property {Record<string,string|undefined>} [env]
|
|
4
|
+
* @property {string} [keysHome] P4a — test seam: load this home's `.env` keys file even when `env` is injected.
|
|
4
5
|
* @property {NodeJS.ReadStream} [stdin]
|
|
5
6
|
* @property {NodeJS.WritableStream} [stdout]
|
|
6
7
|
* @property {NodeJS.WritableStream} [stderr]
|
|
@@ -25,6 +26,10 @@
|
|
|
25
26
|
export function main(argv: string[], deps?: Deps): Promise<number>;
|
|
26
27
|
export type Deps = {
|
|
27
28
|
env?: Record<string, string | undefined> | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* P4a — test seam: load this home's `.env` keys file even when `env` is injected.
|
|
31
|
+
*/
|
|
32
|
+
keysHome?: string | undefined;
|
|
28
33
|
stdin?: NodeJS.ReadStream | undefined;
|
|
29
34
|
stdout?: NodeJS.WritableStream | undefined;
|
|
30
35
|
stderr?: NodeJS.WritableStream | undefined;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `~/.config/bareloop` (or the injected override) — same home every other
|
|
3
|
+
* per-person file (runs.jsonl, config.json) lives in.
|
|
4
|
+
* @param {string} [home] test seam; production never passes it
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
7
|
+
export function keysHome(home?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} [home]
|
|
10
|
+
* @returns {string} `<home>/.env`
|
|
11
|
+
*/
|
|
12
|
+
export function keysFilePath(home?: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Parse the file's text. Lines that are not `NAME=value` are skipped silently
|
|
15
|
+
* (never echoed — a malformed line may hold a secret).
|
|
16
|
+
* @param {string} text
|
|
17
|
+
* @returns {Record<string,string>}
|
|
18
|
+
*/
|
|
19
|
+
export function parseKeysText(text: string): Record<string, string>;
|
|
20
|
+
/**
|
|
21
|
+
* The NAMES in the file that carry a non-empty value, in file order — the Providers rows
|
|
22
|
+
* (an empty preset line is not a row). Names only; a missing/unreadable file = none.
|
|
23
|
+
* @param {string} [home]
|
|
24
|
+
* @returns {string[]}
|
|
25
|
+
*/
|
|
26
|
+
export function filledKeyNames(home?: string): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Create the keys file with the five empty preset lines when — and only when — it does not
|
|
29
|
+
* exist (mode 600). An existing file is never touched.
|
|
30
|
+
* @param {readonly string[]} presetNames the names to seed, in order
|
|
31
|
+
* @param {string} [home]
|
|
32
|
+
* @returns {boolean} true if it was created
|
|
33
|
+
*/
|
|
34
|
+
export function ensureKeysFile(presetNames: readonly string[], home?: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {object} KeysLoad
|
|
37
|
+
* @property {boolean} exists the file is there
|
|
38
|
+
* @property {string[]} names the NAMES the file defines (sorted; never values)
|
|
39
|
+
* @property {string|null} warning a plain-words warning (file readable by others, or
|
|
40
|
+
* unreadable), else null — a warning, never a refusal
|
|
41
|
+
* @property {Record<string,string|undefined>} env shell env with the file filling the
|
|
42
|
+
* names the shell does not set. THE ONLY place values leave this module.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Load the keys file over an env. Missing file is fine (no names, env unchanged).
|
|
46
|
+
* @param {{ env?: Record<string,string|undefined>, home?: string }} [opts]
|
|
47
|
+
* @returns {KeysLoad}
|
|
48
|
+
*/
|
|
49
|
+
export function loadKeysEnv(opts?: {
|
|
50
|
+
env?: Record<string, string | undefined>;
|
|
51
|
+
home?: string;
|
|
52
|
+
}): KeysLoad;
|
|
53
|
+
/**
|
|
54
|
+
* The door helper the CLI and the panel call: load the file only when the caller is
|
|
55
|
+
* on the REAL environment (no injected env) or names a keys home explicitly — so a
|
|
56
|
+
* test that injects `env` alone never reads the real `~/.config/bareloop/.env`.
|
|
57
|
+
* @param {{ env?: Record<string,string|undefined>, keysHome?: string }} deps
|
|
58
|
+
* @returns {KeysLoad & { env: Record<string,string|undefined>, home: string|undefined }}
|
|
59
|
+
* `home` = the home the file was loaded from (the real `~/.config/bareloop` when none was
|
|
60
|
+
* injected) — or `undefined` when loading was skipped (injected env, no keysHome). The
|
|
61
|
+
* door passes it on so config.json (src/config.js) is read from the SAME place, and never
|
|
62
|
+
* from the real home in a test.
|
|
63
|
+
*/
|
|
64
|
+
export function keysForDoor(deps: {
|
|
65
|
+
env?: Record<string, string | undefined>;
|
|
66
|
+
keysHome?: string;
|
|
67
|
+
}): KeysLoad & {
|
|
68
|
+
env: Record<string, string | undefined>;
|
|
69
|
+
home: string | undefined;
|
|
70
|
+
};
|
|
71
|
+
export type KeysLoad = {
|
|
72
|
+
/**
|
|
73
|
+
* the file is there
|
|
74
|
+
*/
|
|
75
|
+
exists: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* the NAMES the file defines (sorted; never values)
|
|
78
|
+
*/
|
|
79
|
+
names: string[];
|
|
80
|
+
/**
|
|
81
|
+
* a plain-words warning (file readable by others, or
|
|
82
|
+
* unreadable), else null — a warning, never a refusal
|
|
83
|
+
*/
|
|
84
|
+
warning: string | null;
|
|
85
|
+
/**
|
|
86
|
+
* shell env with the file filling the
|
|
87
|
+
* names the shell does not set. THE ONLY place values leave this module.
|
|
88
|
+
*/
|
|
89
|
+
env: Record<string, string | undefined>;
|
|
90
|
+
};
|
package/types/ledger.d.ts
CHANGED
|
@@ -81,6 +81,20 @@ export function spendProvenance(events: any[]): {
|
|
|
81
81
|
unpriced: ProvenanceBucket;
|
|
82
82
|
unknown: ProvenanceBucket;
|
|
83
83
|
};
|
|
84
|
+
/**
|
|
85
|
+
* The priced-rounds spend floor + first-record→last-record wall floor,
|
|
86
|
+
* derived straight off the spine's raw records — the same derivation a
|
|
87
|
+
* died run's "at least …" figures use, factored out so build item 6
|
|
88
|
+
* (2026-09-28) can hand a genuinely-still-running spine the identical
|
|
89
|
+
* floor instead of "unknown" on the Run tab, without a second copy of this
|
|
90
|
+
* math.
|
|
91
|
+
* @param {any[]} records raw parsed spine records
|
|
92
|
+
* @returns {{spendFloorUsd: number|null, wallFloorMs: number|null}}
|
|
93
|
+
*/
|
|
94
|
+
export function floorsFromRecords(records: any[]): {
|
|
95
|
+
spendFloorUsd: number | null;
|
|
96
|
+
wallFloorMs: number | null;
|
|
97
|
+
};
|
|
84
98
|
/** severity order, worst-first (the fold's display order; frequency ranks within) */
|
|
85
99
|
export const LEDGER_CLASSES: readonly string[];
|
|
86
100
|
/**
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One run's own spend, off its spine records: this LEG's figure (never the chain fold),
|
|
3
|
+
* plus the drafting spend the run was signed with (counted once — on the first leg only).
|
|
4
|
+
* @param {any[]} records
|
|
5
|
+
* @returns {{ usd: number, complete: boolean }}
|
|
6
|
+
*/
|
|
7
|
+
export function legSpend(records: any[]): {
|
|
8
|
+
usd: number;
|
|
9
|
+
complete: boolean;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* One run's wall time off its spine: job-start -> job-end when the run ended (exact), else the
|
|
13
|
+
* first -> last record floor (the SAME floor a died / still-running run's "at least" wall uses,
|
|
14
|
+
* `floorsFromRecords`). `ms` null = unknown — never 0.
|
|
15
|
+
* @param {any[]} records
|
|
16
|
+
* @returns {{ ms: number|null, complete: boolean }}
|
|
17
|
+
*/
|
|
18
|
+
export function legWall(records: any[]): {
|
|
19
|
+
ms: number | null;
|
|
20
|
+
complete: boolean;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Tokens a spine's rounds processed (input + output + cache read + cache write — all four,
|
|
24
|
+
* so a cache-heavy run is not under-reported), off the real `usage` field names.
|
|
25
|
+
* @param {any[]} records
|
|
26
|
+
* @returns {number}
|
|
27
|
+
*/
|
|
28
|
+
export function legTokens(records: any[]): number;
|
|
29
|
+
/**
|
|
30
|
+
* Every listed run as one {@link Leg}. The ONE reader the month total, the all-time total
|
|
31
|
+
* and the per-provider figures share.
|
|
32
|
+
* @param {{ home?: string, now?: () => number, resumingSpine?: string|null }} [opts] `now` = the clock for the in-flight test; `resumingSpine` = the spine a resume continues: that leg reserves its spend only, never its unspent cap (the resume's own leg cap is that remainder)
|
|
33
|
+
* @returns {Leg[]}
|
|
34
|
+
*/
|
|
35
|
+
export function readLegs(opts?: {
|
|
36
|
+
home?: string;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
resumingSpine?: string | null;
|
|
39
|
+
}): Leg[];
|
|
40
|
+
/**
|
|
41
|
+
* @param {{ home?: string, now?: () => number, resumingSpine?: string|null }} [opts] `resumingSpine`: see {@link readLegs}
|
|
42
|
+
* @returns {{ usd: number, atLeast: boolean, runs: number, reservedUsd: number }} `usd` = real spend (the Money tab's figure); `reservedUsd` = the same with in-flight runs at their cap (the refusal check's)
|
|
43
|
+
*/
|
|
44
|
+
export function monthSpend(opts?: {
|
|
45
|
+
home?: string;
|
|
46
|
+
now?: () => number;
|
|
47
|
+
resumingSpine?: string | null;
|
|
48
|
+
}): {
|
|
49
|
+
usd: number;
|
|
50
|
+
atLeast: boolean;
|
|
51
|
+
runs: number;
|
|
52
|
+
reservedUsd: number;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The Money tab's figures: all-time and this-month spend, and per-provider breakdown
|
|
56
|
+
* (rows from `rowIdFor`; a run on no known row lands under its own provider name, never
|
|
57
|
+
* pooled into another row). Any leg with unknown spend makes the figure it belongs to
|
|
58
|
+
* an "at least".
|
|
59
|
+
* @param {{ home?: string, now?: () => number, rows?: readonly import('./providerrows.js').KeyRow[] }} [opts] `rows` = the Providers rows: when given, `tokensByRow` sums each row's tokens
|
|
60
|
+
* @returns {{ tokensByRow: Record<string, number>, total: {usd: number, atLeast: boolean, tokens: number}, month: {usd: number, atLeast: boolean, tokens: number},
|
|
61
|
+
* byProvider: Record<string, {label: string, monthUsd: number, monthAtLeast: boolean, totalUsd: number, totalAtLeast: boolean, tokens: number, vouchedRounds: number, otherRounds: number, monthWallMs: number|null, monthWallAtLeast: boolean, totalWallMs: number|null, totalWallAtLeast: boolean}> }}
|
|
62
|
+
*/
|
|
63
|
+
export function spendSummary(opts?: {
|
|
64
|
+
home?: string;
|
|
65
|
+
now?: () => number;
|
|
66
|
+
rows?: readonly import("./providerrows.js").KeyRow[];
|
|
67
|
+
}): {
|
|
68
|
+
tokensByRow: Record<string, number>;
|
|
69
|
+
total: {
|
|
70
|
+
usd: number;
|
|
71
|
+
atLeast: boolean;
|
|
72
|
+
tokens: number;
|
|
73
|
+
};
|
|
74
|
+
month: {
|
|
75
|
+
usd: number;
|
|
76
|
+
atLeast: boolean;
|
|
77
|
+
tokens: number;
|
|
78
|
+
};
|
|
79
|
+
byProvider: Record<string, {
|
|
80
|
+
label: string;
|
|
81
|
+
monthUsd: number;
|
|
82
|
+
monthAtLeast: boolean;
|
|
83
|
+
totalUsd: number;
|
|
84
|
+
totalAtLeast: boolean;
|
|
85
|
+
tokens: number;
|
|
86
|
+
vouchedRounds: number;
|
|
87
|
+
otherRounds: number;
|
|
88
|
+
monthWallMs: number | null;
|
|
89
|
+
monthWallAtLeast: boolean;
|
|
90
|
+
totalWallMs: number | null;
|
|
91
|
+
totalWallAtLeast: boolean;
|
|
92
|
+
}>;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* @typedef {object} MonthlyRoom
|
|
96
|
+
* @property {boolean} ok
|
|
97
|
+
* @property {number|null} leftUsd what is left this month (null = no limit set)
|
|
98
|
+
* @property {number|null} limitUsd the person's monthly limit (null = none)
|
|
99
|
+
* @property {boolean} atLeast the month's spend is a floor, so `leftUsd` is a ceiling
|
|
100
|
+
*/
|
|
101
|
+
/**
|
|
102
|
+
* The person's monthly limit off a parsed config.json: key absent (or null) = no limit (null);
|
|
103
|
+
* a number above 0 = the limit; ANY other value present is a broken setting and throws
|
|
104
|
+
* `ConfigError` — a limit the file names but this code cannot read never reads as "no limit".
|
|
105
|
+
* @param {Record<string, any>} config
|
|
106
|
+
* @param {string} [home]
|
|
107
|
+
* @returns {number|null}
|
|
108
|
+
*/
|
|
109
|
+
export function monthlyLimitOf(config: Record<string, any>, home?: string): number | null;
|
|
110
|
+
/**
|
|
111
|
+
* Does a run with this $ cap fit in what is left this month (running jobs counted at their full cap, except the spine a resume continues — `resumingSpine` — which counts its real spend)? No limit set (key absent or null) = ok, no check; a limit that is present but not a
|
|
112
|
+
* number above 0 throws `ConfigError`.
|
|
113
|
+
* Compared in whole cents. Also throws `ConfigError` when config.json is unreadable — a gate
|
|
114
|
+
* whose own instrument is broken refuses; it never silently runs with no limit.
|
|
115
|
+
* @param {{ capUsd: number, home?: string, now?: () => number, resumingSpine?: string|null }} args
|
|
116
|
+
* @returns {MonthlyRoom}
|
|
117
|
+
*/
|
|
118
|
+
export function checkMonthlyRoom({ capUsd, home, now, resumingSpine }: {
|
|
119
|
+
capUsd: number;
|
|
120
|
+
home?: string;
|
|
121
|
+
now?: () => number;
|
|
122
|
+
resumingSpine?: string | null;
|
|
123
|
+
}): MonthlyRoom;
|
|
124
|
+
/**
|
|
125
|
+
* The ONE refusal text, everywhere (CLI, panel cap note, panel Sign refusal).
|
|
126
|
+
* @param {MonthlyRoom} room
|
|
127
|
+
* @returns {string|null} null when the run fits
|
|
128
|
+
*/
|
|
129
|
+
export function monthlyRefusalText(room: MonthlyRoom): string | null;
|
|
130
|
+
export type Leg = {
|
|
131
|
+
/**
|
|
132
|
+
* when the run was listed
|
|
133
|
+
*/
|
|
134
|
+
at: Date;
|
|
135
|
+
/**
|
|
136
|
+
* job-start's provider (null = an older spine that carried none)
|
|
137
|
+
*/
|
|
138
|
+
provider: string | null;
|
|
139
|
+
baseUrl: string | null;
|
|
140
|
+
/**
|
|
141
|
+
* this leg's spend (a floor when `!complete`)
|
|
142
|
+
*/
|
|
143
|
+
usd: number;
|
|
144
|
+
/**
|
|
145
|
+
* every figure behind `usd` is known
|
|
146
|
+
*/
|
|
147
|
+
complete: boolean;
|
|
148
|
+
tokens: number;
|
|
149
|
+
/**
|
|
150
|
+
* job-start's model (older spines carry a model but no provider)
|
|
151
|
+
*/
|
|
152
|
+
model: string | null;
|
|
153
|
+
/**
|
|
154
|
+
* this leg's wall time (null = unknown; a floor when `!wallComplete`)
|
|
155
|
+
*/
|
|
156
|
+
wallMs: number | null;
|
|
157
|
+
/**
|
|
158
|
+
* the wall is the exact job-start -> job-end span
|
|
159
|
+
*/
|
|
160
|
+
wallComplete: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* rounds priced by a rate somebody vouched for (`provider`/`caller`)
|
|
163
|
+
*/
|
|
164
|
+
vouchedRounds: number;
|
|
165
|
+
/**
|
|
166
|
+
* every other round: a built-in guess, unpriced, or no provenance on record
|
|
167
|
+
*/
|
|
168
|
+
otherRounds: number;
|
|
169
|
+
/**
|
|
170
|
+
* what the refusal check counts: `usd`, or the leg's cap when the run is in flight
|
|
171
|
+
*/
|
|
172
|
+
reservedUsd: number;
|
|
173
|
+
/**
|
|
174
|
+
* the spine was missing/unreadable or the listed date was bad (spend unknown)
|
|
175
|
+
*/
|
|
176
|
+
unreadable: boolean;
|
|
177
|
+
};
|
|
178
|
+
export type MonthlyRoom = {
|
|
179
|
+
ok: boolean;
|
|
180
|
+
/**
|
|
181
|
+
* what is left this month (null = no limit set)
|
|
182
|
+
*/
|
|
183
|
+
leftUsd: number | null;
|
|
184
|
+
/**
|
|
185
|
+
* the person's monthly limit (null = none)
|
|
186
|
+
*/
|
|
187
|
+
limitUsd: number | null;
|
|
188
|
+
/**
|
|
189
|
+
* the month's spend is a floor, so `leftUsd` is a ceiling
|
|
190
|
+
*/
|
|
191
|
+
atLeast: boolean;
|
|
192
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** @returns {string} a fresh per-process token — never persisted, never logged */
|
|
2
|
+
export function mintToken(): string;
|
|
3
|
+
/**
|
|
4
|
+
* `true` when a request may reach ANY route in this file — the token header
|
|
5
|
+
* plus an Origin/Host that names this exact bind address. `req.headers`
|
|
6
|
+
* lower-cases header names (node's own `http` behaviour), so this reads
|
|
7
|
+
* lower-case names throughout.
|
|
8
|
+
* @param {import('node:http').IncomingMessage} req
|
|
9
|
+
* @param {{token: string, port: number}} o
|
|
10
|
+
* @returns {{ok: true}|{ok: false, reason: string}}
|
|
11
|
+
*/
|
|
12
|
+
export function checkHumanGuard(req: import("node:http").IncomingMessage, { token, port }: {
|
|
13
|
+
token: string;
|
|
14
|
+
port: number;
|
|
15
|
+
}): {
|
|
16
|
+
ok: true;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Builds this file's own request handler, holding its session store in a
|
|
23
|
+
* closure (one per `createPanelServer` call, exactly like `handleRequest`'s
|
|
24
|
+
* own module holds none — this file owns the state `src/panel/server.js`
|
|
25
|
+
* deliberately has none of).
|
|
26
|
+
* @param {{ port: number, token: string, env?: Record<string,string|undefined>,
|
|
27
|
+
* sessionsRoot?: string, spawnFn?: typeof realSpawn, bareloopBin?: string,
|
|
28
|
+
* jobsDir?: string, fetchImpl?: typeof fetch, home?: string }} opts
|
|
29
|
+
*/
|
|
30
|
+
export function createAuthorRoutes(opts: {
|
|
31
|
+
port: number;
|
|
32
|
+
token: string;
|
|
33
|
+
env?: Record<string, string | undefined>;
|
|
34
|
+
sessionsRoot?: string;
|
|
35
|
+
spawnFn?: typeof realSpawn;
|
|
36
|
+
bareloopBin?: string;
|
|
37
|
+
jobsDir?: string;
|
|
38
|
+
fetchImpl?: typeof fetch;
|
|
39
|
+
home?: string;
|
|
40
|
+
}): {
|
|
41
|
+
handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
|
|
42
|
+
sessions: Map<string, any>;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* THE ONLY function in this file (or anywhere in the panel) that spawns a
|
|
46
|
+
* run. Refuses unless the session is `prepared` AND the hash the request
|
|
47
|
+
* carried matches the session's OWN `signing.json` `specHash` exactly — the
|
|
48
|
+
* click carries the hash (per the mockup's own "the person never has to copy
|
|
49
|
+
* it to sign"), and this is where it is checked, not trusted.
|
|
50
|
+
* @param {ReturnType<typeof createSession>} session
|
|
51
|
+
* @param {string} claimedHash
|
|
52
|
+
* @param {{env: any, spawnFn: typeof realSpawn, bareloopBin: string, sessionsRoot?: string, home?: string}} o
|
|
53
|
+
* @returns {{ok: boolean, error?: string, job?: string}}
|
|
54
|
+
*/
|
|
55
|
+
export function signRun(session: ReturnType<typeof createSession>, claimedHash: string, o: {
|
|
56
|
+
env: any;
|
|
57
|
+
spawnFn: typeof realSpawn;
|
|
58
|
+
bareloopBin: string;
|
|
59
|
+
sessionsRoot?: string;
|
|
60
|
+
home?: string;
|
|
61
|
+
}): {
|
|
62
|
+
ok: boolean;
|
|
63
|
+
error?: string;
|
|
64
|
+
job?: string;
|
|
65
|
+
};
|
|
66
|
+
import { spawn as realSpawn } from 'node:child_process';
|
|
67
|
+
import { createSession } from './authorsession.js';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `true` when `jobs/<name>.json` already exists in the repo's own jobs/ dir
|
|
3
|
+
* (resolved from THIS module's own on-disk location, never `process.cwd()`,
|
|
4
|
+
* the same rule `src/panel/server.js`'s `jobsDir()` follows) — P3 is new-jobs-
|
|
5
|
+
* only (Q4=A), so a name collision refuses at $0 rather than silently
|
|
6
|
+
* overwriting or authoring a second spec under one name.
|
|
7
|
+
* @param {string} name
|
|
8
|
+
* @param {{jobsDir?: string}} [opts] test seam
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function jobNameTaken(name: string, opts?: {
|
|
12
|
+
jobsDir?: string;
|
|
13
|
+
}): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* `$0` validation of the job card fields the server must check BEFORE it
|
|
16
|
+
* creates a session or spends anything — never inside the async pipeline,
|
|
17
|
+
* so a bad card refuses synchronously, in the same HTTP response.
|
|
18
|
+
* @param {any} card
|
|
19
|
+
* @param {{jobsDir?: string, rows?: readonly import('../providerrows.js').KeyRow[]}} [opts] `rows` = the Settings rows (the Model menu); jobsDir is a server-side test seam — NEVER read off the client card
|
|
20
|
+
* @returns {{ok: true}|{ok: false, error: string}}
|
|
21
|
+
*/
|
|
22
|
+
export function validateJobCard(card: any, opts?: {
|
|
23
|
+
jobsDir?: string;
|
|
24
|
+
rows?: readonly import("../providerrows.js").KeyRow[];
|
|
25
|
+
}): {
|
|
26
|
+
ok: true;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* ONE session's engine — created already RUNNING (the caller awaits nothing;
|
|
33
|
+
* the pipeline drives itself, reporting through `state`). `state.pendingAsk`
|
|
34
|
+
* is the ONE thing a person answers next; `answer()`/`send()`/`revise()`/
|
|
35
|
+
* `signPrepare()` are the only ways in.
|
|
36
|
+
* @param {any} card the validated job card (see {@link validateJobCard})
|
|
37
|
+
* @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, home?: string, timeoutMs?: number,
|
|
38
|
+
* scout?: any, generate?: Function, confirmGenerate?: Function, authorFn?: Function,
|
|
39
|
+
* prepareSigningFn?: Function}} [deps] the last five are TEST SEAMS ONLY — see the note
|
|
40
|
+
* just above where each is read, below.
|
|
41
|
+
* @returns {any} the session object
|
|
42
|
+
*/
|
|
43
|
+
export function createSession(card: any, deps?: {
|
|
44
|
+
env?: Record<string, string | undefined>;
|
|
45
|
+
sessionsRoot?: string;
|
|
46
|
+
home?: string;
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
scout?: any;
|
|
49
|
+
generate?: Function;
|
|
50
|
+
confirmGenerate?: Function;
|
|
51
|
+
authorFn?: Function;
|
|
52
|
+
prepareSigningFn?: Function;
|
|
53
|
+
}): any;
|
package/types/panel/server.d.ts
CHANGED
|
@@ -200,13 +200,25 @@ export function getRunJob(runid: string, opts?: {
|
|
|
200
200
|
* separately from {@link createPanelServer} so tests can drive it without a
|
|
201
201
|
* real listening socket where that is simpler (most path-safety/405 tests
|
|
202
202
|
* still go through a real socket, per the build spec).
|
|
203
|
+
*
|
|
204
|
+
* PANEL-BUILD.md P3: this file's own GET/HEAD-only rule stands for every
|
|
205
|
+
* route it owns; `/api/author/*` is the ONE family of routes that may be
|
|
206
|
+
* POSTed to, and it is handled entirely by {@link createAuthorRoutes}
|
|
207
|
+
* (`src/panel/authorroutes.js`) — a separate module so this file's own
|
|
208
|
+
* "read-only, by construction" header comment stays true of everything else
|
|
209
|
+
* in it. `opts.authorRoutes` is absent on every P1/P2 caller (read-only
|
|
210
|
+
* tests, and the CLI's own `panelMain` when no author routes are wired) —
|
|
211
|
+
* only `createPanelServer` below always supplies one.
|
|
203
212
|
* @param {import('node:http').IncomingMessage} req
|
|
204
213
|
* @param {import('node:http').ServerResponse} res
|
|
205
|
-
* @param {{ home?: string, port: number }} opts
|
|
214
|
+
* @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes> }} opts
|
|
206
215
|
*/
|
|
207
216
|
export function handleRequest(req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, opts: {
|
|
208
217
|
home?: string;
|
|
209
218
|
port: number;
|
|
219
|
+
token?: string;
|
|
220
|
+
authorRoutes?: ReturnType<typeof createAuthorRoutes>;
|
|
221
|
+
settingsRoutes?: ReturnType<typeof createSettingsRoutes>;
|
|
210
222
|
}): void;
|
|
211
223
|
/**
|
|
212
224
|
* Start the panel server. Binds `127.0.0.1` ONLY. A taken port is a LOUD,
|
|
@@ -214,15 +226,33 @@ export function handleRequest(req: import("node:http").IncomingMessage, res: imp
|
|
|
214
226
|
* header). Resolves once actually listening; rejects on a bind error
|
|
215
227
|
* (including `EADDRINUSE`) with a `.port` field on the error for the
|
|
216
228
|
* caller's message.
|
|
217
|
-
*
|
|
218
|
-
*
|
|
229
|
+
*
|
|
230
|
+
* PANEL-BUILD.md P3: a fresh {@link mintToken} token and one {@link
|
|
231
|
+
* createAuthorRoutes} instance are built here, ONCE per server start, and
|
|
232
|
+
* held for the server's lifetime — `opts.env`/`opts.sessionsRoot`/
|
|
233
|
+
* `opts.spawnFn`/`opts.bareloopBin` are test seams the author routes take
|
|
234
|
+
* (a real caller passes none of them and {@link createAuthorRoutes} defaults
|
|
235
|
+
* each one to the real environment/spawn/binary, this file itself touching
|
|
236
|
+
* none of them, and the real
|
|
237
|
+
* `~/.config/bareloop/panel-sessions`, the real `child_process.spawn`, and
|
|
238
|
+
* this package's own `bin/bareloop.mjs`).
|
|
239
|
+
* @param {{ port?: number, home?: string, env?: Record<string,string|undefined>,
|
|
240
|
+
* sessionsRoot?: string, spawnFn?: (...a: any[]) => any, bareloopBin?: string,
|
|
241
|
+
* fetchImpl?: typeof fetch }} [opts]
|
|
242
|
+
* @returns {Promise<{ server: import('node:http').Server, port: number, token: string, close: () => Promise<void> }>}
|
|
219
243
|
*/
|
|
220
244
|
export function createPanelServer(opts?: {
|
|
221
245
|
port?: number;
|
|
222
246
|
home?: string;
|
|
247
|
+
env?: Record<string, string | undefined>;
|
|
248
|
+
sessionsRoot?: string;
|
|
249
|
+
spawnFn?: (...a: any[]) => any;
|
|
250
|
+
bareloopBin?: string;
|
|
251
|
+
fetchImpl?: typeof fetch;
|
|
223
252
|
}): Promise<{
|
|
224
253
|
server: import("node:http").Server;
|
|
225
254
|
port: number;
|
|
255
|
+
token: string;
|
|
226
256
|
close: () => Promise<void>;
|
|
227
257
|
}>;
|
|
228
258
|
/**
|
|
@@ -230,13 +260,17 @@ export function createPanelServer(opts?: {
|
|
|
230
260
|
* Never picks a different port on collision (see file header): prints a
|
|
231
261
|
* loud, named error to stderr and returns 1.
|
|
232
262
|
* @param {string[]} argv
|
|
233
|
-
* @param {{ out: (s: string) => void, err: (s: string) => void, runlistHome?: string }} ctx
|
|
263
|
+
* @param {{ out: (s: string) => void, err: (s: string) => void, runlistHome?: string, env?: Record<string,string|undefined> }} ctx
|
|
264
|
+
* `env` is the RAW shell env (never the start-time merge with the keys file) — the routes
|
|
265
|
+
* re-merge `~/.config/bareloop/.env` onto it per request so Reload keys is real;
|
|
266
|
+
* undefined = the process env.
|
|
234
267
|
* @returns {Promise<number>}
|
|
235
268
|
*/
|
|
236
269
|
export function panelMain(argv: string[], ctx: {
|
|
237
270
|
out: (s: string) => void;
|
|
238
271
|
err: (s: string) => void;
|
|
239
272
|
runlistHome?: string;
|
|
273
|
+
env?: Record<string, string | undefined>;
|
|
240
274
|
}): Promise<number>;
|
|
241
275
|
/**
|
|
242
276
|
* A runid, as it appears in a URL path segment — never a raw filesystem
|
|
@@ -257,9 +291,12 @@ export const DEFAULT_PORT: 4700;
|
|
|
257
291
|
* 2026-09-25).
|
|
258
292
|
*/
|
|
259
293
|
export const SOFTGREEN_ADMITTED_ISO: "2026-08-18T00:00:00.000Z";
|
|
260
|
-
export
|
|
294
|
+
export { DIED_MTIME_MS };
|
|
261
295
|
/** The default/max page size for {@link getRunRounds} — tighten-only,
|
|
262
296
|
* named so a future change is a deliberate, visible edit (item 5,
|
|
263
297
|
* 2026-09-25). */
|
|
264
298
|
export const ROUNDS_PAGE_DEFAULT: 50;
|
|
265
299
|
export const ROUNDS_PAGE_MAX: 200;
|
|
300
|
+
import { createAuthorRoutes } from './authorroutes.js';
|
|
301
|
+
import { createSettingsRoutes } from './settingsroutes.js';
|
|
302
|
+
import { DIED_MTIME_MS } from '../runlist.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{ port: number, token: string, home?: string, env?: Record<string,string|undefined>,
|
|
3
|
+
* fetchImpl?: typeof fetch, now?: () => number }} opts
|
|
4
|
+
*/
|
|
5
|
+
export function createSettingsRoutes(opts: {
|
|
6
|
+
port: number;
|
|
7
|
+
token: string;
|
|
8
|
+
home?: string;
|
|
9
|
+
env?: Record<string, string | undefined>;
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
now?: () => number;
|
|
12
|
+
}): {
|
|
13
|
+
handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
|
|
14
|
+
};
|