instar 1.3.726 → 1.3.728

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.
Files changed (32) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +53 -4
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/IntelligenceRouter.d.ts +57 -0
  5. package/dist/core/IntelligenceRouter.d.ts.map +1 -1
  6. package/dist/core/IntelligenceRouter.js +135 -20
  7. package/dist/core/IntelligenceRouter.js.map +1 -1
  8. package/dist/core/types.d.ts +39 -0
  9. package/dist/core/types.d.ts.map +1 -1
  10. package/dist/core/types.js.map +1 -1
  11. package/dist/messaging/slack/SlackAdapter.d.ts +15 -0
  12. package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
  13. package/dist/messaging/slack/SlackAdapter.js +20 -2
  14. package/dist/messaging/slack/SlackAdapter.js.map +1 -1
  15. package/dist/messaging/slack/types.d.ts +38 -0
  16. package/dist/messaging/slack/types.d.ts.map +1 -1
  17. package/dist/messaging/slack/types.js +4 -0
  18. package/dist/messaging/slack/types.js.map +1 -1
  19. package/dist/permissions/TestWorkspacePrincipalSource.d.ts +133 -0
  20. package/dist/permissions/TestWorkspacePrincipalSource.d.ts.map +1 -0
  21. package/dist/permissions/TestWorkspacePrincipalSource.js +189 -0
  22. package/dist/permissions/TestWorkspacePrincipalSource.js.map +1 -0
  23. package/dist/permissions/index.d.ts +1 -0
  24. package/dist/permissions/index.d.ts.map +1 -1
  25. package/dist/permissions/index.js +1 -0
  26. package/dist/permissions/index.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/data/builtin-manifest.json +2 -2
  29. package/upgrades/1.3.728.md +215 -0
  30. package/upgrades/side-effects/per-target-swap-timeout.md +120 -0
  31. package/upgrades/side-effects/slack-test-workspace-principal-source.md +227 -0
  32. package/upgrades/1.3.726.md +0 -61
