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/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
+ };
@@ -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[];