@letterblack/lbe-core 1.3.4 → 1.3.6
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/.githooks/pre-commit +2 -0
- package/.githooks/pre-push +2 -0
- package/CHANGELOG.md +81 -0
- package/LICENSE +1 -1
- package/README.md +158 -170
- package/RELEASE_WORKSPACE_RULES.md +179 -0
- package/Release-README.md +67 -0
- package/WORKSPACE.md +422 -0
- package/_proof.mjs +246 -0
- package/assets/runtime-boundary.svg +36 -36
- package/bin/lbe.js +12 -0
- package/config/identity.config.json +3 -0
- package/config/policy.default.json +24 -0
- package/dist/cli/lbe.js +4431 -0
- package/dist/hooks/register.cjs +505 -0
- package/dist/state/appendCentral.cjs +87 -0
- package/dist/state/index.cjs +101 -0
- package/exec/cli.js +472 -0
- package/exec/index.js +2 -0
- package/index.js +24 -0
- package/npm-pack.json +0 -0
- package/package.json +77 -45
- package/release/README.md +216 -0
- package/release/TRUST.md +90 -0
- package/release/exec-README.md +215 -0
- package/release/exec-types.d.ts +50 -0
- package/release-exec/LICENSE +1 -0
- package/release-exec/README.md +215 -0
- package/release-exec/assets/lbe-gates.jpg +0 -0
- package/release-exec/assets/lbe-gates.png +0 -0
- package/release-exec/assets/runtime-boundary.svg +36 -0
- package/release-exec/assets/story-allow.jpg +0 -0
- package/release-exec/assets/story-allow.png +0 -0
- package/release-exec/assets/story-deny.jpg +0 -0
- package/release-exec/assets/story-deny.png +0 -0
- package/release-exec/dist/cli.js +2841 -0
- package/release-exec/dist/index.js +1835 -0
- package/release-exec/dist/lbe_engine.wasm +0 -0
- package/{dist → release-exec/dist}/wasm.lock.json +4 -5
- package/release-exec/hooks/register.cjs +473 -0
- package/release-exec/package.json +35 -0
- package/release-exec/types.d.ts +50 -0
- package/runtime/engine.js +322 -0
- package/runtime/lbe_engine.wasm +0 -0
- package/src/cli/commands/assertConsumer.js +198 -0
- package/src/cli/commands/auditVerify.js +36 -0
- package/src/cli/commands/dryrun.js +175 -0
- package/src/cli/commands/health.js +153 -0
- package/src/cli/commands/init.js +306 -0
- package/src/cli/commands/integrityCheck.js +57 -0
- package/src/cli/commands/logs.js +53 -0
- package/src/cli/commands/openState.js +44 -0
- package/src/cli/commands/policyAdd.js +8 -0
- package/src/cli/commands/policyMode.js +7 -0
- package/src/cli/commands/policySign.js +72 -0
- package/src/cli/commands/proof.js +102 -0
- package/src/cli/commands/run.js +342 -0
- package/src/cli/commands/status.js +73 -0
- package/src/cli/commands/verify.js +144 -0
- package/src/cli/main.js +181 -0
- package/src/cli/parseArgs.js +115 -0
- package/src/exec/localExecutor.js +289 -0
- package/src/hooks/register.cjs +505 -0
- package/src/state/appendCentral.cjs +87 -0
- package/src/state/fileIndex.js +140 -0
- package/src/state/index.cjs +101 -0
- package/src/state/index.js +65 -0
- package/src/state/intentRegistry.js +84 -0
- package/src/state/migration.js +112 -0
- package/src/state/proofRunner.js +246 -0
- package/src/state/stateRoot.js +40 -0
- package/src/state/targetRegistry.js +109 -0
- package/src/state/workspaceId.js +40 -0
- package/src/state/workspaceRegistry.js +65 -0
- package/types.d.ts +175 -2
- package/dist/cli.js +0 -141
- package/dist/index.js +0 -52
- /package/dist/{lbe_engine.wasm → cli/lbe_engine.wasm} +0 -0
package/WORKSPACE.md
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# LetterBlack LBE — Workspace Instructions
|
|
2
|
+
|
|
3
|
+
This document covers every aspect of the private workspace:
|
|
4
|
+
structure, development, testing, build, release, and governance.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## What this is
|
|
9
|
+
|
|
10
|
+
**LBE (Local-first execution Governance Engine)** puts a deterministic
|
|
11
|
+
validation gate between what an AI agent proposes and what the host system
|
|
12
|
+
actually executes. Every action — file write, shell command, anything — passes
|
|
13
|
+
a 7-gate pipeline before execution. Nothing runs unless the gate passes.
|
|
14
|
+
|
|
15
|
+
There are two published packages and one private source workspace:
|
|
16
|
+
|
|
17
|
+
| Package | npm | What it is |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `@letterblack/lbe-sdk` | published | WASM runtime + CLI only. Raw `execute()` function, no controller. |
|
|
20
|
+
| `@letterblack/lbe-exec` | published | Full in-process controller. `createLocalExecutor()`, policy, audit, sandbox. |
|
|
21
|
+
| `letterblack-lbe-core` | private | This workspace. Source for both packages. Never published. |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Workspace layout
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
letterblack-sentinel/
|
|
29
|
+
│
|
|
30
|
+
├── assets/ Visual assets (source of truth — built into both packages)
|
|
31
|
+
│ ├── lbe-gates.png Gate sequence diagram (Request → Policy → Identity → Scope → Action)
|
|
32
|
+
│ ├── story-allow.png Happy-path storyboard (6 panels)
|
|
33
|
+
│ ├── story-deny.png Deny-path storyboard (6 panels)
|
|
34
|
+
│ └── runtime-boundary.svg WASM boundary diagram
|
|
35
|
+
│
|
|
36
|
+
├── src/
|
|
37
|
+
│ ├── core/ Private controller internals
|
|
38
|
+
│ │ ├── validator.js 7-gate orchestrator — extracts flags, calls WASM
|
|
39
|
+
│ │ ├── localPolicy.js lbe.policy.json read/write, deny-wins evaluation
|
|
40
|
+
│ │ ├── auditLog.js SHA-256 hash-chained JSONL append
|
|
41
|
+
│ │ ├── signature.js Ed25519 sign/verify (tweetnacl)
|
|
42
|
+
│ │ ├── atomicWrite.js Write-then-rename for all state files
|
|
43
|
+
│ │ ├── policyEngine.js Deployment policy evaluation
|
|
44
|
+
│ │ ├── policySignature.js Policy signing and tamper detection
|
|
45
|
+
│ │ ├── policyVersionGuard.js Version bump / rollback protection
|
|
46
|
+
│ │ ├── nonceStore.js Single-use nonce registry
|
|
47
|
+
│ │ ├── requestRateLimiter.js Per-requester rate limiting
|
|
48
|
+
│ │ ├── invariants.js Invariant gate (key present? policy signed?)
|
|
49
|
+
│ │ ├── backup.js Pre-write backup and restore
|
|
50
|
+
│ │ ├── trustedKeys.js Key store management
|
|
51
|
+
│ │ ├── integrity.js Workspace integrity manifest
|
|
52
|
+
│ │ ├── workspaceScanner.js Workspace file enumeration
|
|
53
|
+
│ │ ├── schema.js Request schema constants
|
|
54
|
+
│ │ ├── logger.js Structured logger
|
|
55
|
+
│ │ ├── deepFreeze.js Freeze policy/config objects
|
|
56
|
+
│ │ └── approval-token.js Short-lived approval tokens
|
|
57
|
+
│ │
|
|
58
|
+
│ ├── adapters/ Execution adapters (decide nothing, only execute)
|
|
59
|
+
│ │ ├── fileAdapter.js read / write / patch / delete inside rootDir
|
|
60
|
+
│ │ ├── shellAdapter.js run an allowlisted shell command
|
|
61
|
+
│ │ ├── noopAdapter.js dry-run / observer sink
|
|
62
|
+
│ │ └── index.js Adapter router
|
|
63
|
+
│ │
|
|
64
|
+
│ ├── exec/
|
|
65
|
+
│ │ └── localExecutor.js createLocalExecutor() — in-process controller
|
|
66
|
+
│ │ (source for @letterblack/lbe-exec)
|
|
67
|
+
│ │
|
|
68
|
+
│ └── cli/
|
|
69
|
+
│ ├── parseArgs.js Argument parser
|
|
70
|
+
│ └── commands/ One file per CLI command
|
|
71
|
+
│ ├── init.js npx lbe init
|
|
72
|
+
│ ├── policyMode.js npx lbe observe / enforce
|
|
73
|
+
│ ├── policyAdd.js npx lbe policy add
|
|
74
|
+
│ ├── run.js npx lbe execute
|
|
75
|
+
│ ├── dryrun.js npx lbe dryrun
|
|
76
|
+
│ ├── verify.js npx lbe verify
|
|
77
|
+
│ ├── health.js npx lbe health
|
|
78
|
+
│ ├── auditVerify.js npx lbe audit-verify
|
|
79
|
+
│ ├── integrityCheck.js npx lbe integrity-check
|
|
80
|
+
│ └── policySign.js npx lbe policy-sign
|
|
81
|
+
│
|
|
82
|
+
├── exec/
|
|
83
|
+
│ └── index.js Public entrypoint for @letterblack/lbe-exec
|
|
84
|
+
│
|
|
85
|
+
├── runtime/
|
|
86
|
+
│ ├── lbe_engine.wasm Compiled WASM binary (output of build:engine)
|
|
87
|
+
│ └── engine.js JS WASM loader — exposes validate_pipeline etc.
|
|
88
|
+
│
|
|
89
|
+
├── native/
|
|
90
|
+
│ └── lbe-engine/ Rust crate — compiled to lbe_engine.wasm
|
|
91
|
+
│ ├── Cargo.toml crate-type = cdylib, publish = false
|
|
92
|
+
│ └── src/lib.rs All governance decision logic lives here
|
|
93
|
+
│
|
|
94
|
+
├── bin/
|
|
95
|
+
│ └── lbe.js CLI entry (private — never ships in public packages)
|
|
96
|
+
│
|
|
97
|
+
├── config/
|
|
98
|
+
│ ├── policy.default.json Default policy template (ships in lbe-sdk)
|
|
99
|
+
│ └── identity.config.json Workspace identity config (private)
|
|
100
|
+
│
|
|
101
|
+
├── test/
|
|
102
|
+
│ ├── local-executor.test.js 40 tests — observe/enforce/deny-wins/sandbox/audit
|
|
103
|
+
│ ├── local-policy.test.js Policy file read/write/evaluate
|
|
104
|
+
│ ├── public-api.test.js WASM execute() contract
|
|
105
|
+
│ ├── security-invariants.test.js Key lifecycle, policy signature, version guard
|
|
106
|
+
│ ├── invariant-backup.test.js Backup and rollback
|
|
107
|
+
│ └── shell-adapter.test.js Shell metacharacter safety
|
|
108
|
+
│
|
|
109
|
+
├── scripts/
|
|
110
|
+
│ ├── build-engine.js cargo build → runtime/lbe_engine.wasm
|
|
111
|
+
│ ├── build-public-sdk.mjs Builds release-public/ (@letterblack/lbe-sdk)
|
|
112
|
+
│ ├── build-public-exec.mjs Builds release-exec/ (@letterblack/lbe-exec)
|
|
113
|
+
│ ├── check-public-artifact.mjs Validates release-public/ before publish
|
|
114
|
+
│ ├── check-public-exec.mjs Validates release-exec/ before publish
|
|
115
|
+
│ ├── mainhead-guard.mjs Blocks commits/pushes from non-main branches
|
|
116
|
+
│ └── install-git-hooks.mjs Installs pre-commit and pre-push hooks
|
|
117
|
+
│
|
|
118
|
+
├── docs/
|
|
119
|
+
│ ├── decisions/
|
|
120
|
+
│ │ ├── ADR-001-remove-mcp-execution-surface.md
|
|
121
|
+
│ │ ├── ADR-002-remove-http-server-surface.md
|
|
122
|
+
│ │ └── ADR-003-sdk-only-product-boundary.md
|
|
123
|
+
│ └── governance/
|
|
124
|
+
│ └── mainhead.md Branch authority rules
|
|
125
|
+
│
|
|
126
|
+
├── release/ README and type templates (source for builds)
|
|
127
|
+
│ ├── README.md Template for @letterblack/lbe-sdk README
|
|
128
|
+
│ ├── exec-README.md Template for @letterblack/lbe-exec README
|
|
129
|
+
│ └── exec-types.d.ts Type declarations for @letterblack/lbe-exec
|
|
130
|
+
│
|
|
131
|
+
├── release-public/ Built artifact — @letterblack/lbe-sdk (never edit directly)
|
|
132
|
+
├── release-exec/ Built artifact — @letterblack/lbe-exec (never edit directly)
|
|
133
|
+
│
|
|
134
|
+
├── types.d.ts Private full type declarations
|
|
135
|
+
├── index.js Private package entry
|
|
136
|
+
├── package.json Private workspace package (letterblack-lbe-core)
|
|
137
|
+
├── CHANGELOG.md
|
|
138
|
+
└── WORKSPACE.md This file
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Prerequisites
|
|
144
|
+
|
|
145
|
+
| Tool | Version | Purpose |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| Node.js | ≥ 20.9.0 | Runtime and test runner |
|
|
148
|
+
| Rust / cargo | stable | Compile WASM engine |
|
|
149
|
+
| rustup target | `wasm32-unknown-unknown` | WASM build target (auto-added by build:engine) |
|
|
150
|
+
| npm | any | Package management and publish |
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npm install
|
|
154
|
+
npm run hooks:install # installs pre-commit and pre-push git hooks — do this once after clone
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Development commands
|
|
160
|
+
|
|
161
|
+
### Daily workflow
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npm test # run all 40 tests
|
|
165
|
+
npm run lint # ESLint on src/ and bin/
|
|
166
|
+
npm run validate:all # mainhead guard + engine check + lint + test (full gate)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### WASM engine
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npm run build:engine # compile native/lbe-engine → runtime/lbe_engine.wasm
|
|
173
|
+
npm run engine:check # verify the WASM binary loads and exports the expected functions
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Only needed when `native/lbe-engine/src/lib.rs` changes. The compiled binary
|
|
177
|
+
`runtime/lbe_engine.wasm` is committed and ships in both packages.
|
|
178
|
+
|
|
179
|
+
### CLI (local development)
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
node bin/lbe.js init # create lbe.policy.json in observer mode
|
|
183
|
+
node bin/lbe.js status # mode, rule count, audit entry count
|
|
184
|
+
node bin/lbe.js policy # list rules
|
|
185
|
+
node bin/lbe.js observe # switch to observer mode
|
|
186
|
+
node bin/lbe.js enforce # switch to enforcement mode
|
|
187
|
+
node bin/lbe.js health --json true
|
|
188
|
+
node bin/lbe.js audit-verify
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Or via npm aliases: `npm run init`, `npm run verify`, `npm run health`.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Test suite
|
|
196
|
+
|
|
197
|
+
**40 tests, 0 failures.** Run with `npm test` (uses native Node.js test runner).
|
|
198
|
+
|
|
199
|
+
| File | Coverage |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `local-executor.test.js` | observe mode, enforce mode, deny-wins, sandbox, advisory vs controller split, shell allowlist, symlink escape, dryRun, audit |
|
|
202
|
+
| `local-policy.test.js` | policy load/write/evaluate, deny-wins, observer/enforce defaults |
|
|
203
|
+
| `public-api.test.js` | raw WASM `execute()` input/output contract |
|
|
204
|
+
| `security-invariants.test.js` | Ed25519 key lifecycle, policy signature tamper, version rollback guard |
|
|
205
|
+
| `invariant-backup.test.js` | backup creation, restore on failure, atomic write |
|
|
206
|
+
| `shell-adapter.test.js` | metacharacter arguments treated as data, not shell syntax |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Architecture — the 7-gate pipeline
|
|
211
|
+
|
|
212
|
+
Every request to `execute()` passes these gates in order. A failure at any
|
|
213
|
+
gate returns a structured deny — the remaining gates are not evaluated.
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
[1] Schema — required fields, structural validity
|
|
217
|
+
[2] Timestamp — permitted clock-skew window (±10 minutes)
|
|
218
|
+
[3] Key lifecycle — trusted key, active, not expired
|
|
219
|
+
[4] Signature — Ed25519 signature verified against key store
|
|
220
|
+
[5] Rate limit — per-requester sliding-window limit
|
|
221
|
+
[6] Nonce — single-use commandId replay protection
|
|
222
|
+
[7] Policy — configured authorization decision (deny-wins)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The WASM engine (`runtime/lbe_engine.wasm`) owns all gate decisions.
|
|
226
|
+
`src/core/validator.js` extracts boolean flag sets from the command object
|
|
227
|
+
and calls the WASM functions. The JS layer does IO; the Rust layer decides.
|
|
228
|
+
|
|
229
|
+
### Local policy layer (lbe-exec only)
|
|
230
|
+
|
|
231
|
+
Before the 7-gate pipeline runs, `createLocalExecutor()` evaluates
|
|
232
|
+
`lbe.policy.json`. This is a separate, simpler rule ledger:
|
|
233
|
+
|
|
234
|
+
- Rules have `effect: 'allow' | 'deny'`, `type: 'path' | 'command'`, `pattern`
|
|
235
|
+
- **Deny always wins** over allow regardless of rule order
|
|
236
|
+
- Only the host application writes rules (`addRule()`). Agents may only call
|
|
237
|
+
`proposeRule()` which returns a proposal object — it never writes to disk.
|
|
238
|
+
|
|
239
|
+
### Observer vs enforce mode
|
|
240
|
+
|
|
241
|
+
| Mode | Gates run | Audit written | Mutations |
|
|
242
|
+
|---|---|---|---|
|
|
243
|
+
| `observe` | yes | yes | **no** — short-circuits before adapter |
|
|
244
|
+
| `enforce` | yes | yes | yes — adapter executes on pass |
|
|
245
|
+
|
|
246
|
+
Default when no `lbe.policy.json` exists: **enforce**.
|
|
247
|
+
`npx lbe init` creates `lbe.policy.json` in **observe** mode.
|
|
248
|
+
|
|
249
|
+
### Project-root sandbox
|
|
250
|
+
|
|
251
|
+
All file operations are constrained to `rootDir`. The check resolves
|
|
252
|
+
symlinks before comparing paths (`physicalPath()`), so symlink escapes and
|
|
253
|
+
`../` traversal are blocked before policy is consulted.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Key security properties
|
|
258
|
+
|
|
259
|
+
| Property | Implementation |
|
|
260
|
+
|---|---|
|
|
261
|
+
| Ed25519 signatures | Every proposal signed; WASM verifies before any gate runs |
|
|
262
|
+
| Nonce replay protection | Each `commandId` is single-use |
|
|
263
|
+
| Rate limiting | Per-requester sliding window, configurable |
|
|
264
|
+
| Timestamp skew guard | Rejects proposals outside ±10 minutes |
|
|
265
|
+
| Policy signature verification | Policy file is signed; tampering fails the invariant gate |
|
|
266
|
+
| Immutable audit trail | SHA-256 hash-chained JSONL; deletion or edit is detectable |
|
|
267
|
+
| Atomic writes | All state files use write-then-rename; no partial state |
|
|
268
|
+
| Project-root sandbox | Symlink-safe path resolution in `physicalPath()` |
|
|
269
|
+
| Deny-wins conflict resolution | When allow + deny both match, deny always takes precedence |
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Build system
|
|
274
|
+
|
|
275
|
+
### @letterblack/lbe-sdk
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
npm run build:public-sdk # → release-public/
|
|
279
|
+
npm run verify:public-sdk # build + validate + npm pack --dry-run
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
What the build does:
|
|
283
|
+
1. Bundles `index.js` and `bin/lbe.js` with esbuild → `release-public/dist/`
|
|
284
|
+
2. Copies `runtime/lbe_engine.wasm` and generates `wasm.lock.json`
|
|
285
|
+
3. Copies `assets/` from workspace root → `release-public/assets/`
|
|
286
|
+
4. Copies `release/README.md` (replaces `{{PACKAGE_NAME}}` placeholder) → `release-public/README.md`
|
|
287
|
+
5. Copies `config/policy.default.json` and `types.d.ts`
|
|
288
|
+
6. Writes `release-public/package.json` from workspace `package.json`
|
|
289
|
+
|
|
290
|
+
The validator (`check-public-artifact.mjs`) confirms:
|
|
291
|
+
- No `src/`, `test/`, `scripts/`, `keys/`, `data/` paths in the artifact
|
|
292
|
+
- No private path markers in the bundled JS
|
|
293
|
+
- WASM SHA-256 matches `wasm.lock.json`
|
|
294
|
+
- All required files present
|
|
295
|
+
|
|
296
|
+
### @letterblack/lbe-exec
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
npm run build:public-exec # → release-exec/
|
|
300
|
+
npm run verify:public-exec # build + validate + npm pack --dry-run
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
What the build does:
|
|
304
|
+
1. Bundles `exec/index.js` with esbuild → `release-exec/dist/index.js`
|
|
305
|
+
(external: tweetnacl, json-canonicalize — these are runtime dependencies)
|
|
306
|
+
2. Copies `runtime/lbe_engine.wasm` and generates `wasm.lock.json`
|
|
307
|
+
3. Copies `assets/` from workspace root → `release-exec/assets/`
|
|
308
|
+
4. Copies `release/exec-README.md` → `release-exec/README.md`
|
|
309
|
+
5. Copies `release/exec-types.d.ts` → `release-exec/types.d.ts`
|
|
310
|
+
6. Writes `release-exec/package.json` from workspace `package.json`
|
|
311
|
+
|
|
312
|
+
The validator (`check-public-exec.mjs`) confirms:
|
|
313
|
+
- No private paths or markers in the bundle
|
|
314
|
+
- WASM SHA-256 matches `wasm.lock.json`
|
|
315
|
+
- All required files present
|
|
316
|
+
|
|
317
|
+
### What never ships
|
|
318
|
+
|
|
319
|
+
Neither package contains: `src/`, `test/`, `scripts/`, `keys/`, `data/`,
|
|
320
|
+
`node_modules/`, source maps, or private adapters.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Release process
|
|
325
|
+
|
|
326
|
+
Both packages are published from their respective `release-*/` directories.
|
|
327
|
+
**Never publish from the workspace root.**
|
|
328
|
+
|
|
329
|
+
### Full release checklist
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# 1. All gates pass
|
|
333
|
+
npm run validate:all
|
|
334
|
+
|
|
335
|
+
# 2. Version bump — update in package.json (single source of truth)
|
|
336
|
+
# Build scripts read this version for both release packages.
|
|
337
|
+
|
|
338
|
+
# 3. Build and validate both artifacts
|
|
339
|
+
npm run verify:public-sdk
|
|
340
|
+
npm run verify:public-exec
|
|
341
|
+
|
|
342
|
+
# 4. Commit and push to main
|
|
343
|
+
git add -p
|
|
344
|
+
git commit -m "release: vX.Y.Z"
|
|
345
|
+
git push
|
|
346
|
+
|
|
347
|
+
# 5. Tag
|
|
348
|
+
git tag vX.Y.Z
|
|
349
|
+
git push --tags
|
|
350
|
+
|
|
351
|
+
# 6. Publish (from release directories, not workspace root)
|
|
352
|
+
cd release-public && npm publish
|
|
353
|
+
cd ../release-exec && npm publish
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Version is set once** in `package.json` at the workspace root.
|
|
357
|
+
Both build scripts read `sourcePackage.version` from there — no manual
|
|
358
|
+
sync needed between the two release `package.json` files.
|
|
359
|
+
|
|
360
|
+
### Auth
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
npm login # or set NPM_TOKEN environment variable
|
|
364
|
+
# registry: https://registry.npmjs.org/
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Current published versions
|
|
368
|
+
|
|
369
|
+
| Package | Version |
|
|
370
|
+
|---|---|
|
|
371
|
+
| `@letterblack/lbe-sdk` | 1.2.3 |
|
|
372
|
+
| `@letterblack/lbe-exec` | 1.2.3 |
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Branch governance
|
|
377
|
+
|
|
378
|
+
`main` is the only authoritative branch. The pre-commit and pre-push hooks
|
|
379
|
+
run `scripts/mainhead-guard.mjs` and will block any commit or push from a
|
|
380
|
+
non-main branch or linked worktree.
|
|
381
|
+
|
|
382
|
+
Rules:
|
|
383
|
+
- Never commit from a feature branch — commit directly to `main`
|
|
384
|
+
- Never use `--no-verify` to skip hooks — fix the underlying issue
|
|
385
|
+
- Never publish from a branch, tag, or worktree other than `main`
|
|
386
|
+
- The `mainhead-guard` check is also run at the start of the build scripts
|
|
387
|
+
|
|
388
|
+
Install the hooks once after cloning:
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
npm run hooks:install
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## README authoring
|
|
397
|
+
|
|
398
|
+
Source files (edit these — never edit the built outputs directly):
|
|
399
|
+
|
|
400
|
+
| Source | Builds into |
|
|
401
|
+
|---|---|
|
|
402
|
+
| `release/README.md` | `release-public/README.md` (with `{{PACKAGE_NAME}}` replaced) |
|
|
403
|
+
| `release/exec-README.md` | `release-exec/README.md` |
|
|
404
|
+
| `release/exec-types.d.ts` | `release-exec/types.d.ts` |
|
|
405
|
+
| `types.d.ts` | private package only |
|
|
406
|
+
|
|
407
|
+
Images live in `assets/` at the workspace root and are copied into both
|
|
408
|
+
release packages during build. Reference them in READMEs as `assets/<file>`.
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## What is not in scope
|
|
413
|
+
|
|
414
|
+
LBE governs actions routed through its runtime. It does not provide:
|
|
415
|
+
|
|
416
|
+
- Kernel-level process isolation
|
|
417
|
+
- Network egress control
|
|
418
|
+
- Multi-tenant workload separation
|
|
419
|
+
- A hosted control plane, daemon, or HTTP API
|
|
420
|
+
- An MCP server surface (see ADR-001)
|
|
421
|
+
|
|
422
|
+
See `docs/decisions/` for the rationale behind each removed surface.
|
package/_proof.mjs
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Proof matrix for LBE preload hook
|
|
2
|
+
// Run with: node _proof.mjs
|
|
3
|
+
// All tests use isolated temp dirs. No shared state.
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { spawnSync, spawn } from 'child_process';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const HOOK = path.join(ROOT, 'src', 'hooks', 'register.cjs');
|
|
13
|
+
const HOOK_FWD = HOOK.replace(/\\/g, '/');
|
|
14
|
+
|
|
15
|
+
let passed = 0, failed = 0;
|
|
16
|
+
|
|
17
|
+
function result(name, ok, detail = '') {
|
|
18
|
+
const mark = ok ? '✓' : '✗';
|
|
19
|
+
console.log(` ${mark} ${name}${detail ? ' — ' + detail : ''}`);
|
|
20
|
+
if (ok) passed++; else failed++;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function tmpDir(policy = null) {
|
|
24
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'lbe-proof-'));
|
|
25
|
+
fs.mkdirSync(path.join(d, '.lbe'), { recursive: true });
|
|
26
|
+
fs.writeFileSync(path.join(d, '.lbe', 'policy.json'),
|
|
27
|
+
JSON.stringify(policy || { version: 1, mode: 'observe', workspace: d, rules: [] }));
|
|
28
|
+
return d;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function runNode(dir, code, mode = 'observe') {
|
|
32
|
+
return spawnSync(process.execPath, ['-e', code], {
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
NODE_OPTIONS: `--require "${HOOK_FWD}"`,
|
|
37
|
+
LBE_ROOT: dir,
|
|
38
|
+
LBE_MODE: mode,
|
|
39
|
+
},
|
|
40
|
+
cwd: dir,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function auditEntries(dir) {
|
|
45
|
+
const f = path.join(dir, '.lbe', 'events.jsonl');
|
|
46
|
+
if (!fs.existsSync(f)) return [];
|
|
47
|
+
return fs.readFileSync(f, 'utf8').split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── 1. fs.writeFile (callback) ──────────────────────────────────────────────
|
|
51
|
+
{
|
|
52
|
+
const dir = tmpDir();
|
|
53
|
+
const r = runNode(dir, `
|
|
54
|
+
const fs = require('fs');
|
|
55
|
+
fs.writeFile('out.txt', 'hello', err => { if (err) process.exit(1); });
|
|
56
|
+
`);
|
|
57
|
+
const entries = auditEntries(dir);
|
|
58
|
+
const wrote = fs.existsSync(path.join(dir, 'out.txt'));
|
|
59
|
+
const logged = entries.some(e => e.action === 'file_write');
|
|
60
|
+
result('fs.writeFile', wrote && logged && r.status === 0,
|
|
61
|
+
wrote ? (logged ? 'file written + logged' : 'file written, NOT logged') : 'file not written');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── 2. fs.promises.writeFile ────────────────────────────────────────────────
|
|
65
|
+
{
|
|
66
|
+
const dir = tmpDir();
|
|
67
|
+
const r = runNode(dir, `
|
|
68
|
+
const fs = require('fs');
|
|
69
|
+
(async () => {
|
|
70
|
+
await fs.promises.writeFile('out2.txt', 'async');
|
|
71
|
+
})().catch(() => process.exit(1));
|
|
72
|
+
`);
|
|
73
|
+
// Give async time to flush
|
|
74
|
+
const entries = auditEntries(dir);
|
|
75
|
+
const wrote = fs.existsSync(path.join(dir, 'out2.txt'));
|
|
76
|
+
const logged = entries.some(e => e.action === 'file_write');
|
|
77
|
+
result('fs.promises.writeFile', wrote && logged && r.status === 0,
|
|
78
|
+
wrote ? (logged ? 'file written + logged' : 'file written, NOT logged') : 'file not written');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── 3. fs.rm ────────────────────────────────────────────────────────────────
|
|
82
|
+
{
|
|
83
|
+
const dir = tmpDir();
|
|
84
|
+
fs.writeFileSync(path.join(dir, 'del.txt'), 'x');
|
|
85
|
+
const r = runNode(dir, `
|
|
86
|
+
const fs = require('fs');
|
|
87
|
+
fs.rm('del.txt', err => { if (err) process.exit(1); });
|
|
88
|
+
`);
|
|
89
|
+
const entries = auditEntries(dir);
|
|
90
|
+
const deleted = !fs.existsSync(path.join(dir, 'del.txt'));
|
|
91
|
+
const logged = entries.some(e => e.action === 'file_delete');
|
|
92
|
+
result('fs.rm', deleted && logged && r.status === 0,
|
|
93
|
+
deleted ? (logged ? 'file deleted + logged' : 'file deleted, NOT logged') : 'file not deleted');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── 4. fs.rename ────────────────────────────────────────────────────────────
|
|
97
|
+
{
|
|
98
|
+
const dir = tmpDir();
|
|
99
|
+
fs.writeFileSync(path.join(dir, 'a.txt'), 'x');
|
|
100
|
+
const r = runNode(dir, `
|
|
101
|
+
const fs = require('fs');
|
|
102
|
+
fs.rename('a.txt', 'b.txt', err => { if (err) process.exit(1); });
|
|
103
|
+
`);
|
|
104
|
+
const entries = auditEntries(dir);
|
|
105
|
+
const renamed = !fs.existsSync(path.join(dir, 'a.txt')) && fs.existsSync(path.join(dir, 'b.txt'));
|
|
106
|
+
const logged = entries.some(e => e.action === 'file_rename');
|
|
107
|
+
result('fs.rename', renamed && logged && r.status === 0,
|
|
108
|
+
renamed ? (logged ? 'renamed + logged' : 'renamed, NOT logged') : 'rename failed');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── 5. child_process.spawn ──────────────────────────────────────────────────
|
|
112
|
+
{
|
|
113
|
+
const dir = tmpDir();
|
|
114
|
+
const r = runNode(dir, `
|
|
115
|
+
const { spawn } = require('child_process');
|
|
116
|
+
const child = spawn(process.execPath, ['--version']);
|
|
117
|
+
let out = '';
|
|
118
|
+
child.stdout.on('data', d => out += d);
|
|
119
|
+
child.on('close', code => {
|
|
120
|
+
if (code !== 0) process.exit(1);
|
|
121
|
+
});
|
|
122
|
+
`);
|
|
123
|
+
const entries = auditEntries(dir);
|
|
124
|
+
const logged = entries.some(e => e.action === 'run_shell');
|
|
125
|
+
result('spawn', r.status === 0 && logged,
|
|
126
|
+
logged ? 'spawn logged' : 'spawn NOT logged');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── 6. child_process.exec ───────────────────────────────────────────────────
|
|
130
|
+
{
|
|
131
|
+
const dir = tmpDir();
|
|
132
|
+
// execSync is synchronous — quote path for Windows spaces
|
|
133
|
+
const r = runNode(dir, `
|
|
134
|
+
const { execSync } = require('child_process');
|
|
135
|
+
execSync('"' + process.execPath + '" --version');
|
|
136
|
+
`);
|
|
137
|
+
const entries = auditEntries(dir);
|
|
138
|
+
const logged = entries.some(e => e.action === 'run_shell');
|
|
139
|
+
result('exec / execSync', r.status === 0 && logged,
|
|
140
|
+
logged ? 'execSync logged' : 'execSync NOT logged');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── 7. observe mode — writes still execute ──────────────────────────────────
|
|
144
|
+
{
|
|
145
|
+
const dir = tmpDir();
|
|
146
|
+
const r = runNode(dir, `
|
|
147
|
+
require('fs').writeFileSync('observe.txt', 'x');
|
|
148
|
+
`, 'observe');
|
|
149
|
+
const entries = auditEntries(dir);
|
|
150
|
+
const wrote = fs.existsSync(path.join(dir, 'observe.txt'));
|
|
151
|
+
const e = entries.find(e => e.action === 'file_write');
|
|
152
|
+
const enforced = e && e.enforced === false;
|
|
153
|
+
result('observe mode (write executes)', wrote && enforced,
|
|
154
|
+
wrote ? (enforced ? 'executed=true, enforced=false' : 'wrong enforced flag') : 'file not written');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── 8. enforce mode — deny rule blocks write ─────────────────────────────────
|
|
158
|
+
{
|
|
159
|
+
const dir = tmpDir();
|
|
160
|
+
// Overwrite policy with deny-all enforce rule
|
|
161
|
+
fs.writeFileSync(path.join(dir, '.lbe', 'policy.json'), JSON.stringify({
|
|
162
|
+
version: 1, mode: 'enforce', workspace: dir,
|
|
163
|
+
rules: [{ id: 'r1', effect: 'deny', type: 'path', pattern: '**', from: 'test' }]
|
|
164
|
+
}));
|
|
165
|
+
const r = runNode(dir, `
|
|
166
|
+
try { require('fs').writeFileSync('blocked.txt', 'x'); }
|
|
167
|
+
catch(e) { /* expected */ }
|
|
168
|
+
`, 'enforce');
|
|
169
|
+
const entries = auditEntries(dir);
|
|
170
|
+
const notWritten = !fs.existsSync(path.join(dir, 'blocked.txt'));
|
|
171
|
+
const e = entries.find(e => e.decision === 'deny');
|
|
172
|
+
result('enforce mode (deny blocks)', notWritten && !!e,
|
|
173
|
+
notWritten ? (e ? 'blocked + logged' : 'blocked, NOT logged') : 'file was written (not blocked)');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── 9. no recursion — one write = one audit entry ───────────────────────────
|
|
177
|
+
{
|
|
178
|
+
const dir = tmpDir();
|
|
179
|
+
const r = runNode(dir, `
|
|
180
|
+
require('fs').writeFileSync('single.txt', 'x');
|
|
181
|
+
`);
|
|
182
|
+
const entries = auditEntries(dir).filter(e => e.action === 'file_write' && e.path && e.path.endsWith('single.txt'));
|
|
183
|
+
result('no recursion', entries.length === 1,
|
|
184
|
+
`${entries.length} audit entries for 1 writeFileSync (expected 1)`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── 10. status PID detection ────────────────────────────────────────────────
|
|
188
|
+
{
|
|
189
|
+
const dir = tmpDir();
|
|
190
|
+
// Start a background node process with the hook
|
|
191
|
+
const child = spawn(process.execPath, ['-e', `
|
|
192
|
+
// keep alive for 3 seconds
|
|
193
|
+
setTimeout(() => {}, 3000);
|
|
194
|
+
`], {
|
|
195
|
+
env: { ...process.env, NODE_OPTIONS: `--require "${HOOK_FWD}"`, LBE_ROOT: dir, LBE_MODE: 'observe' },
|
|
196
|
+
cwd: dir,
|
|
197
|
+
stdio: 'ignore',
|
|
198
|
+
detached: false,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Wait briefly for hook to write status file
|
|
202
|
+
await new Promise(r => setTimeout(r, 800));
|
|
203
|
+
|
|
204
|
+
const statusFile = path.join(dir, '.lbe', 'runtime', 'hook-status.json');
|
|
205
|
+
let pidAlive = false;
|
|
206
|
+
let statusOk = false;
|
|
207
|
+
if (fs.existsSync(statusFile)) {
|
|
208
|
+
const s = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
209
|
+
statusOk = true;
|
|
210
|
+
try { process.kill(s.pid, 0); pidAlive = true; } catch (_) {}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
result('status PID (alive)', statusOk && pidAlive,
|
|
214
|
+
statusOk ? (pidAlive ? `PID ${JSON.parse(fs.readFileSync(statusFile,'utf8')).pid} alive` : 'PID dead') : 'hook-status.json missing');
|
|
215
|
+
|
|
216
|
+
child.kill();
|
|
217
|
+
await new Promise(r => setTimeout(r, 300));
|
|
218
|
+
|
|
219
|
+
// After kill, PID should be gone
|
|
220
|
+
let pidGone = false;
|
|
221
|
+
if (fs.existsSync(statusFile)) {
|
|
222
|
+
const s = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
223
|
+
try { process.kill(s.pid, 0); } catch (_) { pidGone = true; }
|
|
224
|
+
}
|
|
225
|
+
result('status PID (stale after kill)', statusOk && pidGone,
|
|
226
|
+
pidGone ? 'PID correctly gone' : 'PID still appears alive');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── 11. clean workspace ──────────────────────────────────────────────────────
|
|
230
|
+
{
|
|
231
|
+
const dir = tmpDir();
|
|
232
|
+
runNode(dir, `require('fs').writeFileSync('x.txt','x');`);
|
|
233
|
+
|
|
234
|
+
const rootFiles = fs.readdirSync(dir);
|
|
235
|
+
const lbeFiles = fs.existsSync(path.join(dir, '.lbe')) ? fs.readdirSync(path.join(dir, '.lbe')) : [];
|
|
236
|
+
const badRootFiles = rootFiles.filter(f =>
|
|
237
|
+
f !== '.lbe' && f !== 'x.txt' &&
|
|
238
|
+
(f.startsWith('lbe') || f === 'CLAUDE.md' || f.startsWith('_lbe'))
|
|
239
|
+
);
|
|
240
|
+
result('clean workspace', badRootFiles.length === 0,
|
|
241
|
+
badRootFiles.length ? 'unexpected root files: ' + badRootFiles.join(', ') : 'no LBE pollution in root');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
245
|
+
console.log(`\n${passed + failed} tests ✓ ${passed} passed ${failed > 0 ? '✗ ' + failed + ' failed' : ''}`);
|
|
246
|
+
if (failed > 0) process.exit(1);
|