@gr8ful/spf 0.9.2 → 0.10.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.
Files changed (37) hide show
  1. package/README.md +56 -0
  2. package/assets/defaults/spf.config.yaml +75 -0
  3. package/assets/skill/references/config.md +98 -4
  4. package/dist/chains/index.d.ts +2 -0
  5. package/dist/chains/index.js +4 -0
  6. package/dist/cli/commands/doctor.js +339 -2
  7. package/dist/cli/commands/fanout.d.ts +7 -14
  8. package/dist/cli/commands/fanout.js +45 -39
  9. package/dist/cli/commands/loop.d.ts +2 -0
  10. package/dist/cli/commands/loop.js +198 -0
  11. package/dist/cli/commands/run.js +14 -4
  12. package/dist/cli/commands/watch.d.ts +29 -1
  13. package/dist/cli/commands/watch.js +219 -64
  14. package/dist/cli/index.js +14 -0
  15. package/dist/core/agent_cc.d.ts +11 -0
  16. package/dist/core/agent_cc.js +25 -2
  17. package/dist/core/agent_flue.js +14 -5
  18. package/dist/core/agents.d.ts +61 -1
  19. package/dist/core/agents.js +363 -6
  20. package/dist/core/data_types.d.ts +316 -0
  21. package/dist/core/data_types.js +143 -0
  22. package/dist/core/loop.d.ts +230 -0
  23. package/dist/core/loop.js +290 -0
  24. package/dist/core/quality.d.ts +1 -2
  25. package/dist/core/sandbox.d.ts +236 -0
  26. package/dist/core/sandbox.js +655 -0
  27. package/dist/core/sandbox_cloudflare.d.ts +137 -0
  28. package/dist/core/sandbox_cloudflare.js +505 -0
  29. package/dist/core/sandbox_opensandbox.d.ts +59 -0
  30. package/dist/core/sandbox_opensandbox.js +484 -0
  31. package/dist/core/sandbox_sdk_types.d.ts +171 -0
  32. package/dist/core/sandbox_sdk_types.js +20 -0
  33. package/dist/core/watch.d.ts +56 -0
  34. package/dist/core/watch.js +358 -52
  35. package/dist/core/worktree_data.d.ts +1 -0
  36. package/dist/core/worktree_data.js +37 -0
  37. package/package.json +1 -1
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Sandbox lease registry, workspace transport, and teardown chain — SPF #15.
3
+ *
4
+ * `SandboxSpec`/`SandboxBackend`/`SandboxScope` live in `data_types.ts`
5
+ * beside the valibot schemas they are derived from (that leaf module imports
6
+ * only valibot; this one is runtime machinery). Everything ELSE the design
7
+ * needs lives here: `SandboxTransport`/`SandboxLease`/`TeardownStep`/
8
+ * `SandboxLog`, the process-local lease registry keyed on `spec.lease_key`,
9
+ * `registerRunLog`'s logger channel, the teardown chain, and the workspace
10
+ * transport (seed / reconcile / extract) written ONCE against the abstract
11
+ * `SandboxTransport` — both backends (OpenSandbox's `SandboxDriver`,
12
+ * Cloudflare's `CloudflareSandboxStub`) satisfy it, so there is exactly one
13
+ * copy of the git sequences below, not one per adapter.
14
+ *
15
+ * The ONE invariant everything in the transport section exists to hold:
16
+ * at every sync point, the sandbox's git HEAD tree is byte-identical to the
17
+ * host tree the judges (permissions/gates/quality/changes) will inspect —
18
+ * tree equality, not history equality. See the design doc's §5.2 for the
19
+ * full proof and the measured transcripts this code reproduces.
20
+ *
21
+ * Backend adapters (`sandbox_opensandbox.ts`, `sandbox_cloudflare.ts`) are a
22
+ * separate slice of this feature — see their own module comments. This file
23
+ * dispatches to them (`factoryFor`) and calls into their transport objects
24
+ * (`seedViaTransport`, `preflight`, `reconcileWorkspace`, `extractWorkspace`)
25
+ * but owns no SDK-specific code itself.
26
+ */
27
+ import type { SandboxFactory } from "@flue/runtime";
28
+ import type { SandboxSpec } from "./data_types.ts";
29
+ /**
30
+ * The live handle the transport (seed/reconcile/extract below) issues
31
+ * commands through. FIVE verbs — narrowed to exactly what BOTH backends can
32
+ * satisfy: OpenSandbox's `SandboxDriver` and Cloudflare's
33
+ * `CloudflareSandboxStub` (7 methods, no `stat`). `readFile` is
34
+ * `Promise<string>` on both, so the transport never hands it a patch —
35
+ * patches cross as base64 (see `applyPatchInSandbox`/`extractWorkspace`
36
+ * below); `readFileBuffer` is for the handoff mirror's small binaries.
37
+ * `size` replaces `stat` for the one thing the patch-size guard needs,
38
+ * because neither backend has a native `stat` in common.
39
+ *
40
+ * `cwd` is REQUIRED on `exec`: the raw driver resolves relative paths
41
+ * against the SDK's own default cwd, not `workspace_dir`.
42
+ */
43
+ export interface SandboxTransport {
44
+ exec(cmd: string, opts: {
45
+ cwd: string;
46
+ timeoutMs?: number;
47
+ }): Promise<{
48
+ stdout: string;
49
+ stderr: string;
50
+ exitCode: number;
51
+ }>;
52
+ readFile(path: string): Promise<string>;
53
+ readFileBuffer(path: string): Promise<Uint8Array>;
54
+ writeFile(path: string, data: string | Uint8Array): Promise<void>;
55
+ /** Bytes, or null when it cannot be determined (`exec("wc -c < <path>")`, parsed). */
56
+ size(path: string): Promise<number | null>;
57
+ }
58
+ export type SandboxLog = (event: {
59
+ level: "info" | "warn" | "error";
60
+ msg: string;
61
+ data?: unknown;
62
+ }) => void;
63
+ export type TeardownStep = {
64
+ name: string;
65
+ run: () => Promise<void>;
66
+ };
67
+ /**
68
+ * The registered handle for one live sandbox. `registerLease` creates the
69
+ * shell (three args — the logger is resolved per-call from `RUN_LOGS`, never
70
+ * passed in); the adapter that created the underlying provider sandbox sets
71
+ * `provider_id`/`native` and pushes its own `teardown` steps afterward.
72
+ */
73
+ export interface SandboxLease {
74
+ spec: SandboxSpec;
75
+ /** Provider id — printed by `spf sandbox list`, the human's drop-in handle. Set by the adapter after create. */
76
+ provider_id: string;
77
+ transport: SandboxTransport;
78
+ /** Resolved, per call, as `RUN_LOGS.get(this.spec.adw_id) ?? NOOP_LOG` — never captured at registration time. */
79
+ log: SandboxLog;
80
+ created_at: string;
81
+ /** Seed fingerprint: {head_sha, patch_sha256}. Drives reconcile; updated by extractWorkspace on the way out. */
82
+ seeded: {
83
+ head_sha: string;
84
+ patch_sha256: string;
85
+ } | null;
86
+ /** Committed sync points so far. Names the extract's marker commit. */
87
+ turn: number;
88
+ /** Flue conversation ids that shared this lease. Trace only, never a key. */
89
+ flue_conversation_ids: string[];
90
+ /** Native, provider-specific surface. NEVER attached to the flue Sandbox. */
91
+ native: {
92
+ endpointUrl?(port: number): Promise<string>;
93
+ egress?(): Promise<unknown>;
94
+ renew?(seconds: number): Promise<void>;
95
+ };
96
+ teardown: TeardownStep[];
97
+ }
98
+ /**
99
+ * The live grant behind one lease's create-time `env`. `env` is what
100
+ * `createEnv(spec)` (each adapter's `createSandbox` MISS branch) hands the
101
+ * provider's create call — the ONLY place `issue()` is ever awaited
102
+ * (`factoryFor` itself stays sync, §6.1/§9). `describe()` is the honest
103
+ * one-liner `spf sandbox list`/`doctor` print: key NAMES only, NEVER a
104
+ * value — the same never-log discipline this file already applies to patch
105
+ * bytes and handoff content. `revoke()` is pushed as the `credentials:revoke`
106
+ * teardown step, at position 2 of the chain both adapters left free for it
107
+ * (see each adapter's MISS branch for the exact splice) — the ordering
108
+ * guarantee itself (a position-2 step is never skipped because position 1
109
+ * threw) is `teardownLease`'s, tested against a fake step; this is the real
110
+ * step riding that proven chain.
111
+ */
112
+ export interface CredentialGrant {
113
+ env: Record<string, string>;
114
+ describe(): string;
115
+ revoke(): Promise<void>;
116
+ }
117
+ /**
118
+ * `id` is the value `sandbox.credentials.broker` names in config —
119
+ * `KNOWN_CREDENTIAL_BROKER_IDS` below is the registry `validateSandboxConfig`
120
+ * (agents.ts) checks an unknown name against, so a typo is a hard config
121
+ * error rather than a silent fallback to `static` (§6.1). `issue(spec)`
122
+ * returns a `Promise` on purpose: a real provisioning broker (§6.3 — not
123
+ * built here, §10 non-goal 2) needs the network, and shaping the interface
124
+ * around that up front means a future broker is a pure insertion, never a
125
+ * signature change to this one.
126
+ */
127
+ export interface CredentialBroker {
128
+ id: string;
129
+ issue(spec: SandboxSpec): Promise<CredentialGrant>;
130
+ }
131
+ /**
132
+ * Factored out of `staticBroker.issue()` so `spf doctor`'s per-agent line
133
+ * (§7 check #9) can print the IDENTICAL sentence a real grant's `describe()`
134
+ * would, from key NAMES it already resolved from config alone — doctor has
135
+ * no live `SandboxSpec` and, deliberately, never reads operator-env VALUES
136
+ * just to build this string.
137
+ */
138
+ export declare function describeStaticCredentials(keyNames: string[]): string;
139
+ /**
140
+ * The ONLY broker registered in this build (§10 non-goal 2; §11's "Deliberately
141
+ * NOT in A"). `issue()` is the identity on `spec.env` — B is observably a
142
+ * no-op on the env PLANE (§6.1); what it adds is this seam, the ordered
143
+ * revoke step, and the describe() line. `revoke()` clears the grant's own
144
+ * `env` object IN PLACE — it does not, and cannot, un-inject an
145
+ * already-created container's process environment; §6.2 is explicit that
146
+ * "revocable" means nothing for a static key, and this is the honest
147
+ * implementation of that, not a `revoke()` that pretends otherwise.
148
+ */
149
+ export declare const staticBroker: CredentialBroker;
150
+ /** The registry `validateSandboxConfig` checks `sandbox.credentials.broker` against (agents.ts). */
151
+ export declare const KNOWN_CREDENTIAL_BROKER_IDS: readonly string[];
152
+ /**
153
+ * THE LOGGER CHANNEL. Module-level, keyed on adw_id, called from where a
154
+ * tracer is genuinely in hand (`agents.ts`'s `execute()`, immediately after
155
+ * the spec is built). Leases resolve `lease.log` through this map with a
156
+ * no-op fallback, so a lease created by a caller that never registered one
157
+ * still works and simply logs nowhere. `teardownRun(adwId)` DELETES the
158
+ * entry — otherwise `spf watch` would retain a closure over every Run it
159
+ * ever processed.
160
+ */
161
+ export declare function registerRunLog(adwId: string, log: SandboxLog): void;
162
+ /**
163
+ * Registers a created lease. THREE arguments — the logger is not one of
164
+ * them (see `SandboxLease.log`'s doc comment). Returns the mutable lease so
165
+ * the caller (an adapter) can set `provider_id`/`native` and push its own
166
+ * `teardown` steps once the underlying provider sandbox actually exists.
167
+ */
168
+ export declare function registerLease(key: string, spec: SandboxSpec, transport: SandboxTransport): SandboxLease;
169
+ /** Find-or-create lookup for adapters — not exported in the design doc's own code block, but required by it: `createSandbox`'s `hit`/`miss` branch reads exactly this. */
170
+ export declare function getLease(key: string): SandboxLease | undefined;
171
+ export declare function leases(): SandboxLease[];
172
+ /**
173
+ * Bounded renewal for the adapter's `hit` branch: `min(lifetime_seconds,
174
+ * remaining budget to max_total_lifetime_seconds)`. Throws — never silently
175
+ * truncates to zero — once the ceiling from creation has passed, so a
176
+ * long-lived `spf watch` process cannot renew a lease forever.
177
+ */
178
+ export declare function renewLease(lease: SandboxLease): Promise<void>;
179
+ export declare function teardownLease(lease: SandboxLease): Promise<void>;
180
+ /** Tears down every lease whose `spec.adw_id === adwId` — a SET, not a single lease (see design §4.3). */
181
+ export declare function teardownRun(adwId: string): Promise<void>;
182
+ export declare function teardownAll(): Promise<void>;
183
+ /**
184
+ * Per-RUN scope, keyed EXPLICITLY on the run's adw_id — never on ambient
185
+ * async context. Flue's claim loop is detached from the wrap site's own
186
+ * async context (see the design doc's §4.3 "Why not AsyncLocalStorage"), so
187
+ * `createSandbox` runs outside any `AsyncLocalStorage` scope a wrapper here
188
+ * could establish. Attribution instead rides data already in hand:
189
+ * `lease.spec.adw_id`, written at create time.
190
+ */
191
+ export declare function withRunScope<T>(adwId: string, fn: () => Promise<T>): Promise<T>;
192
+ /**
193
+ * SYNC. Returns a closure; performs NO I/O. Every provider call — the
194
+ * preflight, the seed, the `setup` commands — lives inside the returned
195
+ * `createSandbox`, which is the async boundary flue awaits itself.
196
+ */
197
+ export declare function factoryFor(spec: SandboxSpec): SandboxFactory;
198
+ /**
199
+ * Mandatory create-time check, not a "leaning": the transport below shells
200
+ * out to `git`, `tar` and `base64` inside the sandbox, so all three are hard
201
+ * requirements of ANY image, not a convenience. Run by both adapters before
202
+ * their seed.
203
+ */
204
+ export declare function preflight(spec: SandboxSpec, transport: SandboxTransport): Promise<void>;
205
+ /**
206
+ * Seeds a freshly created sandbox from the host tree and returns the
207
+ * fingerprint to store as `lease.seeded`. Steps mirror the design doc's
208
+ * §5.2 exactly (refuse-no-commits, mkdir scratch+handoff FIRST, the
209
+ * attributes-proof tar, extract, `git init`+`spf-base` tag, the uncommitted
210
+ * delta as `spf-local`, the handoff mirror's mandatory first push, then
211
+ * `setup`). Called by an adapter's `createSandbox` on the `miss` branch,
212
+ * against the raw driver/stub transport before any lease exists.
213
+ */
214
+ export declare function seedViaTransport(spec: SandboxSpec, transport: SandboxTransport): Promise<{
215
+ head_sha: string;
216
+ patch_sha256: string;
217
+ }>;
218
+ /**
219
+ * Compares the live host fingerprint against `lease.seeded` and applies
220
+ * exactly one of two remedies on a mismatch — conflating them is a
221
+ * data-loss bug (see the design doc's §5.4). A no-op when there is no lease
222
+ * yet (the first send of a run — the seed that follows is the sync point)
223
+ * OR when the fingerprint already matches (nothing has moved on the host
224
+ * since the last sync point, so the sandbox's handoff content — already
225
+ * synced by the last extract — cannot be stale either; no provider call at
226
+ * all in that case).
227
+ */
228
+ export declare function reconcileWorkspace(spec: SandboxSpec): Promise<void>;
229
+ /**
230
+ * Three ordered acts: stage+diff in-sandbox, apply on the host (atomic —
231
+ * `--allow-empty` is mandatory here, the opposite fix from the in-sandbox
232
+ * applies, because no commit follows it), then advance the sandbox's HEAD
233
+ * with a marker commit so the NEXT extract emits an increment rather than
234
+ * the cumulative delta again. A no-op when there is no lease yet.
235
+ */
236
+ export declare function extractWorkspace(spec: SandboxSpec): Promise<void>;