instar 1.3.704 → 1.3.706
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +232 -17
- package/dist/commands/server.js.map +1 -1
- package/dist/core/DeliverMessageHandler.d.ts +16 -0
- package/dist/core/DeliverMessageHandler.d.ts.map +1 -1
- package/dist/core/DeliverMessageHandler.js +0 -0
- package/dist/core/DeliverMessageHandler.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +15 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +128 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/QueueDrainLoop.d.ts +6 -2
- package/dist/core/QueueDrainLoop.d.ts.map +1 -1
- package/dist/core/QueueDrainLoop.js +0 -0
- package/dist/core/QueueDrainLoop.js.map +1 -1
- package/dist/core/SessionRouter.d.ts +24 -4
- package/dist/core/SessionRouter.d.ts.map +1 -1
- package/dist/core/SessionRouter.js +25 -2
- package/dist/core/SessionRouter.js.map +1 -1
- package/dist/core/meshRejectionLog.d.ts +37 -0
- package/dist/core/meshRejectionLog.d.ts.map +1 -0
- package/dist/core/meshRejectionLog.js +73 -0
- package/dist/core/meshRejectionLog.js.map +1 -0
- package/dist/core/registryHighWater.d.ts +59 -0
- package/dist/core/registryHighWater.d.ts.map +1 -0
- package/dist/core/registryHighWater.js +120 -0
- package/dist/core/registryHighWater.js.map +1 -0
- package/dist/core/senderRejectionNotice.d.ts +89 -0
- package/dist/core/senderRejectionNotice.d.ts.map +1 -0
- package/dist/core/senderRejectionNotice.js +149 -0
- package/dist/core/senderRejectionNotice.js.map +1 -0
- package/dist/core/senderValidationGate.d.ts +80 -0
- package/dist/core/senderValidationGate.d.ts.map +1 -0
- package/dist/core/senderValidationGate.js +133 -0
- package/dist/core/senderValidationGate.js.map +1 -0
- package/dist/core/types.d.ts +18 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/messaging/MessageProcessingLedger.d.ts +33 -5
- package/dist/messaging/MessageProcessingLedger.d.ts.map +1 -1
- package/dist/messaging/MessageProcessingLedger.js +50 -7
- package/dist/messaging/MessageProcessingLedger.js.map +1 -1
- package/dist/messaging/ingressDedup.d.ts.map +1 -1
- package/dist/messaging/ingressDedup.js +9 -0
- package/dist/messaging/ingressDedup.js.map +1 -1
- package/dist/monitoring/guardAcceptedFallbacks.d.ts +48 -0
- package/dist/monitoring/guardAcceptedFallbacks.d.ts.map +1 -0
- package/dist/monitoring/guardAcceptedFallbacks.js +106 -0
- package/dist/monitoring/guardAcceptedFallbacks.js.map +1 -0
- package/dist/monitoring/guardManifest.d.ts +25 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +48 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/monitoring/guardPostureView.d.ts +23 -0
- package/dist/monitoring/guardPostureView.d.ts.map +1 -1
- package/dist/monitoring/guardPostureView.js +66 -0
- package/dist/monitoring/guardPostureView.js.map +1 -1
- package/dist/monitoring/probes/GuardPostureProbe.d.ts +13 -2
- package/dist/monitoring/probes/GuardPostureProbe.d.ts.map +1 -1
- package/dist/monitoring/probes/GuardPostureProbe.js +143 -40
- package/dist/monitoring/probes/GuardPostureProbe.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +3 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +1 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +82 -1
- package/dist/server/routes.js.map +1 -1
- package/dist/users/UserManager.d.ts +22 -1
- package/dist/users/UserManager.d.ts.map +1 -1
- package/dist/users/UserManager.js +87 -2
- package/dist/users/UserManager.js.map +1 -1
- package/dist/users/testIdentityMarkers.d.ts +94 -0
- package/dist/users/testIdentityMarkers.d.ts.map +1 -0
- package/dist/users/testIdentityMarkers.js +170 -0
- package/dist/users/testIdentityMarkers.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +63 -63
- package/src/data/state-coherence-registry.json +18 -0
- package/src/scaffold/templates.ts +3 -1
- package/upgrades/1.3.705.md +79 -0
- package/upgrades/1.3.706.md +80 -0
- package/upgrades/side-effects/g3-dark-but-load-bearing-guards.md +143 -0
- package/upgrades/side-effects/silent-loss-refusal-conservation.md +124 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Eradicated the silent-message-loss class from the 2026-07-01 incident, where every
|
|
9
|
+
message from the verified operator was silently dropped for most of a day. Three
|
|
10
|
+
stacked defects are closed:
|
|
11
|
+
|
|
12
|
+
- **A refusal now stays a refusal.** The mesh routing layer no longer encodes a
|
|
13
|
+
`sender-rejected` NACK as a successful `forwarded`. It is a first-class terminal
|
|
14
|
+
`RouteOutcome.action:'rejected'`, enumerated into every consumer (live Telegram +
|
|
15
|
+
Slack, the queue drain, `forceReplace`, `isRemotelyHandled`) and into a distinct
|
|
16
|
+
`MessageProcessingLedger` `rejected` terminal. A build-breaking ratchet pins that
|
|
17
|
+
no consumer can ever map a refusal back into a success shape.
|
|
18
|
+
- **The user is told.** A terminally-refused user-originated message now produces
|
|
19
|
+
ONE neutral notice on the originating channel ("I got your message but couldn't
|
|
20
|
+
confirm you as an approved sender…") via the deterministic delivery path (never
|
|
21
|
+
the tone gate), deduped per messageId, ceilinged across topics, with a
|
|
22
|
+
flapping-proof decay. The deciding machine writes a metadata-only trace to
|
|
23
|
+
`logs/mesh-rejections.jsonl`.
|
|
24
|
+
- **The sanity gate.** Sender re-validation refuses to ARM against a degenerate /
|
|
25
|
+
never-populated / corrupt / operator-unresolvable user registry — it fails toward
|
|
26
|
+
delivery and shouts, using a durable `state/registry-high-water.json` marker to
|
|
27
|
+
tell a fresh-install empty registry (deliver) from an emptied-by-deletion one
|
|
28
|
+
(keep rejecting + alert). Test/fixture identities are refused at the write AND
|
|
29
|
+
load layers, with a dashboard-PIN-minted, load-verifiable signed override for a
|
|
30
|
+
legitimate name-collision, plus a one-time boot migration that quarantines
|
|
31
|
+
already-polluted stores.
|
|
32
|
+
|
|
33
|
+
Ships always-on (a reachability floor may not be dark). Sender re-validation
|
|
34
|
+
remains Telegram-scoped; Slack ships NOTICE parity now (sender re-validation is a
|
|
35
|
+
tracked follow-up). Two tracked deferrals: the full ack-vocabulary split
|
|
36
|
+
(fb-1e751537-655) and the fixture-clobber/wiring-gate filing (fb-b15ac10b-85c).
|
|
37
|
+
|
|
38
|
+
## What to Tell Your User
|
|
39
|
+
|
|
40
|
+
Your messages can no longer be silently dropped. If I ever genuinely cannot confirm
|
|
41
|
+
you as an approved sender, you now get a plain, one-time notice on the same channel
|
|
42
|
+
you wrote to ("I got your message but couldn't confirm you as an approved sender…")
|
|
43
|
+
instead of silence — so a delivery receipt with no reply can never again mean your
|
|
44
|
+
message vanished. And I will not switch that sender-check on at all against a
|
|
45
|
+
registry that looks empty or damaged: on a fresh install, or if the user list ever
|
|
46
|
+
gets wiped or corrupted, I default to letting your messages through and flagging the
|
|
47
|
+
problem, rather than locking you out. This is a safety fix and is always on.
|
|
48
|
+
|
|
49
|
+
## Summary of New Capabilities
|
|
50
|
+
|
|
51
|
+
- A terminally-refused sender-check produces ONE neutral, deduped user notice on the
|
|
52
|
+
originating channel (Telegram now; Slack too), never silence.
|
|
53
|
+
- Sender re-validation self-disables against a degenerate / never-populated /
|
|
54
|
+
corrupt / operator-unresolvable user registry (fails toward delivery + alerts),
|
|
55
|
+
using a durable `state/registry-high-water.json` marker to tell a fresh-install
|
|
56
|
+
empty registry from an emptied-by-deletion one.
|
|
57
|
+
- Test/fixture identities are refused from the production user registry at both the
|
|
58
|
+
write and load layers, with a dashboard-PIN-minted signed override for a
|
|
59
|
+
legitimate name-collision, plus a one-time boot migration that quarantines
|
|
60
|
+
already-polluted stores (backup + audit).
|
|
61
|
+
- A machine-local, metadata-only `logs/mesh-rejections.jsonl` trace records every
|
|
62
|
+
cross-machine sender refusal (never message content).
|
|
63
|
+
|
|
64
|
+
## Evidence
|
|
65
|
+
|
|
66
|
+
- Build: `pnpm build` green.
|
|
67
|
+
- Tests (all three tiers): unit `tests/unit/silent-loss-*.test.ts` (route-outcome
|
|
68
|
+
ratchet, ledger `rejected` terminal + redelivery-drop, registry high-water +
|
|
69
|
+
classification taxonomy, sender-validation gate arm/disarm/re-arm,
|
|
70
|
+
test-identity markers + signed allow-marker, UserManager fixture refusal
|
|
71
|
+
write+load, unified notice dedupe/ceiling/decay/divergence, deliver-handler
|
|
72
|
+
onRejected + suppression cache, mesh-rejection log, fixture-quarantine migration,
|
|
73
|
+
CLAUDE.md awareness idempotency); integration
|
|
74
|
+
`tests/integration/silent-loss-refusal-conservation.integration.test.ts` (full
|
|
75
|
+
route→NACK→rejected→ONE deduped notice + metadata trace + gate arm/disarm/re-arm
|
|
76
|
+
+ operator-delivered-against-clobbered-registry); e2e
|
|
77
|
+
`tests/e2e/silent-loss-allow-identity-route.test.ts` (PIN-gated mint route
|
|
78
|
+
feature-alive + minted marker survives a real reload).
|
|
79
|
+
- Full suite run green before push (Zero-Failure standard).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The guard-posture inventory (`GET /guards`) can now express that a DARK feature
|
|
9
|
+
is load-bearing — that a critical path depends on it — instead of treating it as
|
|
10
|
+
an ordinary optional dark feature that sits quiet while the path it should guard
|
|
11
|
+
runs unguarded. This is the guard-layer arm of the ratified "A Dark Feature Guards
|
|
12
|
+
Nothing" standard, and it closes the gap the 2026-07-01 silent-loss postmortem
|
|
13
|
+
named (operator message delivery depended on dark guards, and nothing surfaced it).
|
|
14
|
+
|
|
15
|
+
A guard declared `loadBearing` in the manifest now carries a `criticalPath` label
|
|
16
|
+
on every row, and when it sits silently unguarded it is classified one of three
|
|
17
|
+
ways:
|
|
18
|
+
- `loadBearingGap` — LOUD. A critical path is unguarded. It alerts on its OWN
|
|
19
|
+
attention episode track (`guard-posture-loadbearing:ep-N`), so a long-lived gap
|
|
20
|
+
can never mask an acute load-shed on another guard.
|
|
21
|
+
- `loadBearingSoaking` — a dry-run guard graduating within a bounded, code-constant
|
|
22
|
+
soak window. Surfaced on `/guards` only, no alert; it LAPSES to a loud gap if it
|
|
23
|
+
stalls past the window.
|
|
24
|
+
- `loadBearingAccepted` — an owned, PIN-authenticated operator acceptance is on
|
|
25
|
+
record. Full suppression + a visible accepted-risk row.
|
|
26
|
+
|
|
27
|
+
New route (dashboard-PIN-gated, `owner` + `reason` both required):
|
|
28
|
+
`POST`/`DELETE /guards/:key/accept-fallback`. The initial curated load-bearing set
|
|
29
|
+
is small and conservative: the durable inbound-message queue and the stranded-topic
|
|
30
|
+
sentinel, both on the operator-message-delivery critical path.
|
|
31
|
+
|
|
32
|
+
The classifier stays observe-only — it never gates, blocks, or disables anything,
|
|
33
|
+
and `/guards` is byte-identical for a consumer that ignores the new fields. Rollback
|
|
34
|
+
lever for the alert: `monitoring.guardPostureProbe.alertLoadBearingGaps` (the
|
|
35
|
+
classification stays either way).
|
|
36
|
+
|
|
37
|
+
## What to Tell Your User
|
|
38
|
+
|
|
39
|
+
I can now tell you when a critical safety system is turned off but something
|
|
40
|
+
important actually depends on it — instead of it quietly staying dark. If that
|
|
41
|
+
happens I will flag it clearly and give you three ways to resolve it: turn the
|
|
42
|
+
guard on, let it finish a short grace period while it is being rolled out, or have
|
|
43
|
+
you formally accept the risk (which I record with your name and reason so it is a
|
|
44
|
+
real decision, not a shrug). Accepting a risk needs your dashboard PIN — I cannot
|
|
45
|
+
do it on your behalf. If you ever ask "why is a critical guard flagged as a gap?",
|
|
46
|
+
that is this feature at work.
|
|
47
|
+
|
|
48
|
+
## Summary of New Capabilities
|
|
49
|
+
|
|
50
|
+
- `GET /guards` rows gain `loadBearing`, `criticalPath`, `loadBearingGap`,
|
|
51
|
+
`loadBearingSoaking`, `loadBearingAccepted`, and `acceptedFallbackReason`; the
|
|
52
|
+
summary and the capacity heartbeat gain `loadBearingGapKeys`,
|
|
53
|
+
`loadBearingSoakingKeys`, and `loadBearingAcceptedKeys`.
|
|
54
|
+
- `POST /guards/:key/accept-fallback` (dashboard-PIN-gated; `owner` + `reason`
|
|
55
|
+
required) records an owned per-machine acceptance; `DELETE` revokes it and
|
|
56
|
+
reopens the gap.
|
|
57
|
+
- The GuardPostureProbe raises a load-bearing gap on its own bounded, per-episode
|
|
58
|
+
attention topic, separate from the acute load-shed track (no masking).
|
|
59
|
+
- Per-machine by design: accepting a risk on one machine never silences a peer's
|
|
60
|
+
gap; a peer's critical-path label is looked up from the local, fleet-uniform
|
|
61
|
+
manifest.
|
|
62
|
+
|
|
63
|
+
## Evidence
|
|
64
|
+
|
|
65
|
+
Before: on an agent where a load-bearing feature ships dark (e.g. the inbound-message
|
|
66
|
+
queue with `enabled:false`), `GET /guards` reported it identically to any optional
|
|
67
|
+
dark feature — an `off:dark-default` row with no signal that a critical path depended
|
|
68
|
+
on it, so nothing ever surfaced the exposure (the postmortem's silent-loss shape).
|
|
69
|
+
|
|
70
|
+
After (verified over the real HTTP route + real disk in the Tier-3 e2e): the same
|
|
71
|
+
dark guard's `/guards` row now carries `loadBearing:true`, a `criticalPath` string,
|
|
72
|
+
and `loadBearingGap:true`, and the summary lists it under `loadBearingGapKeys`.
|
|
73
|
+
`POST /guards/:key/accept-fallback` with a valid PIN + owner + reason flips the row to
|
|
74
|
+
`loadBearingAccepted` (gap cleared, reason visible); `DELETE` reopens the gap, and both
|
|
75
|
+
survive a fresh-server "reboot" reading the same durable per-machine record. The
|
|
76
|
+
masking regression drives the REAL `createAttentionItem` funnel and asserts an acute
|
|
77
|
+
off-runtime-divergent guard surfaces while a load-bearing-gap episode has been open —
|
|
78
|
+
and an inert-lever test proves the funnel dedups by item id, so the separate episode
|
|
79
|
+
track (not a healthKey change) is what carries the fix. `npx tsc --noEmit` clean and
|
|
80
|
+
`npm run lint` exit 0.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Side-Effects Review — G3 Dark-but-Load-Bearing Guard Classification
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `g3-dark-but-load-bearing-guards`
|
|
4
|
+
**Date:** `2026-07-01`
|
|
5
|
+
**Author:** Echo (autonomous)
|
|
6
|
+
**Spec:** `docs/specs/g3-dark-but-load-bearing-guards.md` (review-convergence 2026-07-02, 6 iterations, cross-model codex GPT-5.5; approved:true)
|
|
7
|
+
**Parent principle:** A Dark Feature Guards Nothing (STANDARDS-REGISTRY.md, ratified 2026-07-01 PR #1316)
|
|
8
|
+
**Second-pass reviewer:** not-required (Tier 2; observe-only classifier + one PIN-gated suppress route; no new gating/blocking decision, byte-identical `/guards` for a consumer ignoring the new fields)
|
|
9
|
+
|
|
10
|
+
## Summary of the change
|
|
11
|
+
|
|
12
|
+
The `/guards` inventory could not express "this dark feature is load-bearing" — a
|
|
13
|
+
feature that ships DARK but that a CRITICAL PATH depends on was indistinguishable
|
|
14
|
+
from a genuinely-optional dark feature. It sat quiet while the path it should guard
|
|
15
|
+
ran unguarded (the 2026-07-01 silent-loss postmortem's case study: operator message
|
|
16
|
+
delivery depended on dark guards, and nothing surfaced it).
|
|
17
|
+
|
|
18
|
+
G3 adds an optional `loadBearing`/`criticalPath`/`soakWindowDays`/`declaredLoadBearingAt`
|
|
19
|
+
manifest declaration and classifies a load-bearing guard in a silent-unguarded posture
|
|
20
|
+
into ONE of three states — `loadBearingGap` (loud, its OWN attention channel),
|
|
21
|
+
`loadBearingSoaking` (graduate arm, no attention item), `loadBearingAccepted` (owned,
|
|
22
|
+
PIN-authenticated operator acceptance). `criticalPath` travels on EVERY anomaly of a
|
|
23
|
+
load-bearing guard. Pure classifier, observe-only — it never gates.
|
|
24
|
+
|
|
25
|
+
### Files added
|
|
26
|
+
- `src/monitoring/guardAcceptedFallbacks.ts` — ALL disk I/O for the per-machine
|
|
27
|
+
operator-accept store (`state/guard-accepted-fallbacks.json`, keyed
|
|
28
|
+
`<machineId>:<guardKey>`). Keeps the classifier PURE: the caller reads/scopes
|
|
29
|
+
once and threads the map in. Missing/corrupt file ⇒ empty map (safe direction).
|
|
30
|
+
- Tests: `tests/unit/monitoring/guard-posture-loadbearing.test.ts`,
|
|
31
|
+
`tests/unit/monitoring/guard-posture-probe-loadbearing.test.ts`,
|
|
32
|
+
`tests/integration/guards-accept-fallback-route.test.ts`,
|
|
33
|
+
`tests/e2e/guards-loadbearing-lifecycle.test.ts`,
|
|
34
|
+
`tests/unit/PostUpdateMigrator-guardLoadBearingSection.test.ts`.
|
|
35
|
+
|
|
36
|
+
### Files modified
|
|
37
|
+
- `src/monitoring/guardManifest.ts` — 4 optional fields; the two NEW manifest lints
|
|
38
|
+
(`validateGuardManifest`); the curated initial load-bearing set (inboundQueue +
|
|
39
|
+
strandedTopicSentinel, both operator-message-delivery critical-path, 30-day soak
|
|
40
|
+
window declared 2026-07-01).
|
|
41
|
+
- `src/monitoring/guardPostureView.ts` — the SIX new row fields + three-state
|
|
42
|
+
precedence (accepted → soaking-within-window → gap) on a threaded `now`; both
|
|
43
|
+
`deriveGuardRow` and `buildGuardInventory` stay PURE (accept map threaded via
|
|
44
|
+
`DeriveInput`/`opts`); `ROW_FIELD_ALLOWLIST` extended with all six by name; summary
|
|
45
|
+
+ heartbeat gain the three key-lists.
|
|
46
|
+
- `src/monitoring/probes/GuardPostureProbe.ts` — the SEPARATE `load-bearing-gap`
|
|
47
|
+
episode track (own `openEpisodeId`/`episodeEmitted`/anomalies + item-id namespace
|
|
48
|
+
`guard-posture-loadbearing:ep-N`); acute close condition tests `currentAcute` (not
|
|
49
|
+
the shared set); `load-bearing-gap` class pushed in both evaluate paths; criticalPath
|
|
50
|
+
on all load-bearing anomalies; `alertLoadBearingGaps` sub-flag (default-on).
|
|
51
|
+
- `src/core/types.ts` — the 3 optional key-list fields on `GuardPostureSummary`.
|
|
52
|
+
- `src/server/routes.ts` — GET /guards threads the accept map + now; the PIN-gated
|
|
53
|
+
`POST`/`DELETE /guards/:key/accept-fallback` route (owner+reason required).
|
|
54
|
+
- `src/commands/server.ts` — all 3 `buildGuardInventory` call sites thread the accept
|
|
55
|
+
map + now (route, probe `getLocalPosture`, heartbeat `selfGuardPosture`); the probe's
|
|
56
|
+
`alertLoadBearingGaps` sub-flag resolved from config (default true).
|
|
57
|
+
- `src/scaffold/templates.ts` + `src/core/PostUpdateMigrator.ts` — Agent Awareness +
|
|
58
|
+
Migration Parity (content-sniffed on `loadBearingGap`).
|
|
59
|
+
- `src/data/state-coherence-registry.json` — the new `guard-accepted-fallbacks`
|
|
60
|
+
store category + a `resolution`-class retention policy.
|
|
61
|
+
|
|
62
|
+
## Decision-point inventory
|
|
63
|
+
|
|
64
|
+
- **Added — one PIN-gated route** (`POST/DELETE /guards/:key/accept-fallback`): the
|
|
65
|
+
ONLY new decision surface. It SUPPRESSES a safety SIGNAL for a named operator; it
|
|
66
|
+
never allows/blocks an action. Gated by `checkMandatePin` (Know Your Principal — a
|
|
67
|
+
Bearer token cannot accept a safety risk); `owner` AND `reason` both REQUIRED (400
|
|
68
|
+
otherwise); a non-load-bearing key is 404.
|
|
69
|
+
- **Added — classifier flags**: `deriveGuardRow` sets orthogonal FLAGS; the nine
|
|
70
|
+
`GuardEffectiveState` values + the normative precedence table are UNCHANGED. No new
|
|
71
|
+
effective state, so the nine-state contract cannot regress.
|
|
72
|
+
- **Added — probe anomaly class** `load-bearing-gap` on its OWN episode track.
|
|
73
|
+
Observe-only: the probe only ever calls the attention funnel; never gates.
|
|
74
|
+
- No agent-to-user message-flow gate is added.
|
|
75
|
+
|
|
76
|
+
## Roll-up across the seven review dimensions
|
|
77
|
+
|
|
78
|
+
1. **Over-block**: none. The classifier adds flags; `/guards` is byte-identical for a
|
|
79
|
+
consumer ignoring the new fields. The accept route only suppresses a signal — it
|
|
80
|
+
loosens nothing and blocks nothing. The alert has a rollback sub-flag
|
|
81
|
+
(`alertLoadBearingGaps:false`) that keeps the classification.
|
|
82
|
+
2. **Under-block**: none. No gate was loosened. A load-bearing dark guard is now MORE
|
|
83
|
+
visible, not less. Soaking only defers the alert within a bounded, code-constant
|
|
84
|
+
window; a typo'd `declaredLoadBearingAt` falls to the LOUD gap (safe direction).
|
|
85
|
+
3. **Masking**: the round-3/round-4 fix. `load-bearing-gap` is a designed long-lived
|
|
86
|
+
anomaly; running it on a SEPARATE episode track (its own item-id namespace) means a
|
|
87
|
+
week-open gap can never hold the acute track's episode open. The regression test
|
|
88
|
+
drives the REAL `createAttentionItem` funnel and asserts an acute off-runtime-divergent
|
|
89
|
+
surfaces while the lb episode is open; the inert-lever test proves the funnel dedups
|
|
90
|
+
by ID, so a healthKey-only split would leave masking intact.
|
|
91
|
+
4. **Multi-machine**: classification is per-machine (an accept on machine A never
|
|
92
|
+
silences a peer's gap — proven by test). Manifest fields are fleet-uniform constants;
|
|
93
|
+
a peer's criticalPath is looked up from the local manifest. The heartbeat carries the
|
|
94
|
+
three key-lists (Array.isArray-guarded for an un-upgraded peer). The 3rd-site fix: the
|
|
95
|
+
heartbeat compute threads the local accept map, so an accepted guard never ships to
|
|
96
|
+
peers as a false gap.
|
|
97
|
+
5. **Purity / I/O layering**: `deriveGuardRow` AND `buildGuardInventory` gain no `fs`.
|
|
98
|
+
The caller reads the accept file ONCE per inventory build and threads the scoped map;
|
|
99
|
+
the new `guardAcceptedFallbacks.ts` module owns all disk I/O. One accept-file read per
|
|
100
|
+
inventory build, never per guard.
|
|
101
|
+
6. **Bounded Notification Surface**: each track surfaces as ONE bounded, coalesced,
|
|
102
|
+
per-episode class-level forum topic (two class-level topics max, never per-guard).
|
|
103
|
+
Soaking pushes NO attention item.
|
|
104
|
+
7. **Migration Parity / Agent Awareness**: the manifest ships as code (reaches existing
|
|
105
|
+
agents automatically); the CLAUDE.md vocabulary + accept route reach existing agents
|
|
106
|
+
via a content-sniffed `migrateClaudeMd` addendum; new agents get it in `generateClaudeMd`.
|
|
107
|
+
Migration idempotency + parity covered by a dedicated test.
|
|
108
|
+
|
|
109
|
+
## Rollout / rollback
|
|
110
|
+
|
|
111
|
+
Classification + route + separate-track wiring ship ALWAYS-ON as pure observability
|
|
112
|
+
(additive, never gating). The probe ALERT on `load-bearing-gap` rides the existing
|
|
113
|
+
GuardPostureProbe enablement + the `monitoring.guardPostureProbe.alertLoadBearingGaps`
|
|
114
|
+
sub-flag (default-on; soak windows are code constants). Rollback = set the sub-flag
|
|
115
|
+
false; `/guards` keeps the classification. The accept store is a per-machine JSON file;
|
|
116
|
+
DELETE scopes to the operator record only, never the manifest soak constant.
|
|
117
|
+
|
|
118
|
+
## Test evidence
|
|
119
|
+
|
|
120
|
+
`npx tsc --noEmit` clean; full `npm run lint` exit 0. New tests green: 21 unit
|
|
121
|
+
(classification/lints/allowlist/key-lists/purity), 9 probe-track unit (separate track,
|
|
122
|
+
masking regression against the real funnel, inert-lever guard, soaking-silent,
|
|
123
|
+
criticalPath, heartbeat array-guard, per-machine independence, alert rollback), 7
|
|
124
|
+
integration (PIN gate, owner-required, accept clears gap, DELETE-revoke survives reboot),
|
|
125
|
+
3 e2e (feature-alive: six fields + accept route mounted). Existing guard/posture suites
|
|
126
|
+
(monitoring 260, guard integration+e2e 51, server+migrator 27, parity+discoverability 170)
|
|
127
|
+
all pass.
|
|
128
|
+
|
|
129
|
+
## Post-review fixes (2026-07-02)
|
|
130
|
+
|
|
131
|
+
- Rebased onto JKHeadley/main after PR #1317 (silent-loss fix) merged. #1317's
|
|
132
|
+
rejected-enumeration + wiring gate on `routes.ts`/`server.ts`/`types.ts` and G3's
|
|
133
|
+
additive edits compose cleanly (adjacent, non-overlapping edits; tsc + `npm run lint`
|
|
134
|
+
clean post-rebase; the guard/posture suite re-run green).
|
|
135
|
+
- CI's full sharded suite caught two mechanical guard-tracking failures the loaded-box
|
|
136
|
+
local run (exit 144) missed. Neither weakens a guard:
|
|
137
|
+
1. `no-silent-fallbacks` (492 > baseline 491): `readAcceptedFallbacks`'s
|
|
138
|
+
missing/corrupt-file catch in `guardAcceptedFallbacks.ts` is annotated
|
|
139
|
+
`@silent-fallback-ok` — an empty map is the SAFE direction (no phantom accept can
|
|
140
|
+
suppress a real load-bearing gap). Threshold NOT bumped; count back to 491.
|
|
141
|
+
2. `feature-delivery-completeness`: the migrator's new `loadBearingGap` content-sniff
|
|
142
|
+
section is registered in `legacyMigratorSections` (migrator-only behavioral
|
|
143
|
+
awareness, also emitted inline by `generateClaudeMd` so a fresh init self-matches).
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Side-Effects Review — silent-loss-refusal-conservation
|
|
2
|
+
|
|
3
|
+
Spec: `docs/specs/silent-loss-refusal-conservation.md` (converged, approved, operator-preapproved topic 29836).
|
|
4
|
+
Change: 5 increments (A RouteOutcome honesty, B receiver trace, C unified loss notice, D wiring-time registry gate + fixture refusal, E agent awareness) + 2 migrations.
|
|
5
|
+
|
|
6
|
+
## Over-block (could this REFUSE a message it should deliver?)
|
|
7
|
+
|
|
8
|
+
- The wiring gate's whole design is fail-TOWARD-delivery for the incident class: a
|
|
9
|
+
degenerate / never-populated / clean-ENOENT / operator-unresolvable registry
|
|
10
|
+
DISARMS (delivers). The only paths that REJECT are (a) a HEALTHY populated
|
|
11
|
+
registry that genuinely doesn't resolve the sender (real deauthorization — the
|
|
12
|
+
existing, intended behavior) and (b) an UNKNOWN_UNSAFE (corrupt/partial/tampered)
|
|
13
|
+
store, which fails CLOSED by design — but even there the LOCALLY-bound operator
|
|
14
|
+
passes via the topic-operator binding (KYP), and the rejection is conserved
|
|
15
|
+
through the §C notice so the sender is TOLD (never silent).
|
|
16
|
+
- Risk: `classifyRegistry` reads the RAW file and counts fixture rows as
|
|
17
|
+
"populated". A fixture-clobbered store therefore classifies `populated`, and a
|
|
18
|
+
NON-operator real sender with no bound operator for that topic would be rejected
|
|
19
|
+
in the window BEFORE the §4 boot migration quarantines the fixtures. Mitigations:
|
|
20
|
+
the migration runs at boot; the operator ALWAYS passes via the operator-resolution
|
|
21
|
+
disarm; the rejection is a loud §C notice, never silent loss. Net: strictly
|
|
22
|
+
better than today (today: silent total loss for everyone).
|
|
23
|
+
- New-user first message: a genuinely fresh install (`[]`, no high-water) disarms →
|
|
24
|
+
the operator's first message is delivered. `legitimate-user-named-Olivia-registers`
|
|
25
|
+
pins that a display name is NEVER a fixture criterion.
|
|
26
|
+
|
|
27
|
+
## Under-block (could this DELIVER something it should refuse?)
|
|
28
|
+
|
|
29
|
+
- The degenerate disarm delivers WITHOUT sender re-validation. This is intentional
|
|
30
|
+
and bounded: it only applies to a registry that would otherwise reject EVERYONE
|
|
31
|
+
(protecting nothing). The mesh's outer wall is unchanged — MeshRpc is still
|
|
32
|
+
signed, recipient-bound, router-only; only the DEFENSE-IN-DEPTH sender re-check
|
|
33
|
+
is disarmed, not the transport authz.
|
|
34
|
+
- The read-only probe UserManager (no server key) KEEPS a fixture-with-allow-marker
|
|
35
|
+
it cannot verify. Only reachable by a near-impossible legit fixture-collision AND
|
|
36
|
+
a data-only forger simultaneously; the AUTHORITATIVE server load (with the key)
|
|
37
|
+
quarantines a bogus marker. A fixture uid resolving in a non-authoritative probe
|
|
38
|
+
does not authorize anything (it is not a real sender). Documented out-of-scope:
|
|
39
|
+
a fully-FS-privileged local process (§2.D honest threat model).
|
|
40
|
+
|
|
41
|
+
## Abstraction (right seams? single funnels?)
|
|
42
|
+
|
|
43
|
+
- The refusal is a FIRST-CLASS `RouteOutcome.action:'rejected'` + a distinct
|
|
44
|
+
`LedgerState:'rejected'` terminal — enumerated into EVERY consumer (router
|
|
45
|
+
consumers, forceReplace, drain escape, `decideIngress`/`beginProcessing`/`isActedOn`).
|
|
46
|
+
A ratchet test pins that no consumer maps `rejected`→success.
|
|
47
|
+
- The loss notice is ONE funnel (`SenderRejectionNoticer`) shared by the live path
|
|
48
|
+
(Telegram/Slack) AND the drain `reportLoss('sender-deauthorized')` — unified on
|
|
49
|
+
ONE canonical cause so the two paths can't emit two wordings.
|
|
50
|
+
- The fixture matcher, high-water, and allow-marker sign/verify are single-source
|
|
51
|
+
modules imported by the write path, load path, gate, AND the §4 migration — they
|
|
52
|
+
cannot drift.
|
|
53
|
+
|
|
54
|
+
## Signal-vs-authority
|
|
55
|
+
|
|
56
|
+
- The divergence signal (local-resolves + remote-rejects) is ADVISORY only — it
|
|
57
|
+
raises one deduped coherence alert, NEVER auto-remediation (it cannot distinguish
|
|
58
|
+
a degenerate registry from a lawful deauth still replicating). Auto re-place is a
|
|
59
|
+
tracked follow-up.
|
|
60
|
+
- The receiver-side trace (`onRejected` → `mesh-rejections.jsonl`) is metadata-only
|
|
61
|
+
observability — it never changes the NACK. A trace fault is swallowed.
|
|
62
|
+
- The gate's alerts are deduped/rate-limited signals; the ARM/DISARM decision is
|
|
63
|
+
the authority, driven only by verified registry STATE (never a config flag).
|
|
64
|
+
|
|
65
|
+
## Adjacent systems
|
|
66
|
+
|
|
67
|
+
- `MessageProcessingLedger` gains a `rejected` state + `rejected_at` column
|
|
68
|
+
(idempotent `ALTER TABLE ADD COLUMN`, self-initializing — no migrator step). The
|
|
69
|
+
three ledger consumers are enumerated so a redelivery is DROPPED, not resurrected
|
|
70
|
+
(closes the double-notify-on-redelivery class).
|
|
71
|
+
- `UserManager` write path now THROWS `TestIdentityRefusedError` on a fixture id.
|
|
72
|
+
Full-suite risk: suites/harnesses that legitimately write `livetest`/`g3test`/`u-*`
|
|
73
|
+
ids. Mitigation: the double-keyed escape (`INSTAR_ALLOW_TEST_IDENTITIES=1` + the
|
|
74
|
+
on-disk test-home marker) is set for legitimately-fixture-writing suites; the
|
|
75
|
+
load path refuse-and-SKIPS (never throws → never fails boot).
|
|
76
|
+
- `high-water` write on load/register is monotonic + one-time + best-effort (never
|
|
77
|
+
breaks a write). It shares users.json's FS trust boundary (no separate envelope).
|
|
78
|
+
|
|
79
|
+
## Rollback
|
|
80
|
+
|
|
81
|
+
- Single PR; `git revert` restores all CODE behavior. CARVE-OUT (§6): the §4
|
|
82
|
+
boot-remediation QUARANTINES fixture rows out of users.json to a timestamped
|
|
83
|
+
backup — a revert removes the migration code but does not (and should not)
|
|
84
|
+
restore quarantined rows; a wrongly-quarantined legitimate user is recovered from
|
|
85
|
+
that backup. The boot classification is READ-ONLY (no self-heal write), so it
|
|
86
|
+
leaves no state to unwind. Surviving inert artifacts: `logs/mesh-rejections.jsonl`,
|
|
87
|
+
`state/registry-high-water.json`, any already-sent notices/alerts.
|
|
88
|
+
- Per-increment rollback is clean (disjoint seams): router mapping / handler deps /
|
|
89
|
+
notice consumer / wiring gate + UserManager / template.
|
|
90
|
+
|
|
91
|
+
## Always-on justification
|
|
92
|
+
|
|
93
|
+
Reachability / no-silent-loss floor ("The Agent Is Always Reachable" class): the
|
|
94
|
+
constitution forbids dark-shipping the guarantee that a message is delivered or
|
|
95
|
+
loudly accounted for. A/B/E are pure honesty + hygiene (identical on every healthy
|
|
96
|
+
path). C fires only where today = silent loss. D's disarm fires only in degenerate
|
|
97
|
+
states that today reject everyone; its own failure mode (a probe throwing on a
|
|
98
|
+
transiently-locked POPULATED store) is pinned to ARM by `populated-registry-always-arms`,
|
|
99
|
+
so the new code cannot regress the existing security control.
|
|
100
|
+
|
|
101
|
+
## Follow-up: CI-surfaced test adjustments (2026-07-02)
|
|
102
|
+
|
|
103
|
+
The full sharded CI suite surfaced pre-existing tests the change interacts with.
|
|
104
|
+
Fixed WITHOUT weakening the production fixture-refusal or the no-silent-fallbacks
|
|
105
|
+
guard, via the intended escape/annotation mechanisms:
|
|
106
|
+
|
|
107
|
+
- **Fixture-writing tests** (e2e coordination-mandate + authorization-request; integration
|
|
108
|
+
permissions-routes) now use the intended DOUBLE-KEYED test escape via a shared
|
|
109
|
+
`tests/helpers/allow-test-identities.ts` (env `INSTAR_ALLOW_TEST_IDENTITIES=1` + the
|
|
110
|
+
on-disk `.instar-test-home` marker). Production behavior is unchanged — a stray env var
|
|
111
|
+
alone still cannot disable the guard.
|
|
112
|
+
- **`no-silent-fallbacks` ratchet**: my four new-file intentional fail-toward-delivery
|
|
113
|
+
catches + the four inbound-wiring construction catches in `server.ts` are annotated
|
|
114
|
+
`@silent-fallback-ok` with a per-site reason. Net flagged count returns to baseline
|
|
115
|
+
491 — NO baseline bump; the guard is not weakened.
|
|
116
|
+
- **`feature-delivery-completeness`**: the migrator "Sender-Rejection Notices" section is
|
|
117
|
+
added to `legacyMigratorSections` (template + migrator behavioral awareness, no
|
|
118
|
+
framework-shadowed route).
|
|
119
|
+
- **`capabilities-discoverability`**: the `/users` route prefix (`POST /users/allow-test-identity`)
|
|
120
|
+
is classified in `CapabilityIndex.INTERNAL_PREFIXES` (dashboard-PIN-gated, operator/support-only,
|
|
121
|
+
not an agent-invokable capability).
|
|
122
|
+
- **`session-pool-activation-wiring`**: the fail-safe scan window grew 5200→6500 to contain
|
|
123
|
+
the `rejected` short-circuit added inside the inbound interception block (documented
|
|
124
|
+
window-maintenance pattern).
|