@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.
- package/README.md +56 -0
- package/assets/defaults/spf.config.yaml +75 -0
- package/assets/skill/references/config.md +98 -4
- package/dist/chains/index.d.ts +2 -0
- package/dist/chains/index.js +4 -0
- package/dist/cli/commands/doctor.js +339 -2
- package/dist/cli/commands/fanout.d.ts +7 -14
- package/dist/cli/commands/fanout.js +45 -39
- package/dist/cli/commands/loop.d.ts +2 -0
- package/dist/cli/commands/loop.js +198 -0
- package/dist/cli/commands/run.js +14 -4
- package/dist/cli/commands/watch.d.ts +29 -1
- package/dist/cli/commands/watch.js +219 -64
- package/dist/cli/index.js +14 -0
- package/dist/core/agent_cc.d.ts +11 -0
- package/dist/core/agent_cc.js +25 -2
- package/dist/core/agent_flue.js +14 -5
- package/dist/core/agents.d.ts +61 -1
- package/dist/core/agents.js +363 -6
- package/dist/core/data_types.d.ts +316 -0
- package/dist/core/data_types.js +143 -0
- package/dist/core/loop.d.ts +230 -0
- package/dist/core/loop.js +290 -0
- package/dist/core/quality.d.ts +1 -2
- package/dist/core/sandbox.d.ts +236 -0
- package/dist/core/sandbox.js +655 -0
- package/dist/core/sandbox_cloudflare.d.ts +137 -0
- package/dist/core/sandbox_cloudflare.js +505 -0
- package/dist/core/sandbox_opensandbox.d.ts +59 -0
- package/dist/core/sandbox_opensandbox.js +484 -0
- package/dist/core/sandbox_sdk_types.d.ts +171 -0
- package/dist/core/sandbox_sdk_types.js +20 -0
- package/dist/core/watch.d.ts +56 -0
- package/dist/core/watch.js +358 -52
- package/dist/core/worktree_data.d.ts +1 -0
- package/dist/core/worktree_data.js +37 -0
- 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>;
|