instar 1.3.726 → 1.3.727
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 +45 -4
- package/dist/commands/server.js.map +1 -1
- package/dist/messaging/slack/SlackAdapter.d.ts +15 -0
- package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
- package/dist/messaging/slack/SlackAdapter.js +20 -2
- package/dist/messaging/slack/SlackAdapter.js.map +1 -1
- package/dist/messaging/slack/types.d.ts +38 -0
- package/dist/messaging/slack/types.d.ts.map +1 -1
- package/dist/messaging/slack/types.js +4 -0
- package/dist/messaging/slack/types.js.map +1 -1
- package/dist/permissions/TestWorkspacePrincipalSource.d.ts +133 -0
- package/dist/permissions/TestWorkspacePrincipalSource.d.ts.map +1 -0
- package/dist/permissions/TestWorkspacePrincipalSource.js +189 -0
- package/dist/permissions/TestWorkspacePrincipalSource.js.map +1 -0
- package/dist/permissions/index.d.ts +1 -0
- package/dist/permissions/index.d.ts.map +1 -1
- package/dist/permissions/index.js +1 -0
- package/dist/permissions/index.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.727.md +143 -0
- package/upgrades/side-effects/slack-test-workspace-principal-source.md +227 -0
- 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).
|
package/upgrades/1.3.726.md
DELETED
|
@@ -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.
|