instar 1.3.439 → 1.3.441
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 +31 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/types.d.ts +8 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/messaging/slack/SlackAdapter.d.ts +13 -0
- package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
- package/dist/messaging/slack/SlackAdapter.js +43 -0
- package/dist/messaging/slack/SlackAdapter.js.map +1 -1
- package/dist/messaging/slack/types.d.ts +11 -0
- package/dist/messaging/slack/types.d.ts.map +1 -1
- package/dist/messaging/slack/types.js.map +1 -1
- package/dist/permissions/AnomalyScorer.d.ts +54 -0
- package/dist/permissions/AnomalyScorer.d.ts.map +1 -0
- package/dist/permissions/AnomalyScorer.js +68 -0
- package/dist/permissions/AnomalyScorer.js.map +1 -0
- package/dist/permissions/IntentClassifier.d.ts +33 -0
- package/dist/permissions/IntentClassifier.d.ts.map +1 -0
- package/dist/permissions/IntentClassifier.js +88 -0
- package/dist/permissions/IntentClassifier.js.map +1 -0
- package/dist/permissions/PermissionDecisionLedger.d.ts +42 -0
- package/dist/permissions/PermissionDecisionLedger.d.ts.map +1 -0
- package/dist/permissions/PermissionDecisionLedger.js +61 -0
- package/dist/permissions/PermissionDecisionLedger.js.map +1 -0
- package/dist/permissions/RolePolicy.d.ts +34 -0
- package/dist/permissions/RolePolicy.d.ts.map +1 -0
- package/dist/permissions/RolePolicy.js +60 -0
- package/dist/permissions/RolePolicy.js.map +1 -0
- package/dist/permissions/SlackPermissionGate.d.ts +62 -0
- package/dist/permissions/SlackPermissionGate.d.ts.map +1 -0
- package/dist/permissions/SlackPermissionGate.js +117 -0
- package/dist/permissions/SlackPermissionGate.js.map +1 -0
- package/dist/permissions/SlackPermissionObserver.d.ts +37 -0
- package/dist/permissions/SlackPermissionObserver.d.ts.map +1 -0
- package/dist/permissions/SlackPermissionObserver.js +39 -0
- package/dist/permissions/SlackPermissionObserver.js.map +1 -0
- package/dist/permissions/SlackPrincipalResolver.d.ts +39 -0
- package/dist/permissions/SlackPrincipalResolver.d.ts.map +1 -0
- package/dist/permissions/SlackPrincipalResolver.js +60 -0
- package/dist/permissions/SlackPrincipalResolver.js.map +1 -0
- package/dist/permissions/SlackUserRegistry.d.ts +64 -0
- package/dist/permissions/SlackUserRegistry.d.ts.map +1 -0
- package/dist/permissions/SlackUserRegistry.js +114 -0
- package/dist/permissions/SlackUserRegistry.js.map +1 -0
- package/dist/permissions/index.d.ts +14 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +14 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/permissions/testing/SlackScenarioHarness.d.ts +35 -0
- package/dist/permissions/testing/SlackScenarioHarness.d.ts.map +1 -0
- package/dist/permissions/testing/SlackScenarioHarness.js +130 -0
- package/dist/permissions/testing/SlackScenarioHarness.js.map +1 -0
- package/dist/permissions/types.d.ts +108 -0
- package/dist/permissions/types.d.ts.map +1 -0
- package/dist/permissions/types.js +21 -0
- package/dist/permissions/types.js.map +1 -0
- 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 +104 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/users/UserManager.d.ts +9 -0
- package/dist/users/UserManager.d.ts.map +1 -1
- package/dist/users/UserManager.js +21 -0
- package/dist/users/UserManager.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/src/data/state-coherence-registry.json +24 -0
- package/upgrades/1.3.440.md +25 -0
- package/upgrades/1.3.441.md +26 -0
- package/upgrades/side-effects/slack-org-permission-gate.md +80 -0
- package/upgrades/side-effects/slack-org-permissions-phase1.md +68 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Side-Effects Review — Slack org permission gate (Slice 0)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `slack-org-permission-gate`
|
|
4
|
+
**Date:** `2026-06-08`
|
|
5
|
+
**Author:** `Echo (instar-dev agent)`
|
|
6
|
+
**Second-pass reviewer:** `not required (single-author; observe-only, dark-by-default)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds the first vertical slice of the Slack organizational permission system
|
|
11
|
+
(`docs/specs/SLACK-ORG-INTEGRATION-SPEC.md`). New `src/permissions/` module: a
|
|
12
|
+
`SlackPermissionGate` that turns a verified principal + natural-language request into
|
|
13
|
+
an `allow | clarify | refuse | step-up` verdict, composed from `RolePolicy`
|
|
14
|
+
(role→tier ceilings + an enumerated, deterministic floor), `HeuristicIntentClassifier`
|
|
15
|
+
(conservative floor detection), `AnomalyScorer` (relationship step-up hook),
|
|
16
|
+
`SlackPrincipalResolver` (verified-Slack-id → principal), and an observe-only
|
|
17
|
+
`PermissionDecisionLedger`. Wired into `SlackAdapter._handleMessage` via an optional
|
|
18
|
+
`SlackPermissionObserver` and into server startup behind a `permissionGate.observeOnly`
|
|
19
|
+
config flag (DARK by default). Adds `UserProfile.slackUserId`/`orgRole`,
|
|
20
|
+
`UserManager.resolveFromSlackUserId`, two read routes (`/permissions/decisions`,
|
|
21
|
+
`/permissions/scenario-suite`), and a state-registry entry for the ledger.
|
|
22
|
+
|
|
23
|
+
## Decision-point inventory
|
|
24
|
+
|
|
25
|
+
- `SlackPermissionGate.evaluate` — **add** — the authority decision (allow/clarify/refuse/step-up). Ships OBSERVE-ONLY (logs, never blocks).
|
|
26
|
+
- `RolePolicy` (floor + ceilings) — **add** — deterministic Layer-0 floor; pure, no I/O.
|
|
27
|
+
- `SlackAdapter._handleMessage` observe call — **add (pass-through)** — fire-and-forget; does not alter the existing flow.
|
|
28
|
+
- `UserProfile` / `UserManager` — **add** — additive optional fields + one new resolution method; no existing path changed.
|
|
29
|
+
|
|
30
|
+
## 1. Over-block
|
|
31
|
+
|
|
32
|
+
Observe-only: it blocks **nothing** today, so over-block has no live effect — it can only over-LOG a "would-refuse." Measured over-refusals in the ledger are exactly the FP signal we want before enabling enforcement. When enforcement is later turned on, the known over-block risks are: (a) the heuristic classifier tags a benign message as a floor action (e.g. "delete this slack message" → matches destructive-data) and refuses; (b) a relayed-but-legitimate request ("the client asked us to email them") trips external-send. Both are why enforcement stays off until the observed FP rate is acceptable (§11).
|
|
33
|
+
|
|
34
|
+
## 2. Under-block
|
|
35
|
+
|
|
36
|
+
The deterministic heuristic classifier will miss obfuscated floor requests (e.g. "send forty grand to…" spelled out, or a deploy phrased without the word prod). This is acceptable for the floor because: (a) observe-only today, and (b) the production design adds an LLM classifier ABOVE the floor for ambiguity — but the floor itself must stay deterministic/fail-closed (a missed floor classification falls through to a non-floor tier and is still role-gated). Also: the anomaly scorer is a coarse heuristic (urgency + atypical-action) and will under-flag a calm, in-character-looking compromise — Pillar 3 ships observe-only precisely to measure this before it gates.
|
|
37
|
+
|
|
38
|
+
## 3. Level-of-abstraction fit
|
|
39
|
+
|
|
40
|
+
Correct layering. The **floor** is a low-level deterministic primitive (cheap, conservative, fail-closed) — appropriate because the dangerous path must NOT depend on an LLM that could fail-open (per the "no silent degradation to brittle fallback" standard). The **judgment band** is authority-level and is designed to be LLM-backed in production via the injectable `IntentClassifier` interface (the heuristic is the deterministic test/fallback path, and it routes ambiguity to CLARIFY, never to a silent allow). The gate does not re-implement `ExternalOperationGate`; the spec wires the floor THROUGH it + the Coordination Mandate in Phase 1 (this slice stands alone as the decision core).
|
|
41
|
+
|
|
42
|
+
## 4. Signal vs authority compliance
|
|
43
|
+
|
|
44
|
+
**Required reference:** docs/signal-vs-authority.md
|
|
45
|
+
|
|
46
|
+
- [x] No — this change has no block/allow surface **yet**. It ships observe-only: the gate produces a logged verdict (a signal) and never blocks delivery.
|
|
47
|
+
|
|
48
|
+
The gate is structurally an authority (it WILL block when `enforce` is enabled), but its blocking logic is NOT brittle: the floor is deterministic-conservative-fail-closed, and the judgment band is designed for LLM backing with conversational context. **Follow-up flagged:** before `enforce: true` is ever set (a later phase), this artifact's §1/§2 must be revisited with real FP-rate data from the ledger, and the judgment-band LLM classifier must be the one holding authority — not the heuristic fallback.
|
|
49
|
+
|
|
50
|
+
## 5. Interactions
|
|
51
|
+
|
|
52
|
+
- **Shadowing:** the observe call runs AFTER the fail-closed `authorizedUserIds` AuthGate and BEFORE the `mention-only` skip, so it sees every authorized message (directed or overheard) without changing whether the message is processed. It shadows nothing (it has no return effect).
|
|
53
|
+
- **Double-fire:** none — it only writes to its own ledger; it does not act on the event.
|
|
54
|
+
- **Races:** the ledger is append-only single-writer per process; the observe call is `void`'d (fire-and-forget) so it cannot delay the handler.
|
|
55
|
+
- **Feedback loops:** none — the ledger is read-only observability; nothing consumes it to change behavior.
|
|
56
|
+
|
|
57
|
+
## 6. External surfaces
|
|
58
|
+
|
|
59
|
+
- **Other agents / install base:** none — the whole feature is dark by default (no config = no gate attached); pure no-op for every existing agent.
|
|
60
|
+
- **External systems (Slack):** none — no new Slack API calls; the observe path is local.
|
|
61
|
+
- **Persistent state:** one new append-only JSONL ledger (`state/slack-permission-decisions.jsonl`), registered in `state-coherence-registry.json` (machine-local, append-only, transport none). Created lazily; bounded by `readRecent(limit)`.
|
|
62
|
+
- **HTTP:** two new authenticated GET routes (Bearer-gated like all routes); read-only.
|
|
63
|
+
|
|
64
|
+
## 7. Rollback cost
|
|
65
|
+
|
|
66
|
+
Pure additive code + one optional config flag. Back-out = revert the commits and ship a patch. No migration needed: `UserProfile.slackUserId`/`orgRole` are optional fields (absent on existing profiles = no effect); the ledger file is observe-only and can be deleted with zero consequence. No agent-state repair, no user-visible regression during rollback (the feature is dark on every install).
|
|
67
|
+
|
|
68
|
+
## Conclusion
|
|
69
|
+
|
|
70
|
+
The review produced no design changes — the slice was built observe-only and floor-deterministic by intent, which is exactly what the signal-vs-authority and "no silent degradation" standards require for a gate that will eventually hold blocking authority. One concern is flagged for the NEXT phase (not this one): the §4 follow-up requiring an FP-rate review and an LLM-backed judgment band before `enforce` is ever enabled. The change is clear to ship as a dark, observe-only foundation pending the spec's `approved: true`.
|
|
71
|
+
|
|
72
|
+
## Evidence pointers
|
|
73
|
+
|
|
74
|
+
- 38 tests green: `tests/unit/slack-permission-gate.test.ts` (23), `tests/unit/slack-principal-resolver.test.ts` (7), `tests/unit/slack-permission-wiring.test.ts` (5), `tests/integration/permissions-routes.test.ts` (3).
|
|
75
|
+
- The six worked-example rows + their expected/actual verdicts: `GET /permissions/scenario-suite` (and `src/permissions/testing/SlackScenarioHarness.ts`).
|
|
76
|
+
- No-regression: 90 tests green across touched modules (slack adapter, user-manager, permissions).
|
|
77
|
+
|
|
78
|
+
## Follow-up (capabilities classification, 2026-06-09)
|
|
79
|
+
|
|
80
|
+
The `/permissions/*` routes are registered in `routes.ts`, so the capabilities-discoverability lint requires the prefix to be classified. **Decision: `INTERNAL_PREFIXES`** in `src/server/CapabilityIndex.ts` (agent-invisible) — Slice 0 is dark/observe-only and its routes (registration approval, the decision ledger, the scenario suite) are operator/internal, not a user-surfaced capability. When the enforce path ships and the gate becomes a live capability, the prefix should graduate to `CAPABILITY_INDEX`. Side-effect: the permission gate is intentionally NOT advertised in `/capabilities` while dark (consistent with §6 "dark by default").
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Side-Effects Review — Slack org permissions Phase 1 (registration + enforce path)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `slack-org-permissions-phase1`
|
|
4
|
+
**Date:** 2026-06-09
|
|
5
|
+
**Author:** Instar Agent (echo)
|
|
6
|
+
**Second-pass reviewer:** REQUIRED (block/allow on inbound messaging) — see Phase 5 below
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Builds on Slice 0 (the dark/observe-only Slack permission gate, merged in #1005). Phase 1 adds two things:
|
|
11
|
+
1. **Conversational registration** (`src/permissions/SlackUserRegistry.ts` + `src/server/routes.ts` `POST /permissions/registrations/{register,approve,deny}` + `GET …/pending`): admins register users with a role; self-registration creates a pending entry for approval. Persists to `state/slack-pending-registrations.json` (registered in `state-coherence-registry.json`).
|
|
12
|
+
2. **The enforce path** (`src/messaging/slack/SlackAdapter._handleMessage`): when the injected permission observer is **enforcing**, a non-`allow` verdict sends the conversational refusal/clarify reply (via the existing `sendToChannel`, in-thread) and returns — blocking the message from reaching the session. When NOT enforcing (the default), the observe call stays fire-and-forget exactly as in Slice 0.
|
|
13
|
+
|
|
14
|
+
Decision points touched: the inbound-message block/allow in `_handleMessage` (the enforce branch).
|
|
15
|
+
|
|
16
|
+
## Decision-point inventory
|
|
17
|
+
|
|
18
|
+
- `SlackAdapter._handleMessage` enforce branch — **add** — when `observer.enforcing`, a non-allow verdict blocks message processing + sends the gate reply. Dark by default (`enforcing=false` → unchanged observe-only behavior).
|
|
19
|
+
- `SlackUserRegistry` register/approve/deny — **add** — identity/role assignment; not a message gate (data layer feeding the principal resolver).
|
|
20
|
+
- `POST /permissions/registrations/*` routes — **add** — Bearer-gated admin/operator routes; classified INTERNAL (capabilities) like the rest of `/permissions`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 1. Over-block
|
|
25
|
+
|
|
26
|
+
When `enforcing=true`, the gate blocks any inbound message whose verdict ≠ `allow`. Over-block risk: a legitimate message mis-classified as floor/clarify is withheld from the session and gets a refusal/clarify reply instead. **Mitigation:** the path ships dark (default `enforcing=false`); the verdict logic is Slice 0's (floor = deterministic-conservative; the judgment band routes ambiguity to CLARIFY, which still replies to the user rather than silently dropping). Enabling enforce is gated behind a later phase that requires real FP-rate data from the observe ledger first (carried over from the Slice 0 §4 follow-up).
|
|
27
|
+
|
|
28
|
+
## 2. Under-block
|
|
29
|
+
|
|
30
|
+
When `enforcing=false` (default) the gate blocks nothing — identical to Slice 0 observe-only. That is intentional: Phase 1 ships the mechanism dark; it does not claim to protect anything yet. The registration layer does not itself enforce (a registered role only matters once the gate consumes it). No new protection is asserted.
|
|
31
|
+
|
|
32
|
+
## 3. Level-of-abstraction fit
|
|
33
|
+
|
|
34
|
+
Correct layer. The enforce decision sits in `_handleMessage` AFTER the fail-closed `authorizedUserIds` AuthGate and uses the SAME observer/verdict the observe path already produced — so it's the consume side of an existing signal, not a new parallel detector. Registration is a data layer (identity → role), feeding the existing `SlackPrincipalResolver`; it does not re-implement identity.
|
|
35
|
+
|
|
36
|
+
## 4. Signal vs authority compliance
|
|
37
|
+
|
|
38
|
+
**Required reference:** docs/signal-vs-authority.md. This is the change that turns the Slice 0 SIGNAL into an AUTHORITY (it can block) — so the compliance bar is real:
|
|
39
|
+
- The blocking authority is NOT brittle: the floor is deterministic-conservative-fail-closed; the judgment band is the injectable `IntentClassifier` (heuristic = deterministic test/fallback; LLM-backed in production) and routes ambiguity to CLARIFY, never to a silent allow. A blocked **directed** request (DM or @mention) always gets a conversational reply. (Precise nuance, per the second-pass review: the one no-reply block path is an *overheard/undirected* actionable message in `respondMode:'all'` — verdict `refuse/overheard/''` — which is correctly NOT actioned and NOT replied-to per §6.9; that is by design, not a silent drop of a directed user request.)
|
|
40
|
+
- It ships **dark** (`enforcing=false`), so the authority is installed but inert until a later, FP-data-gated phase explicitly enables it with the LLM judge holding the band — not the heuristic. **This artifact does NOT authorize `enforce:true`.**
|
|
41
|
+
|
|
42
|
+
## 5. Interactions
|
|
43
|
+
|
|
44
|
+
- **Shadowing:** the enforce branch runs in the same spot as the Slice 0 observe call (after AuthGate, before the mention-only skip). When enforcing, it returns early (blocks) — so it shadows the normal handler BY DESIGN for non-allow verdicts; when not enforcing it changes nothing.
|
|
45
|
+
- **Double-fire:** the observe ledger write and the enforce decision use the same single verdict; no double evaluation.
|
|
46
|
+
- **Races:** `SlackUserRegistry` writes `slack-pending-registrations.json` (single-writer per process, atomic write); the enforce decision is synchronous in the handler (no race with the fire-and-forget observe path, which only runs when NOT enforcing).
|
|
47
|
+
- **Registration vs gate:** an unregistered user resolves to the lowest role → the gate treats them conservatively; registration only ever raises trust via an explicit admin/approval action.
|
|
48
|
+
|
|
49
|
+
## 6. External surfaces
|
|
50
|
+
|
|
51
|
+
- **Other agents / install base:** none — dark by default (no permission observer attached unless configured); pure no-op for every existing agent.
|
|
52
|
+
- **External systems (Slack):** **no new Slack Web API calls** — the enforce reply reuses the existing `sendToChannel` (already contract-tested in Slice 0). The Slack API contract surface is unchanged by Phase 1 (verified: no new `client.*`/`postMessage` calls in the diff).
|
|
53
|
+
- **Persistent state:** one new file `state/slack-pending-registrations.json` (registered machine-local in `state-coherence-registry.json`). Created lazily; bounded.
|
|
54
|
+
- **HTTP:** four new Bearer-gated routes under `/permissions/registrations/*` (classified INTERNAL — agent-invisible while dark).
|
|
55
|
+
|
|
56
|
+
## 7. Rollback cost
|
|
57
|
+
|
|
58
|
+
Low / additive. Back-out = revert the 3 commits + ship a patch. No migration: `slack-pending-registrations.json` is observe/admin data (deletable with no consequence); the enforce branch is inert while dark, so reverting it changes nothing on any install (no one has `enforcing=true`). No agent-state repair, no user-visible regression.
|
|
59
|
+
|
|
60
|
+
## Phase 5 — Second-pass review
|
|
61
|
+
|
|
62
|
+
REQUIRED: this change adds a **block/allow decision on inbound messaging**. An independent reviewer must audit this artifact and append "Concur" or "Concern: …" below before the trace is written with `--second-pass true`.
|
|
63
|
+
|
|
64
|
+
## Second-pass review (independent reviewer)
|
|
65
|
+
|
|
66
|
+
**Concur with the review.** Verified against the actual code: the blocking authority is genuinely fail-closed — the floor (`SlackPermissionGate` lines 140–180) is deterministic, regex-detected via `HeuristicIntentClassifier` (no LLM in the floor path), `roleCanAuthorizeFloor` clears only `owner`/explicit grant, an unregistered user resolves to `guest`/`registered:false` and is refused, ambiguity routes to `clarify` (line 131), and `NullAnomalyScorer` (default) can't spuriously raise step-up; the enforce branch sits after the fail-closed AuthGate, reuses the single observe verdict, and is genuinely inert when `enforcing=false` (`observer.enforcing` → `deps.enforce ?? false` → original fire-and-forget path, proven by the `enforcing=false` unit test), with no new Slack API calls (reuses pre-existing `sendToChannel`; diff grep for `client.*`/`postMessage`/`chat.*` additions returns none).
|
|
67
|
+
|
|
68
|
+
*Minor wording nuance (not a blocker, no code change needed):* §4's "a blocked message **always** gets a conversational reply" is slightly overstated. The one non-allow verdict carrying an empty message is `refuse / overheard / ''` (gate line 108), reachable only for an **undirected** (non-DM, non-mention) actionable message in `respondMode:'all'` — in that case the enforce branch blocks with no reply (`if (verdict.message)` is false → silent `return`). This is correct by design (an overheard command must not be actioned per §6.9, and replying to a message not directed at the bot would be channel noise) — it is not a silent drop of a *directed* user request, and it ships dark regardless. Worth a one-word softening of §4 ("a blocked *directed* request always gets a reply") at the authoring agent's discretion; the behavior itself is sound.
|