instar 1.3.414 → 1.3.416
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/dashboard/index.html +2 -1
- package/dashboard/subscriptions.js +3 -0
- package/dist/core/QuotaPoller.d.ts +8 -0
- package/dist/core/QuotaPoller.d.ts.map +1 -1
- package/dist/core/QuotaPoller.js +34 -0
- package/dist/core/QuotaPoller.js.map +1 -1
- package/dist/core/SubscriptionPool.d.ts +8 -0
- package/dist/core/SubscriptionPool.d.ts.map +1 -1
- package/dist/core/SubscriptionPool.js +5 -0
- package/dist/core/SubscriptionPool.js.map +1 -1
- package/dist/monitoring/SessionRecovery.d.ts.map +1 -1
- package/dist/monitoring/SessionRecovery.js +4 -0
- package/dist/monitoring/SessionRecovery.js.map +1 -1
- package/dist/monitoring/agentWorktreeGit.d.ts.map +1 -1
- package/dist/monitoring/agentWorktreeGit.js +4 -0
- package/dist/monitoring/agentWorktreeGit.js.map +1 -1
- package/dist/server/routes.js +4 -4
- package/dist/server/routes.js.map +1 -1
- package/package.json +2 -2
- package/scripts/lint-no-blocking-process-scans.js +115 -0
- package/scripts/lint-no-direct-destructive.js +3 -0
- package/src/data/builtin-manifest.json +46 -46
- package/upgrades/1.3.415.md +32 -0
- package/upgrades/1.3.416.md +42 -0
- package/upgrades/side-effects/lint-no-blocking-process-scans.md +73 -0
- package/upgrades/side-effects/subscription-account-email.md +58 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Side-Effects Review — lint: ban blocking process scans on the runtime hot path
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `lint-no-blocking-process-scans`
|
|
4
|
+
**Date:** `2026-06-07`
|
|
5
|
+
**Author:** `Echo`
|
|
6
|
+
**Tier:** 1 (a new CI lint + comment-only src annotations + a test; no behavior change, no API/route/config/migration)
|
|
7
|
+
**Second-pass reviewer:** `Echo (self) — Tier-1; preventive gate, no runtime behavior change`
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
Adds `scripts/lint-no-blocking-process-scans.js` (wired into `npm run lint`): in
|
|
12
|
+
`src/monitoring/` and `src/server/`, a SYNCHRONOUS child-process call
|
|
13
|
+
(`spawnSync`/`execSync`/`execFileSync`) whose command literal is a process-
|
|
14
|
+
enumeration tool (`ps`/`pgrep`/`lsof`/`pkill`) now fails CI. This is post-mortem
|
|
15
|
+
standard #3 for the 2026-06-07 "server temporarily down" incident (topic 21816):
|
|
16
|
+
synchronous `ps`/`lsof` scans on a cadence blocked the event loop and starved
|
|
17
|
+
`/health` under load → the supervisor restarted an alive server → the loop.
|
|
18
|
+
#972 fixed SessionWatchdog; this lint stops the class from being re-introduced.
|
|
19
|
+
|
|
20
|
+
The two existing in-dir call sites are `lsof` one-shots, allowlisted with inline
|
|
21
|
+
`// lint-allow-blocking-scan:` justifications (comment-only edits):
|
|
22
|
+
- `SessionRecovery.ts` — targeted `lsof -p <pid>` (single process, 5s timeout),
|
|
23
|
+
runs once during a session's JSONL recovery, not on a cadence.
|
|
24
|
+
- `agentWorktreeGit.ts` — full-cwd `lsof` in AgentWorktreeReaper, which ships
|
|
25
|
+
dark + dry-run by default (not on any live agent's hot path); 15s timeout;
|
|
26
|
+
async conversion noted as a follow-up.
|
|
27
|
+
|
|
28
|
+
## Decision-point inventory
|
|
29
|
+
|
|
30
|
+
- The only decision: which sync calls to fail CI on. Scoped to the documented
|
|
31
|
+
load-sensitive enumeration commands (`ps`/`pgrep`/`lsof`/`pkill`) in the two
|
|
32
|
+
runtime hot dirs. tmux/git calls are deliberately NOT covered (bounded, fast,
|
|
33
|
+
ubiquitous — converting them is a separate, bigger concern and not this
|
|
34
|
+
incident's cause).
|
|
35
|
+
|
|
36
|
+
## 1. False positives (flagging a safe call)
|
|
37
|
+
|
|
38
|
+
A genuinely one-shot, bounded sync scan is excused by an inline
|
|
39
|
+
`// lint-allow-blocking-scan: <reason>` (scanned up to 6 lines above the call to
|
|
40
|
+
allow a multi-line justification). The escape hatch requires a written reason, so
|
|
41
|
+
the decision is reviewed, not silent. Two current sites use it.
|
|
42
|
+
|
|
43
|
+
## 2. False negatives (missing a real one)
|
|
44
|
+
|
|
45
|
+
Static detection matches a string-literal command. A sync scan that passes the
|
|
46
|
+
command via a variable (e.g. `execFileSync(file, args)` in `mcpProcessReaperDeps`)
|
|
47
|
+
is not caught — accepted: the lint is a ratchet against the common, copy-pasted
|
|
48
|
+
literal form; the variable-indirection cases are pre-existing and tracked. tmux
|
|
49
|
+
is intentionally out of scope.
|
|
50
|
+
|
|
51
|
+
## 3. Level-of-abstraction fit
|
|
52
|
+
|
|
53
|
+
Correct: a CI lint in the existing `lint-no-*` family, modeled on
|
|
54
|
+
`lint-no-unfunneled-headless-launch.js`. Structure > Willpower — a future periodic
|
|
55
|
+
`spawnSync('ps')` fails the build instead of being discovered as a stall in prod.
|
|
56
|
+
|
|
57
|
+
## 4. Blast radius
|
|
58
|
+
|
|
59
|
+
CI-only. No runtime code changes (the two src edits are comments). Cannot affect a
|
|
60
|
+
running agent. Worst case of a bug in the lint = a spurious CI failure, fixed by an
|
|
61
|
+
allowlist comment or a lint tweak — never a production impact.
|
|
62
|
+
|
|
63
|
+
## 5. Rollback
|
|
64
|
+
|
|
65
|
+
Remove the script + the one `package.json` chain entry; revert the two comment
|
|
66
|
+
annotations. No state/format change.
|
|
67
|
+
|
|
68
|
+
## 6. Tests
|
|
69
|
+
|
|
70
|
+
`tests/unit/lint-no-blocking-process-scans.test.ts` (5): flags sync ps; flags
|
|
71
|
+
spawnSync pgrep + execSync lsof; honours the inline allow justification; ignores
|
|
72
|
+
comment-only mentions + async/tmux calls; the real runtime tree is clean. The lint
|
|
73
|
+
also self-validated against a synthetic violation. tsc clean.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Side-Effects Review — per-account email on the Subscription Pool
|
|
2
|
+
|
|
3
|
+
## Scope of change
|
|
4
|
+
|
|
5
|
+
- `src/core/SubscriptionPool.ts` — add optional `email` to `SubscriptionAccount`,
|
|
6
|
+
`AddAccountInput`, `UpdateAccountInput`; `add()`/`update()` store/patch it.
|
|
7
|
+
- `src/core/QuotaPoller.ts` — new exported `readAccountEmail(configHome)` (reads the
|
|
8
|
+
PUBLIC `oauthAccount.emailAddress` from the config home's `.claude.json`); `pollAll()`
|
|
9
|
+
auto-populates `account.email` from it on each poll.
|
|
10
|
+
- `src/server/routes.ts` — POST/PATCH `/subscription-pool` pass `email` through.
|
|
11
|
+
- `dashboard/subscriptions.js` + `dashboard/index.html` — render the email under the
|
|
12
|
+
account nickname. Tests (unit + render assertion).
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Operator requirement (Justin, topic 20905): accounts must be identified by nickname
|
|
17
|
+
AND email — e.g. "SageMind - Justin" vs "SageMind - Adriana" vs "SageMind - Dawn"
|
|
18
|
+
are the same org, so the email is the disambiguator. The pool previously stored only
|
|
19
|
+
nickname + login location.
|
|
20
|
+
|
|
21
|
+
## The safety property (load-bearing)
|
|
22
|
+
|
|
23
|
+
The email is **auto-populated from the account's own login record** (`oauthAccount.
|
|
24
|
+
emailAddress` in the config home), not just operator-typed. So the stored email
|
|
25
|
+
always reflects WHICH account actually authenticated under that config home — a
|
|
26
|
+
login into the wrong account *surfaces* (the email won't match the nickname's intent)
|
|
27
|
+
instead of hiding. This directly serves the operator's stated worry about mislabeling
|
|
28
|
+
accounts. The email is a PUBLIC identifier — never a token/secret.
|
|
29
|
+
|
|
30
|
+
## What does NOT change
|
|
31
|
+
|
|
32
|
+
- Additive-optional everywhere: `email` is optional; existing accounts and existing
|
|
33
|
+
add/update calls (no email) behave exactly as before (`email` stays undefined).
|
|
34
|
+
- No new route, no new authority, no behavior change to selection/swap/poll beyond
|
|
35
|
+
writing one extra public field. The credential-field guard is unaffected (`email`
|
|
36
|
+
is not a forbidden field name).
|
|
37
|
+
|
|
38
|
+
## Framework generality
|
|
39
|
+
|
|
40
|
+
`readAccountEmail` reads claude-code's `oauthAccount.emailAddress` (the Claude config
|
|
41
|
+
layout), matching the standard's Claude-first scope. The `email` field itself is
|
|
42
|
+
framework-agnostic (any provider's account can carry one); only the auto-populate
|
|
43
|
+
reader is claude-code-specific today. A non-claude account simply gets no auto-filled
|
|
44
|
+
email (operators can still set one via the route) — no behavior regression, and the
|
|
45
|
+
reader extends per-framework when those logins are wired.
|
|
46
|
+
|
|
47
|
+
## Failure modes considered
|
|
48
|
+
|
|
49
|
+
- Config home has no `.claude.json` / no `oauthAccount` → `readAccountEmail` returns
|
|
50
|
+
null → poll leaves `email` unchanged (no throw).
|
|
51
|
+
- Operator passes `email` at add time → stored as-is; a later poll overwrites it with
|
|
52
|
+
the real logged-in email (reality wins — the safety property).
|
|
53
|
+
- `update({ email: '' })` clears the email (explicit unset).
|
|
54
|
+
|
|
55
|
+
## Migration / parity
|
|
56
|
+
|
|
57
|
+
Additive-optional field on an existing store — no migration needed (old records load
|
|
58
|
+
with `email` undefined; the next poll fills it). Ships via dist.
|