@logicseek/hello-cc 1.0.0 → 1.0.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 +77 -0
- package/README.md +32 -0
- package/README.zh-CN.md +31 -0
- package/docs/guide.md +58 -0
- package/docs/guide.zh-CN.md +55 -0
- package/lib/cli/app.mjs +2 -0
- package/lib/cli/commands/coordination.mjs +3 -1
- package/lib/cli/commands/gc.mjs +182 -32
- package/lib/cli/commands/misc.mjs +15 -0
- package/lib/cli/commands/peer.mjs +21 -5
- package/lib/process/identity.mjs +30 -0
- package/lib/runtime/buffer-directory-lease.mjs +2 -1
- package/lib/runtime/client.mjs +3 -0
- package/lib/shared/file-lock.mjs +11 -4
- package/lib/shared/socket-lock-worker.mjs +73 -24
- package/lib/terminal/tmux.mjs +53 -23
- package/lib/web/buffer-gc-runtime.mjs +2 -2
- package/lib/web/project-contexts.mjs +6 -1
- package/lib/web/runtime-main.mjs +3 -0
- package/lib/web/startup.mjs +4 -2
- package/package.json +2 -2
- package/scripts/regression.mjs +95 -98
- package/docs/architecture-split-review.md +0 -217
- package/docs/current-state-review-2026-08.md +0 -341
- package/docs/defect-review-2026-07.md +0 -382
- package/docs/layout-split-stack-audit.md +0 -298
- package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +0 -294
- package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +0 -400
- package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +0 -284
- package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +0 -303
- package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +0 -71
- package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +0 -117
- package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +0 -120
- package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +0 -117
- package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +0 -104
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
# Web and Runtime API v2 Hardening Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Ship an explicit Runtime API v2 with bounded browser sessions, session-scoped terminal input, CSP, secret redaction, and symlink-safe project databases.
|
|
6
|
-
|
|
7
|
-
**Architecture:** Central helpers own API negotiation, redaction, token comparison, and canonical path validation. The HTTP and WebSocket entry points enforce these helpers before routing while preserving the accepted plaintext-LAN and arbitrary-root behaviors.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Node.js 24 HTTP/HTTPS, `ws`, browser WebSocket/fetch, `node:test`, real filesystem integration tests.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## File Map
|
|
14
|
-
|
|
15
|
-
- Create `lib/web/api-version.mjs` and `test/web-api-version.test.mjs`.
|
|
16
|
-
- Create `lib/shared/redact.mjs` and `test/redact.test.mjs`.
|
|
17
|
-
- Create `lib/runtime/project-path.mjs` and `test/project-path.test.mjs`.
|
|
18
|
-
- Modify `lib/web/http.mjs`, `lib/web/runtime.mjs`, `lib/runtime/client.mjs`.
|
|
19
|
-
- Modify `bin/hcc.mjs` HTTP/WS routing, cookie lifecycle, logging, and TLS use.
|
|
20
|
-
- Modify `lib/web/ui-template.mjs` for API headers, WS version query, and CSP nonce.
|
|
21
|
-
- Modify `lib/web/tls.mjs` only where focused tests show a contract gap.
|
|
22
|
-
- Modify `scripts/regression.mjs` for real HTTP/HTTPS/WS tests.
|
|
23
|
-
|
|
24
|
-
### Task 1: Add Runtime API v2 negotiation
|
|
25
|
-
|
|
26
|
-
**Files:**
|
|
27
|
-
- Create: `lib/web/api-version.mjs`
|
|
28
|
-
- Create: `test/web-api-version.test.mjs`
|
|
29
|
-
- Modify: `lib/runtime/client.mjs:7-35`
|
|
30
|
-
- Modify: `lib/web/runtime.mjs:11-185`
|
|
31
|
-
- Modify: `bin/hcc.mjs:4675-5190`
|
|
32
|
-
- Modify: `lib/web/ui-template.mjs:1-2100`
|
|
33
|
-
|
|
34
|
-
- [ ] **Step 1: Write failing negotiation tests**
|
|
35
|
-
|
|
36
|
-
```js
|
|
37
|
-
import test from 'node:test';
|
|
38
|
-
import assert from 'node:assert/strict';
|
|
39
|
-
import { API_VERSION, readHttpApiVersion, readWebSocketApiVersion } from '../lib/web/api-version.mjs';
|
|
40
|
-
|
|
41
|
-
test('reads API v2 from the HTTP header', () => {
|
|
42
|
-
assert.equal(API_VERSION, 2);
|
|
43
|
-
assert.deepEqual(readHttpApiVersion({ headers: { 'x-hcc-api-version': '2' } }), { ok: true, version: 2 });
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('rejects missing and old protected API versions', () => {
|
|
47
|
-
assert.deepEqual(readHttpApiVersion({ headers: {} }), { ok: false, version: null });
|
|
48
|
-
assert.deepEqual(readHttpApiVersion({ headers: { 'x-hcc-api-version': '1' } }), { ok: false, version: 1 });
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('reads browser WebSocket version from query', () => {
|
|
52
|
-
assert.deepEqual(readWebSocketApiVersion(new URL('http://localhost/ws?api_version=2')), { ok: true, version: 2 });
|
|
53
|
-
});
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
- [ ] **Step 2: Run to verify RED**
|
|
57
|
-
|
|
58
|
-
Run: `node --test test/web-api-version.test.mjs`.
|
|
59
|
-
|
|
60
|
-
Expected: missing-module failure.
|
|
61
|
-
|
|
62
|
-
- [ ] **Step 3: Implement and enforce v2**
|
|
63
|
-
|
|
64
|
-
```js
|
|
65
|
-
export const API_VERSION = 2;
|
|
66
|
-
|
|
67
|
-
function result(value) {
|
|
68
|
-
const version = Number.parseInt(String(value ?? ''), 10);
|
|
69
|
-
return { ok: version === API_VERSION, version: Number.isFinite(version) ? version : null };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function readHttpApiVersion(req) {
|
|
73
|
-
return result(req?.headers?.['x-hcc-api-version']);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function readWebSocketApiVersion(url) {
|
|
77
|
-
return result(url?.searchParams?.get('api_version'));
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Before every protected `/api/*` route, return status 426 and:
|
|
82
|
-
|
|
83
|
-
```json
|
|
84
|
-
{"ok":false,"error":{"code":"API_VERSION_UNSUPPORTED","message":"Runtime API version 2 is required","supported_version":2}}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Expose `api_version: 2` from runtime metadata. Add `X-HCC-API-Version: 2` in `runtimeRequest` and every browser fetch wrapper. Add `api_version=2` to terminal WebSocket URLs and reject old/missing WS versions before authentication/session lookup.
|
|
88
|
-
|
|
89
|
-
- [ ] **Step 4: Verify GREEN and real compatibility rejection**
|
|
90
|
-
|
|
91
|
-
Run unit tests. Add regression requests with missing/1/2 headers plus WS query 1/2. Public `/`, `/login`, and static assets must remain exempt.
|
|
92
|
-
|
|
93
|
-
Run: `npm run test:regression`.
|
|
94
|
-
|
|
95
|
-
- [ ] **Step 5: Commit**
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
git add lib/web/api-version.mjs test/web-api-version.test.mjs lib/runtime/client.mjs lib/web/runtime.mjs lib/web/ui-template.mjs bin/hcc.mjs scripts/regression.mjs
|
|
99
|
-
git commit -m "feat: require Runtime API v2"
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Task 2: Enforce administrator and session-action token semantics
|
|
103
|
-
|
|
104
|
-
**Files:**
|
|
105
|
-
- Modify: `lib/web/http.mjs:62-122`
|
|
106
|
-
- Modify: `bin/hcc.mjs:2382-2465,2475-2580,4675-4955,5100-5185`
|
|
107
|
-
- Modify: `lib/web/ui-template.mjs:1570-1760,2050-2110`
|
|
108
|
-
- Modify: `scripts/regression.mjs:1193-1355,1974-2115,2866-3300`
|
|
109
|
-
|
|
110
|
-
- [ ] **Step 1: Add failing privilege-boundary tests**
|
|
111
|
-
|
|
112
|
-
Through real HTTP and WebSocket connections assert:
|
|
113
|
-
|
|
114
|
-
```text
|
|
115
|
-
admin bearer -> create/stop/inject allowed
|
|
116
|
-
admin cookie + same Origin -> create/stop/inject allowed
|
|
117
|
-
admin cookie + foreign Origin -> mutation denied
|
|
118
|
-
session action token absent from GET /api/sessions
|
|
119
|
-
wrong/missing session action token -> WS input denied
|
|
120
|
-
correct token for another session -> WS input denied
|
|
121
|
-
logout/expiry/eviction -> existing cookie WS closed and future input denied
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Also assert tokenless non-loopback startup fails while tokenless loopback still works.
|
|
125
|
-
Because constant-time behavior is not safely distinguishable with a timing test,
|
|
126
|
-
add a static security assertion that extracts `resolveWebActionSession` from
|
|
127
|
-
`bin/hcc.mjs`, requires `tokenMatches(provided, expected)`, and rejects direct
|
|
128
|
-
`provided !== expected` comparison. This is the repeatable validation artifact
|
|
129
|
-
for the timing boundary; the integration cases remain the behavior proof.
|
|
130
|
-
|
|
131
|
-
- [ ] **Step 2: Run to verify RED**
|
|
132
|
-
|
|
133
|
-
Run: `npm run test:regression`.
|
|
134
|
-
|
|
135
|
-
Expected: FAIL on the static constant-time assertion while the current code uses
|
|
136
|
-
direct inequality. Any additional integration failure is fixed through the same
|
|
137
|
-
boundary, but is not required to establish RED.
|
|
138
|
-
|
|
139
|
-
- [ ] **Step 3: Apply one shared token comparator and explicit roles**
|
|
140
|
-
|
|
141
|
-
Use `tokenMatches(provided, expected)` for administrator and session action tokens. Keep `webAuthMode` results `token` and `cookie`, document both as administrator roles, and keep same-origin enforcement on every cookie mutation. Track each cookie-authenticated WS in its server session record and validate the record before inbound and outbound frames.
|
|
142
|
-
|
|
143
|
-
Do not put action tokens in serialized session lists. Deliver them only in the corresponding terminal snapshot frame. Preserve CLI injection through administrator bearer authentication without requiring the session token.
|
|
144
|
-
|
|
145
|
-
- [ ] **Step 4: Verify GREEN and alternate bypasses**
|
|
146
|
-
|
|
147
|
-
Run the regression suite, then repeat token tests with same-length wrong tokens, URL-encoded peer IDs, revoked cookies, and a token from a sibling project session.
|
|
148
|
-
|
|
149
|
-
Expected: legitimate admin and matching-session behavior remains; all bypasses fail.
|
|
150
|
-
|
|
151
|
-
- [ ] **Step 5: Commit**
|
|
152
|
-
|
|
153
|
-
```bash
|
|
154
|
-
git add lib/web/http.mjs bin/hcc.mjs lib/web/ui-template.mjs scripts/regression.mjs
|
|
155
|
-
git commit -m "fix: enforce Web administrator and session tokens"
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### Task 3: Add structured secret redaction and CSP
|
|
159
|
-
|
|
160
|
-
**Files:**
|
|
161
|
-
- Create: `lib/shared/redact.mjs`
|
|
162
|
-
- Create: `test/redact.test.mjs`
|
|
163
|
-
- Modify: `lib/web/http.mjs:33-59`
|
|
164
|
-
- Modify: `lib/web/ui-template.mjs:1-2150`
|
|
165
|
-
- Modify: `bin/hcc.mjs:2180-2250,2368-2470,5290-5300`
|
|
166
|
-
- Modify: `scripts/regression.mjs:1841-2115,3894-4300`
|
|
167
|
-
|
|
168
|
-
- [ ] **Step 1: Write failing redaction tests**
|
|
169
|
-
|
|
170
|
-
```js
|
|
171
|
-
import test from 'node:test';
|
|
172
|
-
import assert from 'node:assert/strict';
|
|
173
|
-
import { redactSecrets } from '../lib/shared/redact.mjs';
|
|
174
|
-
|
|
175
|
-
test('redacts CLI, URL, headers, cookies, and structured tokens', () => {
|
|
176
|
-
const secret = 's3cr3t-value';
|
|
177
|
-
const value = {
|
|
178
|
-
command: `hcc web --token ${secret}`,
|
|
179
|
-
url: `http://host/?token=${secret}&root=/tmp/x`,
|
|
180
|
-
headers: { authorization: `Bearer ${secret}`, cookie: `hcc_sid=${secret}` },
|
|
181
|
-
action_token: secret
|
|
182
|
-
};
|
|
183
|
-
const redacted = JSON.stringify(redactSecrets(value));
|
|
184
|
-
assert.equal(redacted.includes(secret), false);
|
|
185
|
-
assert.equal(redacted.includes('[REDACTED]'), true);
|
|
186
|
-
});
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
Add an HTML test that extracts the CSP nonce, verifies the script tag uses it, and asserts a second un-nonced inline script would violate the generated `script-src` policy.
|
|
190
|
-
|
|
191
|
-
- [ ] **Step 2: Run to verify RED**
|
|
192
|
-
|
|
193
|
-
Run: `node --test test/redact.test.mjs`.
|
|
194
|
-
|
|
195
|
-
Expected: missing-module failure.
|
|
196
|
-
|
|
197
|
-
- [ ] **Step 3: Implement redaction and nonce responses**
|
|
198
|
-
|
|
199
|
-
`redactSecrets(value)` recursively clones arrays/objects, replaces secret-key values, redacts bearer/cookie fragments, replaces `--token` arguments, and deletes URL `token` parameter values. It must handle cycles with a `WeakSet` and replace them with `[Circular]`.
|
|
200
|
-
|
|
201
|
-
Pass all startup/error logging through this helper before stringification. Never log raw request headers or URLs.
|
|
202
|
-
|
|
203
|
-
Generate a random nonce per HTML response and call `webIndexHtml({ nonce })` or `webLoginPage({ nonce })`. Add the nonce to every inline script. Emit CSP:
|
|
204
|
-
|
|
205
|
-
```text
|
|
206
|
-
default-src 'self'; script-src 'self' 'nonce-<value>'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
- [ ] **Step 4: Verify GREEN and leak controls**
|
|
210
|
-
|
|
211
|
-
Run unit tests. Start Web with unique test secrets, exercise login/WS/errors, stop it, and scan all generated logs/runtime output. Assert no supplied secret appears. Verify browser HTML contains a matching nonce and no un-nonced script.
|
|
212
|
-
|
|
213
|
-
Run: `npm run test:regression`.
|
|
214
|
-
|
|
215
|
-
- [ ] **Step 5: Commit**
|
|
216
|
-
|
|
217
|
-
```bash
|
|
218
|
-
git add lib/shared/redact.mjs test/redact.test.mjs lib/web/http.mjs lib/web/ui-template.mjs bin/hcc.mjs scripts/regression.mjs
|
|
219
|
-
git commit -m "fix: redact Web secrets and enforce CSP"
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### Task 4: Canonicalize arbitrary project roots without symlink escape
|
|
223
|
-
|
|
224
|
-
**Files:**
|
|
225
|
-
- Create: `lib/runtime/project-path.mjs`
|
|
226
|
-
- Create: `test/project-path.test.mjs`
|
|
227
|
-
- Modify: `bin/hcc.mjs:2479-2525`
|
|
228
|
-
- Modify: `scripts/regression.mjs:2866-3300,6300-6700`
|
|
229
|
-
|
|
230
|
-
- [ ] **Step 1: Write failing real-filesystem tests**
|
|
231
|
-
|
|
232
|
-
Create temporary roots for: normal project, missing `.hello-cc`, `.hello-cc` symlink to an outside directory, nested DB symlink escape, `/proc` pseudo-file target on Linux, and a normal custom DB below `.hello-cc`.
|
|
233
|
-
|
|
234
|
-
Use the intended API:
|
|
235
|
-
|
|
236
|
-
```js
|
|
237
|
-
const safe = resolveProjectDatabase({ root, db: path.join(root, '.hello-cc', 'mesh.db'), createStateDir: true });
|
|
238
|
-
assert.equal(safe.db.startsWith(safe.stateDir + path.sep), true);
|
|
239
|
-
assert.throws(() => resolveProjectDatabase({ root: escapedRoot, db: escapedDb, createStateDir: true }), /PROJECT_PATH_FORBIDDEN/);
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
- [ ] **Step 2: Run to verify RED**
|
|
243
|
-
|
|
244
|
-
Run: `node --test test/project-path.test.mjs`.
|
|
245
|
-
|
|
246
|
-
Expected: missing-module failure.
|
|
247
|
-
|
|
248
|
-
- [ ] **Step 3: Implement canonical containment**
|
|
249
|
-
|
|
250
|
-
Resolve an existing root with `realpathSync`. Create `.hello-cc` with mode `0700` only when requested and absent. Reject it when `lstat` reports a symlink or when its real path is outside root. Resolve the DB parent, reject symlink components escaping the state directory, and require an existing DB target to be a regular file.
|
|
251
|
-
|
|
252
|
-
Replace `assertDbUnderRoot`/lexical-only checks with `resolveProjectDatabase`. Continue allowing every existing canonical root after administrator authentication.
|
|
253
|
-
|
|
254
|
-
- [ ] **Step 4: Verify GREEN and HTTP boundary**
|
|
255
|
-
|
|
256
|
-
Run unit tests, then issue real authenticated Web requests for normal arbitrary roots and each escape fixture. Require normal requests to work and every escape to return `PROJECT_PATH_FORBIDDEN` without creating/opening the outside DB.
|
|
257
|
-
|
|
258
|
-
Run: `npm run test:regression`.
|
|
259
|
-
|
|
260
|
-
- [ ] **Step 5: Commit**
|
|
261
|
-
|
|
262
|
-
```bash
|
|
263
|
-
git add lib/runtime/project-path.mjs test/project-path.test.mjs bin/hcc.mjs scripts/regression.mjs
|
|
264
|
-
git commit -m "fix: prevent project database symlink escape"
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### Task 5: Finish TLS and trusted-proxy mode coverage
|
|
268
|
-
|
|
269
|
-
**Files:**
|
|
270
|
-
- Track/modify: `lib/web/tls.mjs`
|
|
271
|
-
- Modify: `lib/web/runtime.mjs:80-190`
|
|
272
|
-
- Modify: `lib/runtime/state.mjs:91-123`
|
|
273
|
-
- Modify: `lib/web/http.mjs:76-112`
|
|
274
|
-
- Modify: `scripts/regression.mjs:6440-6600`
|
|
275
|
-
|
|
276
|
-
- [ ] **Step 1: Add failing mode-matrix tests**
|
|
277
|
-
|
|
278
|
-
Cover direct self-signed TLS, expired certificate rejection, SAN mismatch regeneration, key mismatch regeneration, loopback trusted proxy, forwarded headers from non-loopback, and accepted plaintext LAN. Assert `Secure` cookie only for direct TLS or trusted loopback proxy reporting HTTPS.
|
|
279
|
-
|
|
280
|
-
- [ ] **Step 2: Run to verify RED**
|
|
281
|
-
|
|
282
|
-
Run: `npm run test:regression`.
|
|
283
|
-
|
|
284
|
-
Expected: any uncovered certificate generation race or proxy-origin discrepancy fails with the specific mode fixture.
|
|
285
|
-
|
|
286
|
-
- [ ] **Step 3: Make only evidence-required production changes**
|
|
287
|
-
|
|
288
|
-
Keep generation directories, `.creating`/`.published`, atomic `current.json`, SAN/date/key validation, and CA-scoped runtime requests. Ensure proxy host/proto is consumed only when `--trust-proxy` is enabled and socket remote address is loopback. Do not make TLS mandatory or change the accepted default listener.
|
|
289
|
-
|
|
290
|
-
- [ ] **Step 4: Verify GREEN**
|
|
291
|
-
|
|
292
|
-
Run: `npm run test:unit`.
|
|
293
|
-
|
|
294
|
-
Run: `npm run test:regression`.
|
|
295
|
-
|
|
296
|
-
Expected: all three supported modes pass and every spoofed forwarded-header control fails closed.
|
|
297
|
-
|
|
298
|
-
- [ ] **Step 5: Commit**
|
|
299
|
-
|
|
300
|
-
```bash
|
|
301
|
-
git add lib/web/tls.mjs lib/web/runtime.mjs lib/runtime/state.mjs lib/web/http.mjs scripts/regression.mjs
|
|
302
|
-
git commit -m "test: close Web transport mode coverage"
|
|
303
|
-
```
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# Runtime Buffer GC Two-Phase Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Make manual GC use one server-derived timing snapshot, one unified clock decision, a one-shot runtime file apply, and only then an exact database apply.
|
|
6
|
-
|
|
7
|
-
**Architecture:** The runtime prepares an immutable canonical file plan from a canonical retention period and stores it behind a short-lived root/DB-bound token. The CLI uses the runtime-derived cutoff to capture an exact database plan and merges both plans' would-delete boundaries into one clock observation. Runtime file batches each recheck clock grace under `BEGIN IMMEDIATE`; database rows are finalized only after the runtime reports that every prepared file candidate was processed.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Node.js ESM, `node:sqlite`, synchronous filesystem APIs, built-in test runner, HTTP runtime API.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
### Task 1: Read-only clock prediction
|
|
14
|
-
|
|
15
|
-
**Files:**
|
|
16
|
-
- Modify: `lib/core/coordination/clock-safety.mjs`
|
|
17
|
-
- Test: `test/clock-safety.test.mjs`
|
|
18
|
-
|
|
19
|
-
- [ ] Write a failing test that seeds a watermark, pending gap, and grace row, calls `previewClockSafety`, verifies the same grace decision expected from apply, and compares the complete `meta` table before/after.
|
|
20
|
-
- [ ] Run `node --test test/clock-safety.test.mjs` and verify the missing export fails.
|
|
21
|
-
- [ ] Extract the shared pending-gap decision calculation and implement `previewClockSafety(db, options)` without a transaction or write.
|
|
22
|
-
- [ ] Re-run the focused test and verify preview and observer decisions match while preview leaves `meta` byte-for-byte unchanged.
|
|
23
|
-
|
|
24
|
-
### Task 2: One-shot runtime plan tokens and bounded apply barriers
|
|
25
|
-
|
|
26
|
-
**Files:**
|
|
27
|
-
- Create: `lib/runtime/buffer-gc-protocol.mjs`
|
|
28
|
-
- Modify: `lib/runtime/buffer-gc.mjs`
|
|
29
|
-
- Create: `test/buffer-gc-protocol.test.mjs`
|
|
30
|
-
|
|
31
|
-
- [ ] Write failing tests for token entropy shape, root/DB binding, replay, expiry, mismatch cleanup, and dry-run plans leaving no token.
|
|
32
|
-
- [ ] Write a failing real SQLite/filesystem test with more than 64 frozen candidates; extend grace from a second connection after the first committed batch and assert exactly the first batch is deleted and the remainder deferred.
|
|
33
|
-
- [ ] Implement `createBufferGcPlanStore` with a 15-second TTL and destructive take semantics.
|
|
34
|
-
- [ ] Implement `applyClockSafeBufferPlan` using batches of 64; each batch opens `BEGIN IMMEDIATE`, runs `observeClockSafetyInTransaction`, checks current grace, and performs only that batch's bounded identity rechecks/unlinks before commit.
|
|
35
|
-
- [ ] Re-run the focused protocol tests.
|
|
36
|
-
|
|
37
|
-
### Task 3: Authenticated prepare/apply API
|
|
38
|
-
|
|
39
|
-
**Files:**
|
|
40
|
-
- Modify: `bin/hcc.mjs`
|
|
41
|
-
- Modify: `scripts/regression.mjs`
|
|
42
|
-
|
|
43
|
-
- [ ] Add failing authenticated endpoint regressions: legacy or forged `cutoffMs`/`observedAt` bodies return `BAD_REQUEST`; prepare accepts only canonical safe `retentionSec`; apply rejects replay, wrong root/DB, and expired tokens.
|
|
44
|
-
- [ ] Replace the single-call endpoint with `phase: prepare|apply`. Prepare derives `observedAt` and `cutoffMs` from server time, freezes the full actual-directory plan, and returns counts/cutoffs plus a token only for apply. Apply destructively takes the token and runs the bounded barrier.
|
|
45
|
-
- [ ] Add periodic opportunistic expiry cleanup and verify dry-run preparation stores no token.
|
|
46
|
-
- [ ] Run the endpoint regression through the multi-project runtime workflow.
|
|
47
|
-
|
|
48
|
-
### Task 4: Manual CLI unified decision and apply ordering
|
|
49
|
-
|
|
50
|
-
**Files:**
|
|
51
|
-
- Modify: `bin/hcc.mjs`
|
|
52
|
-
- Modify: `lib/core/coordination/gc-plan.mjs`
|
|
53
|
-
- Modify: `scripts/regression.mjs`
|
|
54
|
-
- Test: `test/gc-plan.test.mjs`
|
|
55
|
-
|
|
56
|
-
- [ ] Add a failing regression with an exact DB history/lock/peer plan plus an actual-directory buffer candidate crossing one pending gap; assert dry-run predicts all would-delete categories deferred without changing `meta`, and apply deletes neither resource.
|
|
57
|
-
- [ ] Add a failing regression where grace is extended after prepare and before apply; assert runtime deletes zero and database finalization never starts.
|
|
58
|
-
- [ ] Add async snapshot cleanup support so the history temp table is always dropped across runtime awaits while preserving primary/cleanup errors.
|
|
59
|
-
- [ ] Split manual GC into prepare, unified preview/observe, runtime/local file apply, and exact DB finalization. Treat runtime unavailability, token failure, incomplete batched apply, subject drift, or clock failure as a barrier that prevents all database deletion.
|
|
60
|
-
- [ ] Ensure result counters report only committed deletes, retain verified-live buffer counts as protected, and report all unprocessed would-delete rows/files as deferred.
|
|
61
|
-
- [ ] Run `node --test test/gc-plan.test.mjs` and the focused manual/runtime regression.
|
|
62
|
-
|
|
63
|
-
### Task 5: Full verification and amended commit
|
|
64
|
-
|
|
65
|
-
**Files:**
|
|
66
|
-
- Verify all Task 4 files only.
|
|
67
|
-
|
|
68
|
-
- [ ] Run `npm run test:unit` and require zero failures.
|
|
69
|
-
- [ ] Run `npm run test:regression` and require `FULL_REGRESSION_OK`.
|
|
70
|
-
- [ ] Rebuild the Node 24 image and run its full regression.
|
|
71
|
-
- [ ] Run `git diff --cached --check`, selectively stage Task 4 files, and amend `f075976` without staging unrelated worktree changes.
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# Runtime Safety, Clock, GC, and Registry Design
|
|
2
|
-
|
|
3
|
-
Status: approved in conversation on 2026-08-03.
|
|
4
|
-
|
|
5
|
-
## Purpose
|
|
6
|
-
|
|
7
|
-
Close destructive-operation and ownership races without forcing every project
|
|
8
|
-
to wait after normal idle time. Verified process evidence is authoritative;
|
|
9
|
-
clock grace is reserved for unknown evidence.
|
|
10
|
-
|
|
11
|
-
## Clock Observation
|
|
12
|
-
|
|
13
|
-
The meta table stores a last-observed wall-clock watermark and the active grace
|
|
14
|
-
deadline. Observation runs before ownership-changing or destructive operations,
|
|
15
|
-
including lock acquisition, takeover, hook/heartbeat recovery, reaping, and GC.
|
|
16
|
-
Pure read-only commands do not update the watermark, preserving read-only DB
|
|
17
|
-
behavior.
|
|
18
|
-
|
|
19
|
-
A clock observation transaction:
|
|
20
|
-
|
|
21
|
-
1. reads the previous watermark and current grace deadline;
|
|
22
|
-
2. evaluates backward movement and long forward gaps;
|
|
23
|
-
3. for ownership operations, checks whether the gap crossed a peer or lock
|
|
24
|
-
expiry boundary whose owner is currently `unknown`;
|
|
25
|
-
4. for GC, checks whether suspicious backward/forward movement or a long gap
|
|
26
|
-
crossed the requested age cutoff for any deletion category, independent of
|
|
27
|
-
owner evidence;
|
|
28
|
-
5. atomically extends grace to `max(existing, now + 120 seconds)` when needed;
|
|
29
|
-
6. writes the new watermark only after the safety decision succeeds.
|
|
30
|
-
|
|
31
|
-
A verified live owner is renewed immediately from persisted `ttl_sec`. A
|
|
32
|
-
verified dead owner may expire immediately. Only unknown evidence is delayed by
|
|
33
|
-
grace. Failure to persist a required grace blocks the current destructive or
|
|
34
|
-
ownership-changing operation and returns an observable error.
|
|
35
|
-
|
|
36
|
-
The Web runtime also compares wall-clock movement with its monotonic probe and
|
|
37
|
-
uses the same database transaction. Concurrent probes can extend but never
|
|
38
|
-
shorten grace.
|
|
39
|
-
|
|
40
|
-
## Grace Semantics
|
|
41
|
-
|
|
42
|
-
While grace is active, every age-based deletion is deferred:
|
|
43
|
-
|
|
44
|
-
- expired or apparently expired locks;
|
|
45
|
-
- stale peers and peer bindings;
|
|
46
|
-
- events, completed tasks, messages, and handoffs;
|
|
47
|
-
- buffer files selected only by mtime.
|
|
48
|
-
|
|
49
|
-
Explicit `exited` peers and artifacts proven dead by identity may still be
|
|
50
|
-
cleaned. Results expose separate `deleted`, `protected`, and `deferred` counts.
|
|
51
|
-
|
|
52
|
-
## Buffer GC Arbitration
|
|
53
|
-
|
|
54
|
-
The buffer pruning policy is a shared helper that receives a fixed cutoff,
|
|
55
|
-
dry-run flag, project context, and protected absolute paths. It never recomputes
|
|
56
|
-
the cutoff during a run.
|
|
57
|
-
|
|
58
|
-
When a healthy compatible runtime exists, manual GC calls an authenticated
|
|
59
|
-
internal API. The runtime computes protection from the actual files referenced
|
|
60
|
-
by its session map and performs synchronous pruning in its event loop. Session
|
|
61
|
-
creation and replacement cannot interleave with that synchronous decision. New
|
|
62
|
-
files created after the fixed cutoff are not eligible.
|
|
63
|
-
|
|
64
|
-
If a runtime pointer exists but the runtime is unreachable or API-incompatible,
|
|
65
|
-
buffer deletion fails closed and reports deferred files. Safe database cleanup
|
|
66
|
-
may continue.
|
|
67
|
-
|
|
68
|
-
Without a runtime, local GC protects:
|
|
69
|
-
|
|
70
|
-
- external file groups whose wrapper or child identity is live or unknown;
|
|
71
|
-
- tmux pipes belonging to verified live or unknown managed panes;
|
|
72
|
-
- any legacy artifact whose ownership cannot be proved dead.
|
|
73
|
-
|
|
74
|
-
Only old artifacts with definitive dead or orphan evidence are deleted.
|
|
75
|
-
|
|
76
|
-
## GC Command Contract
|
|
77
|
-
|
|
78
|
-
`--older-than` must be zero or greater. Invalid values fail before opening a
|
|
79
|
-
write connection or deleting any file.
|
|
80
|
-
|
|
81
|
-
`hcc gc` remains dry-run by default. `--yes` removes technical state only.
|
|
82
|
-
Business history removal requires both `--history` and `--yes`. Open-task
|
|
83
|
-
handoffs, live-owner records, and unknown session artifacts are never removed
|
|
84
|
-
only because of age. Dry-run and apply execute the same planner.
|
|
85
|
-
|
|
86
|
-
A WAL checkpoint runs only after successful applied database cleanup. A busy
|
|
87
|
-
checkpoint is reported without converting successful safe cleanup into failure.
|
|
88
|
-
|
|
89
|
-
## Project Registry Lock
|
|
90
|
-
|
|
91
|
-
Registry read-modify-write is guarded by an atomic lock directory adjacent to
|
|
92
|
-
`projects.json`. Lock owner metadata includes process identity and creation
|
|
93
|
-
time. Acquisition uses bounded retry. A lock is reclaimed only when its owner is
|
|
94
|
-
definitively dead; unknown ownership times out with an error rather than being
|
|
95
|
-
stolen.
|
|
96
|
-
|
|
97
|
-
After acquiring the lock, the writer re-reads the registry, merges by canonical
|
|
98
|
-
real root, and writes with the existing atomic temp-and-rename helper. A changed
|
|
99
|
-
DB path updates immediately. The 60-second throttle applies only when root and
|
|
100
|
-
DB are both unchanged.
|
|
101
|
-
|
|
102
|
-
Authenticated Web users may continue registering arbitrary existing server
|
|
103
|
-
directories. This is an explicitly accepted product risk, not an isolation
|
|
104
|
-
guarantee.
|
|
105
|
-
|
|
106
|
-
## Acceptance Criteria
|
|
107
|
-
|
|
108
|
-
- CLI-only wake with a live process renews ownership without a blanket delay.
|
|
109
|
-
- CLI-only wake with unknown evidence enters grace before takeover or GC.
|
|
110
|
-
- A GC after a suspicious clock gap defers age-based history deletion even when
|
|
111
|
-
no peer or lock candidate exists.
|
|
112
|
-
- Grace never shrinks under concurrent writers.
|
|
113
|
-
- Manual GC preserves live external and tmux artifacts with old mtimes.
|
|
114
|
-
- Unreachable runtime state defers buffer deletion.
|
|
115
|
-
- Negative retention is rejected without mutation.
|
|
116
|
-
- Grace defers every wall-clock-based history deletion.
|
|
117
|
-
- Concurrent registry writers retain both projects and update changed DB paths.
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# hello-cc v1 Data and Identity Design
|
|
2
|
-
|
|
3
|
-
Status: approved in conversation on 2026-08-03.
|
|
4
|
-
|
|
5
|
-
## Purpose
|
|
6
|
-
|
|
7
|
-
Define the breaking v1 data contract, schema migration, process identity model,
|
|
8
|
-
and provider peer identity semantics. This specification intentionally does not
|
|
9
|
-
provide downgrade compatibility or map pre-v1 provider peer IDs to v1 IDs.
|
|
10
|
-
|
|
11
|
-
## Product Decisions
|
|
12
|
-
|
|
13
|
-
- Release the result as package version `1.0.0`.
|
|
14
|
-
- Use database schema version 7.
|
|
15
|
-
- A schema v7 database is not supported by pre-v1 binaries.
|
|
16
|
-
- Provider peer IDs use the v1 full-value hash algorithm for every provider ID,
|
|
17
|
-
including UUIDs and user-defined session names.
|
|
18
|
-
- Pre-v1 peer IDs, tasks, locks, messages, and bindings are not merged into the
|
|
19
|
-
new v1 identity automatically.
|
|
20
|
-
- Migration must create a consistent database backup before the first v7 write.
|
|
21
|
-
|
|
22
|
-
## Migration Contract
|
|
23
|
-
|
|
24
|
-
Before migrating a v5 or v6 database, hello-cc must create a SQLite-consistent
|
|
25
|
-
snapshot next to the source database. The backup name includes the source
|
|
26
|
-
schema version and a collision-resistant timestamp suffix. It must never
|
|
27
|
-
overwrite an existing file.
|
|
28
|
-
|
|
29
|
-
The backup is produced through the open SQLite connection with `VACUUM INTO`,
|
|
30
|
-
not by copying `mesh.db` while WAL frames may still be pending. Migration stops
|
|
31
|
-
without changing the source database if backup creation or backup integrity
|
|
32
|
-
validation fails. A successful backup is opened read-only and must pass
|
|
33
|
-
`PRAGMA quick_check` before schema migration begins.
|
|
34
|
-
|
|
35
|
-
Registered sibling projects follow the same contract independently. A failed
|
|
36
|
-
sibling backup or migration is reported and isolated; it must not partially
|
|
37
|
-
migrate that sibling or fail the active project's command.
|
|
38
|
-
|
|
39
|
-
## Schema v7
|
|
40
|
-
|
|
41
|
-
Schema v7 adds process identity fields to `peers`:
|
|
42
|
-
|
|
43
|
-
- `pid_start_token TEXT`: opaque platform-derived process start identity.
|
|
44
|
-
- `pid_command_hash TEXT`: hash of the normalized executable/command identity.
|
|
45
|
-
|
|
46
|
-
Both fields are nullable. Missing or unreadable identity evidence means
|
|
47
|
-
`unknown`; it never means live or dead.
|
|
48
|
-
|
|
49
|
-
Existing v6 `locks.ttl_sec` remains required. Invalid or absent historical TTL
|
|
50
|
-
values are repaired to the documented default, capped at one hour when derived
|
|
51
|
-
from historical timestamps.
|
|
52
|
-
|
|
53
|
-
The schema migration history records v7 exactly once. `doctor` reports the
|
|
54
|
-
current schema, supported schema, backup location when known, and the explicit
|
|
55
|
-
no-downgrade contract.
|
|
56
|
-
|
|
57
|
-
## Process Identity
|
|
58
|
-
|
|
59
|
-
A shared process identity helper returns one of:
|
|
60
|
-
|
|
61
|
-
- a verified identity `{ pid, startToken, commandHash }`;
|
|
62
|
-
- `null` when the process does not exist;
|
|
63
|
-
- `unknown` when the process cannot be inspected safely.
|
|
64
|
-
|
|
65
|
-
Linux identity combines boot ID, `/proc/<pid>/stat` start ticks, and normalized
|
|
66
|
-
executable identity. macOS identity combines boot time, `ps` process start time,
|
|
67
|
-
PID, and normalized command identity. Inspection failures, permission errors,
|
|
68
|
-
or malformed output return `unknown`.
|
|
69
|
-
|
|
70
|
-
A stored identity is live only when the current identity matches every stored
|
|
71
|
-
component. A missing PID is dead only when the OS definitively reports that the
|
|
72
|
-
PID does not exist. A reused PID with a different start token or command hash is
|
|
73
|
-
dead for the stored peer.
|
|
74
|
-
|
|
75
|
-
External session metadata stores separate wrapper and child identities. New
|
|
76
|
-
metadata is written atomically. Legacy metadata without fingerprints is treated
|
|
77
|
-
as unknown unless both referenced PIDs are definitively absent.
|
|
78
|
-
|
|
79
|
-
tmux identity requires all of the following:
|
|
80
|
-
|
|
81
|
-
- the managed session name matches the hello-cc naming contract;
|
|
82
|
-
- the pane still exists;
|
|
83
|
-
- `HCC_ROOT` resolves to the expected project root;
|
|
84
|
-
- the pane process identity matches the stored identity when one is available.
|
|
85
|
-
|
|
86
|
-
## Liveness Precedence
|
|
87
|
-
|
|
88
|
-
All ownership, lock, reaper, and GC decisions consume one shared resolver with
|
|
89
|
-
the result `live`, `dead`, or `unknown`.
|
|
90
|
-
|
|
91
|
-
1. Explicit administrative `exited` status is dead.
|
|
92
|
-
2. Verified tmux or process identity is live.
|
|
93
|
-
3. `detached` is not dead by itself; a detached tmux session may still be live.
|
|
94
|
-
4. Definitive process absence plus definitive tmux absence is dead.
|
|
95
|
-
5. Conflicting, missing, legacy, or unreadable evidence is unknown.
|
|
96
|
-
|
|
97
|
-
Wall-clock age never overrides verified live or dead evidence.
|
|
98
|
-
|
|
99
|
-
## Provider Peer IDs
|
|
100
|
-
|
|
101
|
-
JavaScript and generated shims derive the same first eight lowercase SHA-1 hex
|
|
102
|
-
characters from the complete provider ID. Hash output is accepted only when it
|
|
103
|
-
is exactly 40 lowercase hexadecimal characters. When no supported hash command
|
|
104
|
-
can produce a valid digest, a shim starts the real provider without hello-cc
|
|
105
|
-
coordination rather than creating an ambiguous ID.
|
|
106
|
-
|
|
107
|
-
The v1 release notes must show examples of old and new IDs and explain that old
|
|
108
|
-
coordination history remains queryable in the backup but is not attached to the
|
|
109
|
-
new peer automatically.
|
|
110
|
-
|
|
111
|
-
## Acceptance Criteria
|
|
112
|
-
|
|
113
|
-
- A v5 and a development v6 fixture each produce a validated backup and migrate
|
|
114
|
-
once to v7.
|
|
115
|
-
- A forced backup failure leaves the source schema and data unchanged.
|
|
116
|
-
- A pre-v1 binary rejects v7 with the documented error.
|
|
117
|
-
- PID reuse is classified as dead for the stored identity.
|
|
118
|
-
- A live tmux pane remains live across sleep-sized wall-clock gaps.
|
|
119
|
-
- A detached but live tmux session is not made takeover-ready.
|
|
120
|
-
- JavaScript and every generated shim produce identical v1 peer IDs.
|