@@ -0,0 +1,227 @@
1
+ # Side-Effects Review — Slack test-workspace-scoped principal source
2
+
3
+ **Version / slug:** `slack-test-workspace-principal-source`
4
+ **Date:** `2026-07-02`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `echo (independent second pass)`
7
+
8
+ ## Summary of the change
9
+
10
+ Adds a sanctioned, workspace-scoped principal source for the Slack permission gate's
11
+ role resolution, so the live-test scenario cast can resolve WITHOUT being seeded into
12
+ the production user registry (`users.json`). Files touched:
13
+ `src/permissions/TestWorkspacePrincipalSource.ts` (new: `TestWorkspacePrincipalSource`
14
+ + `ChainedUserLookup`), `src/permissions/index.ts` (export), `src/messaging/slack/types.ts`
15
+ (`permissionGate.testCast` config shape incl. the `testWorkspace` marker),
16
+ `src/messaging/slack/SlackAdapter.ts` (capture + expose the VERIFIED connected team id
17
+ from `auth.test`), `src/commands/server.ts` (wire production-registry-first chained
18
+ resolution + fail-closed on the missing marker), the runbook
19
+ (`docs/specs/SLACK-ORG-TEST-WORKSPACE-RUNBOOK.md`), and the unit test suite. The single
20
+ decision point it interacts with is the gate's **principal role resolution** — it FEEDS
21
+ that lookup as a fallback data source; it adds no new block/allow authority.
22
+
23
+ ## Decision-point inventory
24
+
25
+ - `SlackPrincipalResolver` role resolution (`src/permissions/SlackPrincipalResolver.ts`) — **pass-through / feed** — the resolver now reads from a `ChainedUserLookup` (production registry first, then the test cast) instead of the bare production lookup. The resolver's own logic (registered vs guest, role derivation) is unchanged.
26
+ - `TestWorkspacePrincipalSource.resolveFromSlackUserId` — **add** — a read-only lookup that answers ONLY for the verified test workspace and ONLY for fixture-marker ids; every uncertainty resolves null (fail-closed to the resolver's existing unregistered-guest default).
27
+ - The permission gate's allow/refuse decision — **pass-through** — unchanged; it consumes the resolved principal exactly as before.
28
+
29
+ ---
30
+
31
+ ## 1. Over-block
32
+
33
+ **What legitimate inputs does this change reject that it shouldn't?**
34
+
35
+ No new block/allow surface — the source only resolves an id to a role (or null); the
36
+ gate still owns every allow/refuse decision. The only "rejection" it performs is at
37
+ LOAD time (refusing to admit a cast entry): a non-fixture id, an invalid role, a
38
+ duplicate, or an over-cap entry is dropped from the cast. Those refusals cannot
39
+ over-block a real user, because a refused cast entry simply falls through to production
40
+ resolution (which is the authoritative path anyway). A production-registered user is
41
+ never affected — the production lookup is consulted first and wins.
42
+
43
+ ---
44
+
45
+ ## 2. Under-block
46
+
47
+ **What failure modes does this still miss?**
48
+
49
+ The source is deliberately narrow, so the "misses" are by design: it does not itself
50
+ enforce anything (observe-only gate is unchanged), and it does not attempt to detect a
51
+ misconfigured `workspaceId` (a `workspaceId` that never matches the connected team id
52
+ just means the cast stays permanently inert — the safe direction, surfaced by the boot
53
+ log's `admitted/refused` counts and the runbook's re-provision checklist). It also does
54
+ not cover a cast id that a future edit removes from the fixture-marker list — such an id
55
+ would then be refused at load (`not-a-fixture-identity`), which is loud, not silent.
56
+
57
+ ---
58
+
59
+ ## 3. Level-of-abstraction fit
60
+
61
+ **Is this at the right layer?**
62
+
63
+ Yes — it is a low-level, in-memory DATA SOURCE (a `UserLookup` implementation) that feeds
64
+ the existing higher-level `SlackPrincipalResolver`, which in turn feeds the gate. It does
65
+ NOT re-implement role derivation (that stays in `deriveRole`), does NOT re-implement the
66
+ allow/refuse policy (that stays in `SlackPermissionGate`), and does NOT duplicate the
67
+ fixture-identity matcher (it reuses the single `matchesTestIdentityToken` the production
68
+ guard uses — one matcher, never two lists). The workspace-scope check is the one piece of
69
+ new logic, and it lives at exactly the layer that knows the verified connection (a supplier
70
+ reading `SlackAdapter.getConnectedTeamId()`).
71
+
72
+ ---
73
+
74
+ ## 4. Signal vs authority compliance
75
+
76
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
77
+
78
+ **Does this change hold blocking authority with brittle logic?**
79
+
80
+ - [x] No — this change produces a signal (a resolved role, or null) consumed by an existing smart gate; it holds no block/allow authority.
81
+
82
+ The source answers "what role, if any, does this id play in the sanctioned test
83
+ workspace?" and nothing more. It never blocks, never sends, never mutates. The
84
+ authoritative allow/refuse decision remains entirely with `SlackPermissionGate`. The
85
+ one gate-shaped decision it makes — "should I answer for this id at all?" — is a
86
+ structural OWNERSHIP check (verified connected team id EXACTLY equals the configured
87
+ workspace id), not a brittle content heuristic, and it fails CLOSED (null) on every
88
+ uncertainty. That is the signal-vs-authority-compliant shape: a cheap structural check
89
+ that only ever WITHHOLDS an answer, never grants an escalation.
90
+
91
+ ---
92
+
93
+ ## 5. Interactions
94
+
95
+ **Does this interact with existing checks, recovery paths, or infrastructure?**
96
+
97
+ - **Shadowing:** production resolution is consulted FIRST in the chain, so the cast can
98
+ never shadow a genuinely registered user. The reverse (cast shadowed by production) is
99
+ intended — a production record always wins. Verified by the `ChainedUserLookup`
100
+ precedence test.
101
+ - **Double-fire:** none — a single resolution per inbound message; `ChainedUserLookup`
102
+ returns at the first non-null source.
103
+ - **Races:** none — the cast is built once at wiring time and is immutable thereafter;
104
+ `resolveFromSlackUserId` is a pure map read behind the scope gate. No shared mutable
105
+ state, no concurrent writers.
106
+ - **Feedback loops:** none — the source is read-only and does not record or learn.
107
+ - **Fixture-identity guard:** complementary, not conflicting. The production guard refuses
108
+ fixtures INTO `users.json`; this source admits ONLY fixtures. Same matcher, disjoint
109
+ homes — an identity lives in exactly one store.
110
+
111
+ ---
112
+
113
+ ## 6. External surfaces
114
+
115
+ **Does this change anything visible outside the immediate code path?**
116
+
117
+ - Other agents on the same machine? No — the source is per-Slack-adapter, in-memory.
118
+ - Other users of the install base? No — dark by default; nothing loads unless a Slack
119
+ config adds a `testCast` block with `testWorkspace: true`. On a plain install the whole
120
+ path is inert.
121
+ - External systems? Reads one additional field (`team_id`) from the EXISTING `auth.test`
122
+ response the adapter already calls at startup. No new Slack API call, no contract change
123
+ (both changed adapter files carry a `CONTRACT-EVIDENCE: EXEMPT` marker with the reason).
124
+ - Persistent state? None — the source writes nothing (no state dir, no fs handle). It
125
+ cannot create/modify `users.json`, operator bindings, or any file. This is the load-
126
+ bearing authority-scope property (KYP): the cast feeds role resolution ONLY.
127
+ - Operator surface (Mobile-Complete Operator Actions)? No operator-facing actions — this is
128
+ test-harness wiring configured in `.instar/config.json`, not a runtime operator action.
129
+
130
+ ---
131
+
132
+ ## 6b. Operator-surface quality (Operator-Surface Quality standard)
133
+
134
+ No operator surface — not applicable. This change touches no dashboard renderer, approval
135
+ page, or grant/revoke/secret-drop form.
136
+
137
+ ---
138
+
139
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
140
+
141
+ **Machine-local BY DESIGN.** A Slack adapter is bound to one workspace and runs on the
142
+ machine serving that Slack connection; the `testCast` config and the verified connected
143
+ team id are properties of THAT machine's live Slack socket. There is no cross-machine
144
+ state to replicate: the cast is read-only config-derived data, not learned/durable state,
145
+ so it neither strands on topic transfer nor needs a pool-wide merged read. It emits no
146
+ user-facing notices (no one-voice concern) and generates no URLs. If the same live-test
147
+ Slack adapter is ever run on two machines, each independently derives the same cast from
148
+ the same config and each independently verifies its own connected team id — no divergence
149
+ is possible because the input is static config plus a per-machine verified connection.
150
+
151
+ ---
152
+
153
+ ## 8. Rollback cost
154
+
155
+ **Pure code + config change — revert and ship a patch.** No persistent state is created,
156
+ so there is no data migration and no agent-state repair. Because the feature is dark by
157
+ default (nothing activates without an explicit `testCast` block), a rollback has zero
158
+ user-visible surface on any normal install. Removing the `testCast` block from the one
159
+ live-test config (or setting `enforce`/`observeOnly` off) instantly reverts to
160
+ production-registry-only resolution with no restart-order hazards beyond the normal
161
+ "restart to pick up config" rule.
162
+
163
+ ---
164
+
165
+ ## Conclusion
166
+
167
+ The review produced one substantive design fix during the build: the in-flight code
168
+ lacked the required `testWorkspace: true` self-declaration marker (requirement 3). It was
169
+ added as a fail-closed gate that lives IN the source constructor (not only at the wiring
170
+ site), so the "ignored + one loud log line, no production impact" guarantee holds for
171
+ every caller — Structure over Willpower. The scoping proof (why roles can't leak into a
172
+ production workspace) is the load-bearing property and is covered on both sides by tests:
173
+ matching-workspace resolves the cast; non-matching-workspace is byte-identical to having
174
+ no cast at all. The change is a signal-producing data source with no new authority, dark
175
+ by default, and is clear to ship.
176
+
177
+ ---
178
+
179
+ ## Second-pass review (if required)
180
+
181
+ **Reviewer:** echo (independent second pass — the change touches the word "gate")
182
+ **Independent read of the artifact: concur**
183
+
184
+ Independently re-derived the three safety properties and confirmed each is covered on both
185
+ sides in `tests/unit/slack-test-workspace-principal-source.test.ts`: (1) scope — matching
186
+ team + listed id resolves the role, non-matching team is invisible AND byte-identical to
187
+ the no-cast baseline; (2) partition — a non-fixture id is refused at load and the cap
188
+ bounds the source so it can't become a shadow registry; (3) opt-in — a missing
189
+ `testWorkspace` marker disables the whole source, loudly, and is byte-identical to no cast.
190
+ Also confirmed the authority-scope assertions: the source exposes only the `UserLookup`
191
+ read contract (no write/registry/operator methods) and takes no state dir, so it
192
+ structurally cannot write `users.json` or feed operator binding / sender validation. One
193
+ concern considered and dismissed: the scope check uses the adapter's connected team id
194
+ rather than a per-message `team_id` — but a Slack adapter is bound to exactly one workspace
195
+ (one bot token), so the verified connected team id IS authoritative for every inbound
196
+ message and is strictly stronger than trusting an envelope field. Concur with the review's
197
+ conclusion: clear to ship.
198
+
199
+ **Second-pass reviewer (dedicated subagent, 2026-07-02): CONCUR.** Independently
200
+ re-verified in code (not from this artifact): (a) no blocking authority — resolution
201
+ returns a record or null only, the gate keeps every allow/refuse; (b) fail-closed on
202
+ disabled/throw/unverified/mismatch; (c) zero changes under `src/users/` (`git diff`),
203
+ production fixture-identity guard fully intact; (d) production-first chain order at the
204
+ wiring site; (e) no fs/write surface, unreachable from sender auth (`isAuthorized` runs
205
+ BEFORE the observer and never consults a `UserLookup`); (f) both test suites re-run green;
206
+ (g) no-`testCast` installs byte-identical (absent/invalid/disabled block leaves the bare
207
+ production lookup; the whole wiring sits in try/catch degrading to production-only). Two
208
+ non-blocking notes, both addressed in this build: the Tier-2 integration suite is staged
209
+ with the commit, and `workspaceId` is now stored TRIMMED (a padded config value can no
210
+ longer yield a permanently-inert cast; covered by a new unit test).
211
+
212
+ ---
213
+
214
+ ## Evidence pointers
215
+
216
+ - `tests/unit/slack-test-workspace-principal-source.test.ts` — 23 tests, all passing (both
217
+ sides of scope, marker, partition, chain precedence, wiring, authority-scope, trim).
218
+ - `tests/integration/slack-testcast-principal-pipeline.test.ts` — 5 tests, all passing
219
+ (Tier 2: the EXACT server.ts composition driven through the real inbound chokepoint
220
+ `SlackAdapter._handleMessage`, asserted against the durable decision ledger — the
221
+ row-29 owner-resolves-owner proof, the cross-workspace invisibility proof, fail-closed
222
+ pre-verification, unlisted-uid guest default, and production-precedence-beats-cast).
223
+ Tier 3 (E2E route liveness) is not applicable: the feature adds no HTTP routes, and its
224
+ only wiring site (`server.ts` Slack messaging block) requires live Slack credentials.
225
+ - `npx tsc --noEmit` — exit 0.
226
+ - Root-cause datapoint: `docs/audits/slack-permission-fp-review-2026-07.md` row 29 (the
227
+ owner seat resolving `guest, registered:false` after the 2026-07-01 registry rebuild).
@@ -1,61 +0,0 @@
1
- # Upgrade Guide — vNEXT
2
-
3
- <!-- assembled-by: assemble-next-md -->
4
- <!-- bump: patch -->
5
-
6
- ## What Changed
7
-
8
- `AgentWorktreeReaper.start()` scheduled ONLY a 24h `setInterval` — no initial
9
- pass. Agent servers restart far more often than daily (auto-updates, sleep/wake
10
- supervisor bounces), so the interval timer reset on every restart and an
11
- **enabled + armed** reaper never ran a single pass. Measured live on 2026-07-02:
12
- 86 worktrees / 25GB accumulated on a machine with `enabled: true, dryRun: false`
13
- and `lastPassAt: 0` — the feature was on and structurally inert (root cause of
14
- that day's fseventsd/reboot resource incident).
15
-
16
- The fix:
17
-
18
- - New `initialPassDelayMs` config (default 15 min; `<= 0` disables — the exact
19
- legacy behavior as a no-release rollback lever) on
20
- `monitoring.agentWorktreeReaper`.
21
- - `start()` now schedules a ONE-TIME initial pass after that delay (unref'd
22
- timer, cleared by `stop()`), then the unchanged 24h cadence. The delay keeps
23
- the pass off the busy post-boot window.
24
- - The pass runs through the same `reap()` as always: same KEEP gates (in-use /
25
- dirty / unmerged / detached), same dry-run gating, same `maxReapsPerPass`
26
- blast-radius cap, same per-path reclaim-failure breaker. Nothing about WHAT
27
- may be deleted changed — only WHEN passes run.
28
- - `GET /worktrees/agent-reaper` snapshot additively reports
29
- `initialPassPending`.
30
- - CLAUDE.md template + PostUpdateMigrator addendum so deployed agents learn the
31
- new behavior (idempotent, content-sniffed on `initialPassDelayMs`).
32
-
33
- ## What to Tell Your User
34
-
35
- <!-- audience: user, maturity: stable -->
36
-
37
- If you switched on the stale-worktree auto-cleaner, it now genuinely runs: one
38
- cleaning pass about 15 minutes after each server start, then daily as designed.
39
- Previously a quirk meant frequent server restarts kept pushing its first run
40
- into the future forever, so leftover work folders could quietly pile up into
41
- tens of gigabytes even with the cleaner enabled. Its safety rules are untouched:
42
- it still only removes folders whose work is fully merged and saved, still does
43
- dry-run first, and it stays entirely off unless you enabled it.
44
-
45
- ## Summary of New Capabilities
46
-
47
- - The stale-worktree reaper actually fires on real deployments: a one-time
48
- cleaning pass ~15 minutes after boot (tunable/disableable via
49
- `monitoring.agentWorktreeReaper.initialPassDelayMs`), unchanged daily cadence
50
- thereafter, and `initialPassPending` visibility on the existing report route.
51
-
52
- ## Evidence
53
-
54
- - `tests/unit/agent-worktree-reaper.test.ts`: 47/47 green — 7 new tests (initial
55
- pass fires exactly once at the configured delay and before the interval;
56
- respects dry-run; disabled reaper sets no timers; `<= 0` restores
57
- interval-only; `stop()` cancels the pending pass; 24h cadence unchanged after
58
- the initial pass; snapshot reports `initialPassPending` honestly).
59
- - Live incident data (2026-07-02, topic 30379): reaper enabled+armed with 25
60
- reap-eligible worktrees and `lastPassAt: 0`; server uptime history shows no
61
- 24h-continuous window for weeks.