bareloop 0.24.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +111 -1
- package/bareloop.context.md +177 -0
- package/package.json +1 -1
- package/src/index.js +12 -0
- package/src/kinds.js +11 -3
- package/src/source.js +849 -0
- package/src/validate.js +15 -0
- package/types/index.d.ts +1 -0
- package/types/kinds.d.ts +26 -0
- package/types/source.d.ts +206 -0
- package/types/validate.d.ts +7 -0
package/src/validate.js
CHANGED
|
@@ -154,6 +154,21 @@ export const SECRET_PATTERNS = [
|
|
|
154
154
|
];
|
|
155
155
|
const SECRET_RE = new RegExp(SECRET_PATTERNS.map((r) => r.source).join('|'));
|
|
156
156
|
|
|
157
|
+
/** Human-readable name for each entry in `SECRET_PATTERNS`, SAME order, SAME
|
|
158
|
+
* length — for a caller that must name WHICH shape matched (a refusal
|
|
159
|
+
* message, an audit line) without ever printing the matched text itself. Kept
|
|
160
|
+
* beside the inventory it describes rather than hand-typed at each call site,
|
|
161
|
+
* for the same reason `scanSecrets`/`redactSecrets` are: a second, drifting
|
|
162
|
+
* copy is how a name and its pattern come apart. */
|
|
163
|
+
export const SECRET_PATTERN_NAMES = [
|
|
164
|
+
'sk- prefixed API key',
|
|
165
|
+
'GitHub personal access token (ghp_)',
|
|
166
|
+
'GitHub fine-grained PAT (github_pat_)',
|
|
167
|
+
'AWS access key ID (AKIA)',
|
|
168
|
+
'Slack token (xox)',
|
|
169
|
+
'Google/Gemini API key (AIza)',
|
|
170
|
+
];
|
|
171
|
+
|
|
157
172
|
/**
|
|
158
173
|
* Scan a RAW text stream (a spine file, a close's output, a transcript) for
|
|
159
174
|
* known secret shapes and return the literal matches. The ONE spelling of the
|
package/types/index.d.ts
CHANGED
|
@@ -29,3 +29,4 @@ export { runInterview, authorCloseForJob, assembleSpec, prepareSigning, refusalE
|
|
|
29
29
|
export { PROMPT_REGISTERS, isPromptFile } from "./promptregisters.js";
|
|
30
30
|
export { exportBundle, bundleHash, readBundle, resolveBundleSpec, checkEnvelope, bless, verifyBlessing, appendHistory, checkBundleDeps } from "./bundle.js";
|
|
31
31
|
export { readCloseScripts, checkCloseAbsolutePaths, absolutePathLiteralsOf, hashCloseScriptBytes, checkCloseByteSignature, checkStageByteSignature, signCloseScripts } from "./close-integrity.js";
|
|
32
|
+
export { prepareSource, proveDestination, copyOut, readSourceManifest, frontDoorFromManifest, datedDestination, pickDelivery } from "./source.js";
|
package/types/kinds.d.ts
CHANGED
|
@@ -49,6 +49,32 @@ export function doorOpens(job: any, reviewDoor?: boolean | null): boolean;
|
|
|
49
49
|
* @returns {any[]}
|
|
50
50
|
*/
|
|
51
51
|
export function mechanicalStages(stages: any[] | null | undefined): any[];
|
|
52
|
+
/**
|
|
53
|
+
* git, in a repository (reads by every caller in this file; `src/source.js`
|
|
54
|
+
* also uses it for the hidden-git seed's `init`/`add`/`commit`). Faults come
|
|
55
|
+
* back as `ok:false` with the stderr — a caller decides whether that is a
|
|
56
|
+
* stop.
|
|
57
|
+
*
|
|
58
|
+
* Exported (PRD item 33/M2, `src/source.js`) so the hidden-git seed a plain
|
|
59
|
+
* folder/file/URL job runs against goes through the SAME spawn primitive as
|
|
60
|
+
* every other git call in this file — a second `spawn('git', …)` for the
|
|
61
|
+
* source front door would be a second instrument with its own timeout and
|
|
62
|
+
* buffer ceiling to keep in sync with this one.
|
|
63
|
+
* @param {string} cwd @param {string[]} args
|
|
64
|
+
*/
|
|
65
|
+
export function git(cwd: string, args: string[]): Promise<{
|
|
66
|
+
ok: boolean;
|
|
67
|
+
out: string;
|
|
68
|
+
err: string;
|
|
69
|
+
code: null;
|
|
70
|
+
fault: string;
|
|
71
|
+
} | {
|
|
72
|
+
ok: boolean;
|
|
73
|
+
out: string;
|
|
74
|
+
err: string;
|
|
75
|
+
code: number;
|
|
76
|
+
fault: "failed";
|
|
77
|
+
}>;
|
|
52
78
|
/**
|
|
53
79
|
* D8's seed, READ rather than typed: HEAD at run start. A close stores the
|
|
54
80
|
* counting rule and never the number (D12), so which commit the run is measured
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the frozen source tree — `<into>/tree/input/…` (the copy), `<into>/tree/output/`
|
|
3
|
+
* (empty, where the run writes), `<into>/source.json` (the manifest, OUTSIDE
|
|
4
|
+
* the tree so no worker can read or edit it). One hidden git commit seeds the
|
|
5
|
+
* tree with a neutralized identity; the seed sha rides in the manifest and is
|
|
6
|
+
* what every close in this run measures against (`seedAtHead`, src/kinds.js).
|
|
7
|
+
*
|
|
8
|
+
* `destination`, when given, is recorded in the manifest (never in a signed
|
|
9
|
+
* job spec — a job spec is a repeatable SHAPE, source/destination a PER-RUN
|
|
10
|
+
* value, see `readSourceManifest`'s own comment) as the run's declared
|
|
11
|
+
* drop-off point. D3 rework (hamr's ruling, 2026-09-12, condensed in
|
|
12
|
+
* `docs/product/ITEM33-BUILD.md`): **destination is a DIRECTORY, never a
|
|
13
|
+
* filename** — it may already exist (need not be empty), and it may sit
|
|
14
|
+
* inside the source itself (the same-dir case: "i can limit your actions to
|
|
15
|
+
* a certain fix in a certain dir and your output will also be there"). A job
|
|
16
|
+
* may produce more than one file; the agent names them, and `copyOut`
|
|
17
|
+
* delivers every non-empty file it finds under `output/`, each under its own
|
|
18
|
+
* dated name. There is therefore no separate `output` field any more — the
|
|
19
|
+
* old single-file `output-invalid` guard existed only to stop a
|
|
20
|
+
* manifest-declared path from escaping the tree, and that vector is closed
|
|
21
|
+
* structurally now: `copyOut` always reads `output/` itself, never a path a
|
|
22
|
+
* manifest could have been hand-edited to name.
|
|
23
|
+
*
|
|
24
|
+
* A `kind: 'repo'` source's `destination` is a DIFFERENT thing entirely
|
|
25
|
+
* (hamr: "destination if for code on a green then that would be the place
|
|
26
|
+
* agent allowed to do changes") — it names the WRITE FENCE inside the copied
|
|
27
|
+
* repo, the signed `writeScope` field's job (`src/job.js:363`), not a
|
|
28
|
+
* filesystem drop-off point this door proves or copies into. It is recorded
|
|
29
|
+
* as declared, unvalidated by `proveDestination`, and `frontDoorFromManifest`
|
|
30
|
+
* never hands a repo source's destination to `copyOut` — wiring it into
|
|
31
|
+
* `writeScope` is M3/M4's job, not this one.
|
|
32
|
+
* @param {{source: string, into: string, destination?: string,
|
|
33
|
+
* fetchTimeoutMs?: number}} args `fetchTimeoutMs` is test-only — production
|
|
34
|
+
* callers omit it and get `PROVIDER_TIMEOUT_MS`.
|
|
35
|
+
* @returns {Promise<{stop: null, into: string, tree: string, manifestPath: string, manifest: object}|SourceRefusal>}
|
|
36
|
+
*/
|
|
37
|
+
export function prepareSource({ source, into, destination, fetchTimeoutMs }: {
|
|
38
|
+
source: string;
|
|
39
|
+
into: string;
|
|
40
|
+
destination?: string;
|
|
41
|
+
fetchTimeoutMs?: number;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
stop: null;
|
|
44
|
+
into: string;
|
|
45
|
+
tree: string;
|
|
46
|
+
manifestPath: string;
|
|
47
|
+
manifest: object;
|
|
48
|
+
} | SourceRefusal>;
|
|
49
|
+
/**
|
|
50
|
+
* The delivered name (M2b fix 4, kept under D3): a destination FILE of
|
|
51
|
+
* `/home/me/profile.md` lands as `/home/me/profile-2026-09-12.md`, and a
|
|
52
|
+
* SECOND delivery of the same name the same day as
|
|
53
|
+
* `profile-2026-09-12-2.md`, `-3`, … (the work-branch collision rule, the same
|
|
54
|
+
* spelling `prepareWorkBranch` uses). The date is the delivery's, not the
|
|
55
|
+
* run's: what a person wants to know opening the folder later is when the file
|
|
56
|
+
* arrived. D3: `destination` here is a FULL FILE PATH inside the destination
|
|
57
|
+
* DIRECTORY (`copyOut` builds one per output file it delivers) — this
|
|
58
|
+
* function itself is unchanged from M2b, it is just no longer handed the
|
|
59
|
+
* bare top-level destination directly.
|
|
60
|
+
*
|
|
61
|
+
* Nothing is ever overwritten, which is why the name is computed rather than
|
|
62
|
+
* taken: a plain `profile.md` would eventually collide with the person's own
|
|
63
|
+
* file or a previous run's, and the only honest answers to a collision are
|
|
64
|
+
* "refuse" or "a new name" — hamr picked a new name.
|
|
65
|
+
* @param {string} destination the absolute FILE path as declared
|
|
66
|
+
* @param {Date} now
|
|
67
|
+
* @param {number} n 1 = the plain dated name, 2+ = the same-day suffix
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
export function datedDestination(destination: string, now: Date, n: number): string;
|
|
71
|
+
/**
|
|
72
|
+
* The first dated name that is free, or a refusal when every same-day slot up
|
|
73
|
+
* to `MAX_SAME_DAY` is taken. Used by `copyOut` at delivery time (per output
|
|
74
|
+
* file) so a same-day repeat delivery of the same name never disagrees with
|
|
75
|
+
* what "landable" means.
|
|
76
|
+
* @param {string} destination an absolute FILE path
|
|
77
|
+
* @param {Date} [now]
|
|
78
|
+
* @returns {{stop: null, path: string}|SourceRefusal}
|
|
79
|
+
*/
|
|
80
|
+
export function pickDelivery(destination: string, now?: Date): {
|
|
81
|
+
stop: null;
|
|
82
|
+
path: string;
|
|
83
|
+
} | SourceRefusal;
|
|
84
|
+
/**
|
|
85
|
+
* Prove a destination DIRECTORY is landable — $0, mechanical, before any
|
|
86
|
+
* token spends (PRD item 33 ruling: "source and destination are proven at
|
|
87
|
+
* job start"). D3 rework (hamr's ruling, 2026-09-12): **destination is a
|
|
88
|
+
* directory, never a filename** — it may already exist, and need not be
|
|
89
|
+
* empty (a job may write into a folder that already holds other files, or
|
|
90
|
+
* even sit inside the source itself, the same-dir case). What this proves is
|
|
91
|
+
* narrower than the M2/M2b version: only that the directory is USABLE
|
|
92
|
+
* (exists as a directory and is writable, or can be created) — it can say
|
|
93
|
+
* nothing about individual delivered FILE names, because those are not known
|
|
94
|
+
* until the run actually produces them (the agent names its own output
|
|
95
|
+
* files); per-file same-day collisions are handled at delivery time, inside
|
|
96
|
+
* `copyOut`, via `pickDelivery`.
|
|
97
|
+
* @param {unknown} destination absolute directory path
|
|
98
|
+
* @param {{into: string}} o `into` — the run's own scratch root (prepared by
|
|
99
|
+
* `prepareSource`, or a run's own `workdir` when there is no source door);
|
|
100
|
+
* the destination may never sit inside it.
|
|
101
|
+
* @returns {Promise<{stop: null}|SourceRefusal>}
|
|
102
|
+
*/
|
|
103
|
+
export function proveDestination(destination: unknown, { into }: {
|
|
104
|
+
into: string;
|
|
105
|
+
}): Promise<{
|
|
106
|
+
stop: null;
|
|
107
|
+
} | SourceRefusal>;
|
|
108
|
+
/**
|
|
109
|
+
* Copy the run's own output to the proven destination DIRECTORY — once per
|
|
110
|
+
* file, never overwriting (`COPYFILE_EXCL`, so even a file that appears
|
|
111
|
+
* between `proveDestination` and here refuses rather than clobbers), on a
|
|
112
|
+
* minted green only. This function renders no verdict and changes none: a
|
|
113
|
+
* refusal here is reported beside the green it could not deliver, never
|
|
114
|
+
* folded back into it.
|
|
115
|
+
*
|
|
116
|
+
* D3 rework (hamr's ruling, 2026-09-12): a job may produce MORE THAN ONE
|
|
117
|
+
* file (his example: a flight search producing one sheet for "SFO→LAX
|
|
118
|
+
* red-eye" and another for "SFO under $700") — the agent names them
|
|
119
|
+
* meaningfully, and EVERY non-empty file under `tree/output/` (excluding
|
|
120
|
+
* `output/.gitkeep`) is delivered, each under its own dated name
|
|
121
|
+
* (`datedDestination`/`pickDelivery`, M2b fix 4, unchanged). An empty file is
|
|
122
|
+
* silently skipped (not a delivered result, same judgment M2b made for the
|
|
123
|
+
* single-file case) rather than refusing the whole batch over one empty
|
|
124
|
+
* file; the refusal is reserved for the case where NOTHING is left to
|
|
125
|
+
* deliver at all.
|
|
126
|
+
*
|
|
127
|
+
* `into`, when given, is the run's own SCRATCH ROOT (`prepareSource`'s
|
|
128
|
+
* `into`, of which `tree` is the `tree/` subdirectory) — review finding #3:
|
|
129
|
+
* a destination sitting inside `<into>` but outside `tree` (e.g. dropped
|
|
130
|
+
* straight in the scratch area, never inside the frozen tree) must still
|
|
131
|
+
* refuse `destination-contained`, which a containment check against `tree`
|
|
132
|
+
* alone cannot see. Omitted, it defaults to `tree` (the pre-fix behaviour),
|
|
133
|
+
* for direct callers that only ever had a tree, never an `into`.
|
|
134
|
+
* @param {{tree: string, into?: string, destination: string}} o
|
|
135
|
+
* @returns {Promise<{stop: null, files: {path: string, bytes: number, sha256: string}[]}|SourceRefusal>}
|
|
136
|
+
*/
|
|
137
|
+
export function copyOut({ tree, into, destination }: {
|
|
138
|
+
tree: string;
|
|
139
|
+
into?: string;
|
|
140
|
+
destination: string;
|
|
141
|
+
}): Promise<{
|
|
142
|
+
stop: null;
|
|
143
|
+
files: {
|
|
144
|
+
path: string;
|
|
145
|
+
bytes: number;
|
|
146
|
+
sha256: string;
|
|
147
|
+
}[];
|
|
148
|
+
} | SourceRefusal>;
|
|
149
|
+
/**
|
|
150
|
+
* Read a source-door manifest, if one is there. mid-build spec correction
|
|
151
|
+
* (2026-09-11, the main session, on hamr's question about export): a job
|
|
152
|
+
* spec is a SHAPE, signed once — source and destination are a PER-RUN value,
|
|
153
|
+
* the way `bareloop run <bundle> --repo <path>` varies the repo instance
|
|
154
|
+
* without touching the bundle's signature. Baking an absolute destination
|
|
155
|
+
* path into `job.js`/`jobSpecHash` would sign one instance where the spec is
|
|
156
|
+
* meant to describe a repeatable shape, so the run instance lives HERE
|
|
157
|
+
* instead — the manifest `prepareSource` already writes outside the tree —
|
|
158
|
+
* and nothing was added to the job schema.
|
|
159
|
+
*
|
|
160
|
+
* Absence is reported as absence: a patient the JOBS table points at
|
|
161
|
+
* directly, never having gone through `prepareSource`, has no manifest, and
|
|
162
|
+
* that IS "repo jobs untouched" — nothing here invents a destination for a
|
|
163
|
+
* job that never declared one. A manifest that EXISTS but cannot be read or
|
|
164
|
+
* parsed is a named stop, never a silent skip: the run has evidence
|
|
165
|
+
* something is there and wrong, and hiding that is worse than refusing on it.
|
|
166
|
+
* @param {string} into the manifest's own directory — `<into>/source.json`
|
|
167
|
+
* (a run-u-style caller passes `dirname(workdir)`, since `workdir` IS
|
|
168
|
+
* `<into>/tree`)
|
|
169
|
+
* @returns {Promise<{stop: null, present: false, manifest: null}|{stop: null, present: true, manifest: Record<string, any>}|SourceRefusal>}
|
|
170
|
+
*/
|
|
171
|
+
export function readSourceManifest(into: string): Promise<{
|
|
172
|
+
stop: null;
|
|
173
|
+
present: false;
|
|
174
|
+
manifest: null;
|
|
175
|
+
} | {
|
|
176
|
+
stop: null;
|
|
177
|
+
present: true;
|
|
178
|
+
manifest: Record<string, any>;
|
|
179
|
+
} | SourceRefusal>;
|
|
180
|
+
/**
|
|
181
|
+
* Reduce a `readSourceManifest` success result to the `{destination}` a
|
|
182
|
+
* runner acts on, or `null` when there is none to act on (no manifest, one
|
|
183
|
+
* that never declared a destination, or — D3 — a `kind: 'repo'` manifest,
|
|
184
|
+
* whose `destination` names the WRITE FENCE inside the copied repo, never a
|
|
185
|
+
* filesystem drop-off point this door proves or copies into: "repo jobs
|
|
186
|
+
* UNCHANGED", `writeScope` already does that job, `src/job.js:363`). Pure
|
|
187
|
+
* extraction of the one piece of `run-u.mjs`'s front-door reading that has
|
|
188
|
+
* any logic in it (the rest is spine emits and control flow) —
|
|
189
|
+
* unit-testable apart from the script, which is otherwise unreachable
|
|
190
|
+
* without a live provider key.
|
|
191
|
+
* @param {{present: boolean, manifest: Record<string, any>|null}} read
|
|
192
|
+
* @returns {{destination: string}|null}
|
|
193
|
+
*/
|
|
194
|
+
export function frontDoorFromManifest(read: {
|
|
195
|
+
present: boolean;
|
|
196
|
+
manifest: Record<string, any> | null;
|
|
197
|
+
}): {
|
|
198
|
+
destination: string;
|
|
199
|
+
} | null;
|
|
200
|
+
/**
|
|
201
|
+
* a named, non-throwing stop
|
|
202
|
+
*/
|
|
203
|
+
export type SourceRefusal = {
|
|
204
|
+
stop: string;
|
|
205
|
+
code: string;
|
|
206
|
+
};
|
package/types/validate.d.ts
CHANGED
|
@@ -226,3 +226,10 @@ export function sweepNestedQuantifiers(root: any, red: (code: string, path: stri
|
|
|
226
226
|
*/
|
|
227
227
|
export const CLOSE_SCRIPT_INTERPRETERS: readonly string[];
|
|
228
228
|
export const SECRET_PATTERNS: RegExp[];
|
|
229
|
+
/** Human-readable name for each entry in `SECRET_PATTERNS`, SAME order, SAME
|
|
230
|
+
* length — for a caller that must name WHICH shape matched (a refusal
|
|
231
|
+
* message, an audit line) without ever printing the matched text itself. Kept
|
|
232
|
+
* beside the inventory it describes rather than hand-typed at each call site,
|
|
233
|
+
* for the same reason `scanSecrets`/`redactSecrets` are: a second, drifting
|
|
234
|
+
* copy is how a name and its pattern come apart. */
|
|
235
|
+
export const SECRET_PATTERN_NAMES: string[];
|