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.
@@ -0,0 +1,133 @@
1
+ /**
2
+ * TestWorkspacePrincipalSource — the sanctioned, workspace-scoped principal source
3
+ * for the Slack live-test scenario cast (roadmap 0.3 entry condition).
4
+ *
5
+ * WHY THIS EXISTS (the 2026-07-01 lesson): the June scenario drives seeded the
6
+ * five-seat test cast into the production user registry (`users.json`). The
7
+ * silent-loss registry rebuild removed them, and the fixture-identity guard
8
+ * ("Test Identity Never Enters Production State", silent-loss-refusal-conservation
9
+ * §2.D) now — correctly — refuses to let them back in. Principal resolution for
10
+ * the live-test workspace therefore needs a home that is NOT the production
11
+ * registry. This module is that home.
12
+ *
13
+ * THE PARTITION INVARIANT (the load-bearing design decision):
14
+ * - The production registry REFUSES fixture identities (the existing guard,
15
+ * untouched by this module).
16
+ * - This source ACCEPTS ONLY fixture identities — every cast entry's
17
+ * slackUserId must match the SAME single matcher the production guard uses
18
+ * (`matchesTestIdentityToken` from `users/testIdentityMarkers.ts`). A
19
+ * non-fixture UID is refused at load, loudly.
20
+ * By construction an identity can live in exactly one of the two stores —
21
+ * a real employee's UID can never be smuggled into the cast to gain a role
22
+ * outside the audited registration path, and standing up a NEW test cast
23
+ * structurally forces the fixture-marker list to be updated first (which is
24
+ * what keeps the production guard aware of the new fixtures). The single
25
+ * matcher can never drift into two lists.
26
+ *
27
+ * THE SELF-DECLARATION MARKER (fail-closed to ignoring the cast): the config
28
+ * object MUST self-declare `testWorkspace: true`. Without that marker the source
29
+ * refuses to load a SINGLE principal — it disables itself, emits ONE loud log
30
+ * line, and every lookup returns null (production resolution is byte-identical to
31
+ * having no cast at all). The marker exists so a cast config can never be
32
+ * activated by accident: sanctioning a live-test workspace is a deliberate,
33
+ * self-evident opt-in, never an implicit side effect of the block existing.
34
+ *
35
+ * SCOPING (fail-closed): the cast resolves ONLY while the Slack adapter's
36
+ * VERIFIED connected team id (captured from Slack's own `auth.test` response at
37
+ * adapter start — never from config alone) equals the configured test
38
+ * `workspaceId`. Before verification, on verification failure, or when the
39
+ * adapter is connected to any other workspace, every lookup returns null and
40
+ * the resolver falls through to its existing safe default (unregistered guest).
41
+ *
42
+ * READ-ONLY BY DESIGN: this source never writes anything — not users.json, not
43
+ * a state file. It is consulted ONLY by the permission gate's principal
44
+ * resolver (production-registry-first via {@link ChainedUserLookup}); it is
45
+ * invisible to UserManager, sender auth (`authorizedUserIds`), cross-machine
46
+ * replication, and every other identity surface.
47
+ *
48
+ * Note on imports: `testIdentityMarkers.ts` is dependency-light (node builtins +
49
+ * a type-only core import), so reusing its matcher keeps the permission module
50
+ * free of runtime core dependencies — and keeps ONE fixture matcher, not two.
51
+ *
52
+ * Design: docs/specs/SLACK-ORG-INTEGRATION-SPEC.md §6.2.1;
53
+ * runbook: docs/specs/SLACK-ORG-TEST-WORKSPACE-RUNBOOK.md.
54
+ */
55
+ import type { ResolvedUserRecord, UserLookup } from './SlackPrincipalResolver.js';
56
+ /** One seat of the live-test scenario cast (configured in the Slack config block). */
57
+ export interface TestCastEntry {
58
+ /** The seat's REAL Slack user id in the live-test workspace (must be a fixture-marker id). */
59
+ slackUserId: string;
60
+ /** Display name for conversational messages — never a basis for authority. */
61
+ name?: string;
62
+ /** The org role this seat plays (owner / admin / member / contributor / …). */
63
+ orgRole: string;
64
+ }
65
+ /** Why a configured cast entry was refused at load. */
66
+ export interface RejectedCastEntry {
67
+ slackUserId: string;
68
+ reason: 'missing-slack-user-id' | 'not-a-fixture-identity' | 'invalid-role' | 'duplicate' | 'cast-cap-exceeded';
69
+ }
70
+ /**
71
+ * Hard ceiling on cast seats. The scenario cast is five seats today; the cap
72
+ * exists so the test-cast path can never quietly become a shadow user registry.
73
+ */
74
+ export declare const MAX_TEST_CAST_SEATS = 12;
75
+ export interface TestWorkspacePrincipalSourceOpts {
76
+ /** The sanctioned live-test workspace/team id (T…) this cast is scoped to. */
77
+ workspaceId: string;
78
+ /**
79
+ * The REQUIRED self-declaration marker. The source refuses to load ANY
80
+ * principal unless this is exactly `true` — fail-closed to ignoring the cast
81
+ * (one loud log line, never a crash, never a production-registry effect).
82
+ */
83
+ testWorkspace: boolean;
84
+ /** The configured cast seats. Invalid entries are refused (see {@link RejectedCastEntry}). */
85
+ principals: TestCastEntry[];
86
+ /**
87
+ * Supplier of the adapter's VERIFIED connected team id (from `auth.test`).
88
+ * Returning null/undefined (not yet verified, verification failed) keeps the
89
+ * source inert — fail-closed to the resolver's unregistered-guest default.
90
+ */
91
+ getVerifiedWorkspaceId: () => string | null | undefined;
92
+ }
93
+ /** Why the whole source was disabled at construction (loads zero principals). */
94
+ export type TestCastDisabledReason = 'missing-testWorkspace-marker';
95
+ export declare class TestWorkspacePrincipalSource implements UserLookup {
96
+ private readonly bySlackId;
97
+ private readonly workspaceId;
98
+ private readonly getVerifiedWorkspaceId;
99
+ /** Entries refused at load (surfaced loudly by the wiring site). */
100
+ readonly rejected: RejectedCastEntry[];
101
+ /**
102
+ * True when the whole source refused to load (the self-declaration marker was
103
+ * absent). A disabled source holds ZERO principals and resolves everything to
104
+ * null — production resolution is byte-identical to having no cast configured.
105
+ */
106
+ readonly disabled: boolean;
107
+ /** Why the source disabled itself (only set when {@link disabled} is true). */
108
+ readonly disabledReason?: TestCastDisabledReason;
109
+ constructor(opts: TestWorkspacePrincipalSourceOpts);
110
+ /** Number of admitted cast seats. */
111
+ get size(): number;
112
+ /**
113
+ * Resolve a cast seat — ONLY while the verified connected workspace equals the
114
+ * sanctioned test workspace. Any uncertainty (no verified id yet, supplier
115
+ * throw, mismatch) resolves null: fail-closed to unregistered guest.
116
+ */
117
+ resolveFromSlackUserId(slackUserId: string): ResolvedUserRecord | null;
118
+ }
119
+ /**
120
+ * ChainedUserLookup — production-registry-first principal resolution.
121
+ *
122
+ * Sources are consulted in order; the first non-null record wins. The wiring
123
+ * site puts the production registry FIRST, so a genuinely registered user can
124
+ * never be shadowed or role-escalated by a cast entry, and the cast is only
125
+ * ever a fallback for identities the production registry does not know.
126
+ * A throwing source is skipped (resolution must never break the message path).
127
+ */
128
+ export declare class ChainedUserLookup implements UserLookup {
129
+ private readonly sources;
130
+ constructor(sources: UserLookup[]);
131
+ resolveFromSlackUserId(slackUserId: string): ResolvedUserRecord | null;
132
+ }
133
+ //# sourceMappingURL=TestWorkspacePrincipalSource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestWorkspacePrincipalSource.d.ts","sourceRoot":"","sources":["../../src/permissions/TestWorkspacePrincipalSource.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAElF,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,8FAA8F;IAC9F,WAAW,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,uDAAuD;AACvD,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EACF,uBAAuB,GACvB,wBAAwB,GACxB,cAAc,GACd,WAAW,GACX,mBAAmB,CAAC;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,MAAM,WAAW,gCAAgC;IAC/C,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,8FAA8F;IAC9F,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B;;;;OAIG;IACH,sBAAsB,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACzD;AAED,iFAAiF;AACjF,MAAM,MAAM,sBAAsB,GAAG,8BAA8B,CAAC;AAEpE,qBAAa,4BAA6B,YAAW,UAAU;IAC7D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyC;IACnE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAkC;IACzE,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,EAAE,CAAM;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAS;IACnC,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,CAAC,EAAE,sBAAsB,CAAC;gBAErC,IAAI,EAAE,gCAAgC;IA8DlD,qCAAqC;IACrC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;OAIG;IACH,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI;CAcvE;AAED;;;;;;;;GAQG;AACH,qBAAa,iBAAkB,YAAW,UAAU;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,UAAU,EAAE;IAElD,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI;CAYvE"}
@@ -0,0 +1,189 @@
1
+ /**
2
+ * TestWorkspacePrincipalSource — the sanctioned, workspace-scoped principal source
3
+ * for the Slack live-test scenario cast (roadmap 0.3 entry condition).
4
+ *
5
+ * WHY THIS EXISTS (the 2026-07-01 lesson): the June scenario drives seeded the
6
+ * five-seat test cast into the production user registry (`users.json`). The
7
+ * silent-loss registry rebuild removed them, and the fixture-identity guard
8
+ * ("Test Identity Never Enters Production State", silent-loss-refusal-conservation
9
+ * §2.D) now — correctly — refuses to let them back in. Principal resolution for
10
+ * the live-test workspace therefore needs a home that is NOT the production
11
+ * registry. This module is that home.
12
+ *
13
+ * THE PARTITION INVARIANT (the load-bearing design decision):
14
+ * - The production registry REFUSES fixture identities (the existing guard,
15
+ * untouched by this module).
16
+ * - This source ACCEPTS ONLY fixture identities — every cast entry's
17
+ * slackUserId must match the SAME single matcher the production guard uses
18
+ * (`matchesTestIdentityToken` from `users/testIdentityMarkers.ts`). A
19
+ * non-fixture UID is refused at load, loudly.
20
+ * By construction an identity can live in exactly one of the two stores —
21
+ * a real employee's UID can never be smuggled into the cast to gain a role
22
+ * outside the audited registration path, and standing up a NEW test cast
23
+ * structurally forces the fixture-marker list to be updated first (which is
24
+ * what keeps the production guard aware of the new fixtures). The single
25
+ * matcher can never drift into two lists.
26
+ *
27
+ * THE SELF-DECLARATION MARKER (fail-closed to ignoring the cast): the config
28
+ * object MUST self-declare `testWorkspace: true`. Without that marker the source
29
+ * refuses to load a SINGLE principal — it disables itself, emits ONE loud log
30
+ * line, and every lookup returns null (production resolution is byte-identical to
31
+ * having no cast at all). The marker exists so a cast config can never be
32
+ * activated by accident: sanctioning a live-test workspace is a deliberate,
33
+ * self-evident opt-in, never an implicit side effect of the block existing.
34
+ *
35
+ * SCOPING (fail-closed): the cast resolves ONLY while the Slack adapter's
36
+ * VERIFIED connected team id (captured from Slack's own `auth.test` response at
37
+ * adapter start — never from config alone) equals the configured test
38
+ * `workspaceId`. Before verification, on verification failure, or when the
39
+ * adapter is connected to any other workspace, every lookup returns null and
40
+ * the resolver falls through to its existing safe default (unregistered guest).
41
+ *
42
+ * READ-ONLY BY DESIGN: this source never writes anything — not users.json, not
43
+ * a state file. It is consulted ONLY by the permission gate's principal
44
+ * resolver (production-registry-first via {@link ChainedUserLookup}); it is
45
+ * invisible to UserManager, sender auth (`authorizedUserIds`), cross-machine
46
+ * replication, and every other identity surface.
47
+ *
48
+ * Note on imports: `testIdentityMarkers.ts` is dependency-light (node builtins +
49
+ * a type-only core import), so reusing its matcher keeps the permission module
50
+ * free of runtime core dependencies — and keeps ONE fixture matcher, not two.
51
+ *
52
+ * Design: docs/specs/SLACK-ORG-INTEGRATION-SPEC.md §6.2.1;
53
+ * runbook: docs/specs/SLACK-ORG-TEST-WORKSPACE-RUNBOOK.md.
54
+ */
55
+ import { matchesTestIdentityToken } from '../users/testIdentityMarkers.js';
56
+ import { ORG_ROLES } from './types.js';
57
+ /**
58
+ * Hard ceiling on cast seats. The scenario cast is five seats today; the cap
59
+ * exists so the test-cast path can never quietly become a shadow user registry.
60
+ */
61
+ export const MAX_TEST_CAST_SEATS = 12;
62
+ export class TestWorkspacePrincipalSource {
63
+ bySlackId = new Map();
64
+ workspaceId;
65
+ getVerifiedWorkspaceId;
66
+ /** Entries refused at load (surfaced loudly by the wiring site). */
67
+ rejected = [];
68
+ /**
69
+ * True when the whole source refused to load (the self-declaration marker was
70
+ * absent). A disabled source holds ZERO principals and resolves everything to
71
+ * null — production resolution is byte-identical to having no cast configured.
72
+ */
73
+ disabled = false;
74
+ /** Why the source disabled itself (only set when {@link disabled} is true). */
75
+ disabledReason;
76
+ constructor(opts) {
77
+ if (!opts.workspaceId || typeof opts.workspaceId !== 'string' || !opts.workspaceId.trim()) {
78
+ throw new Error('TestWorkspacePrincipalSource requires a non-empty workspaceId (the sanctioned live-test workspace)');
79
+ }
80
+ // Stored TRIMMED (second-pass review note): Slack's own `team_id` never
81
+ // carries whitespace, so a config value with a stray space would otherwise
82
+ // make the scope equality permanently false — an inert cast (fail-closed,
83
+ // but a silent config foot-gun the boot log alone would have to catch).
84
+ this.workspaceId = opts.workspaceId.trim();
85
+ this.getVerifiedWorkspaceId = opts.getVerifiedWorkspaceId;
86
+ // THE SELF-DECLARATION GATE (fail-closed): without an explicit
87
+ // `testWorkspace: true` the cast is IGNORED entirely — zero principals
88
+ // loaded, one loud line, no throw, no production impact. The loud line lives
89
+ // HERE (not only at the wiring site) so the fail-closed guarantee holds for
90
+ // EVERY caller — Structure > Willpower.
91
+ if (opts.testWorkspace !== true) {
92
+ this.disabled = true;
93
+ this.disabledReason = 'missing-testWorkspace-marker';
94
+ console.warn(`[slack] TestWorkspacePrincipalSource IGNORED for workspace ${this.workspaceId} — the testCast config is ` +
95
+ `missing its required "testWorkspace: true" self-declaration. Loading ZERO cast principals (fail-closed; the ` +
96
+ `production user registry is unaffected). Add "testWorkspace": true to sanction the live-test cast.`);
97
+ return; // no principals admitted
98
+ }
99
+ for (const entry of opts.principals ?? []) {
100
+ const slackUserId = typeof entry?.slackUserId === 'string' ? entry.slackUserId.trim() : '';
101
+ if (!slackUserId) {
102
+ this.rejected.push({ slackUserId: String(entry?.slackUserId ?? ''), reason: 'missing-slack-user-id' });
103
+ continue;
104
+ }
105
+ // THE PARTITION INVARIANT: only fixture-marker identities are admitted.
106
+ // Same single matcher as the production guard — never a second list.
107
+ if (!matchesTestIdentityToken(slackUserId)) {
108
+ this.rejected.push({ slackUserId, reason: 'not-a-fixture-identity' });
109
+ continue;
110
+ }
111
+ if (!entry.orgRole || !ORG_ROLES.includes(entry.orgRole)) {
112
+ this.rejected.push({ slackUserId, reason: 'invalid-role' });
113
+ continue;
114
+ }
115
+ if (this.bySlackId.has(slackUserId)) {
116
+ this.rejected.push({ slackUserId, reason: 'duplicate' });
117
+ continue;
118
+ }
119
+ if (this.bySlackId.size >= MAX_TEST_CAST_SEATS) {
120
+ this.rejected.push({ slackUserId, reason: 'cast-cap-exceeded' });
121
+ continue;
122
+ }
123
+ this.bySlackId.set(slackUserId, {
124
+ // Namespaced id so a cast principal is visibly a test seat in every
125
+ // ledger row / audit surface it appears in.
126
+ id: `test-cast:${slackUserId}`,
127
+ name: entry.name?.trim() || slackUserId,
128
+ permissions: [entry.orgRole],
129
+ orgRole: entry.orgRole,
130
+ });
131
+ }
132
+ }
133
+ /** Number of admitted cast seats. */
134
+ get size() {
135
+ return this.bySlackId.size;
136
+ }
137
+ /**
138
+ * Resolve a cast seat — ONLY while the verified connected workspace equals the
139
+ * sanctioned test workspace. Any uncertainty (no verified id yet, supplier
140
+ * throw, mismatch) resolves null: fail-closed to unregistered guest.
141
+ */
142
+ resolveFromSlackUserId(slackUserId) {
143
+ // A disabled source (missing marker) holds no principals — this is redundant
144
+ // with the empty map but makes the fail-closed contract explicit.
145
+ if (this.disabled)
146
+ return null;
147
+ let verified;
148
+ try {
149
+ verified = this.getVerifiedWorkspaceId();
150
+ }
151
+ catch {
152
+ // Fail-closed: an errored verification supplier keeps the cast inert.
153
+ return null;
154
+ }
155
+ if (!verified || verified !== this.workspaceId)
156
+ return null;
157
+ return this.bySlackId.get(slackUserId) ?? null;
158
+ }
159
+ }
160
+ /**
161
+ * ChainedUserLookup — production-registry-first principal resolution.
162
+ *
163
+ * Sources are consulted in order; the first non-null record wins. The wiring
164
+ * site puts the production registry FIRST, so a genuinely registered user can
165
+ * never be shadowed or role-escalated by a cast entry, and the cast is only
166
+ * ever a fallback for identities the production registry does not know.
167
+ * A throwing source is skipped (resolution must never break the message path).
168
+ */
169
+ export class ChainedUserLookup {
170
+ sources;
171
+ constructor(sources) {
172
+ this.sources = sources;
173
+ }
174
+ resolveFromSlackUserId(slackUserId) {
175
+ for (const source of this.sources) {
176
+ try {
177
+ const record = source.resolveFromSlackUserId(slackUserId);
178
+ if (record)
179
+ return record;
180
+ }
181
+ catch {
182
+ // A faulty source must never break principal resolution — fall through
183
+ // to the next source (ultimately the unregistered-guest default).
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+ }
189
+ //# sourceMappingURL=TestWorkspacePrincipalSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestWorkspacePrincipalSource.js","sourceRoot":"","sources":["../../src/permissions/TestWorkspacePrincipalSource.ts"],"names":[],"mappings":"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"}
@@ -16,4 +16,5 @@ export * from './MandateBackedGrantStore.js';
16
16
  export * from './SlackPrincipalResolver.js';
17
17
  export * from './SlackPermissionObserver.js';
18
18
  export * from './SlackUserRegistry.js';
19
+ export * from './TestWorkspacePrincipalSource.js';
19
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/permissions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/permissions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC;AACvC,cAAc,mCAAmC,CAAC"}
@@ -16,4 +16,5 @@ export * from './MandateBackedGrantStore.js';
16
16
  export * from './SlackPrincipalResolver.js';
17
17
  export * from './SlackPermissionObserver.js';
18
18
  export * from './SlackUserRegistry.js';
19
+ export * from './TestWorkspacePrincipalSource.js';
19
20
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/permissions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/permissions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC;AACvC,cAAc,mCAAmC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.726",
3
+ "version": "1.3.727",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-03T01:15:56.453Z",
5
- "instarVersion": "1.3.726",
4
+ "generatedAt": "2026-07-03T01:19:27.750Z",
5
+ "instarVersion": "1.3.727",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,143 @@
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
+ Fixes the roadmap-0.3 blocker found in the Slack permission-gate false-positive
34
+ review (`docs/audits/slack-permission-fp-review-2026-07.md`, row 29): after the
35
+ 2026-07-01 silent-loss registry rebuild, the Slack live-test scenario cast could no
36
+ longer resolve, so every Slack sender — including the workspace owner — resolved as an
37
+ unregistered guest. Root cause: the gate derives roles from the production user registry
38
+ (`users.json`), the rebuild removed the five Slack test-cast principals, and the
39
+ fixture-identity guard (correctly) refuses to let test identities back into the
40
+ production registry.
41
+
42
+ The fix is a SEPARATE, test-workspace-scoped principal source rather than a registry
43
+ edit:
44
+
45
+ - **New `TestWorkspacePrincipalSource` + `ChainedUserLookup`** (`src/permissions/`).
46
+ The gate's `SlackPrincipalResolver` now reads production-registry-FIRST, then the
47
+ cast as a fallback. A production-registered user can never be shadowed or
48
+ role-escalated by a cast entry.
49
+ - **Config-carried cast** — `permissionGate.testCast` in the Slack messaging config
50
+ (`{ testWorkspace, workspaceId, principals[] }`). It lives beside the workspace
51
+ tokens, so a `users.json` rebuild can never silently drop it again.
52
+ - **Code-enforced workspace scoping** — the cast resolves ONLY while the adapter's
53
+ VERIFIED connected team id (captured from Slack's own `auth.test` at start, exposed
54
+ via `SlackAdapter.getConnectedTeamId()`) EXACTLY equals `workspaceId`. Any other
55
+ workspace → the source is structurally invisible (production behavior byte-identical
56
+ to no cast). An unlisted uid in the test workspace → today's unregistered-guest default.
57
+ - **Partition invariant** — every cast `slackUserId` must match the SAME fixture-marker
58
+ matcher (`users/testIdentityMarkers.ts`) the production guard uses to refuse fixtures;
59
+ a non-fixture id is refused at load. One identity, two disjoint homes.
60
+ - **Fail-closed self-declaration** — the block must set `testWorkspace: true`; without
61
+ it the whole cast is ignored (zero principals, one loud log line, no production
62
+ effect). A cast can never be activated by accident.
63
+ - **Authority scope (KYP)** — the source feeds permission-gate role resolution ONLY. It
64
+ takes no state dir and has no write surface, so it cannot create user-registry
65
+ entries, cannot feed operator binding, and cannot affect message authorization /
66
+ sender validation.
67
+
68
+ The runbook (`docs/specs/SLACK-ORG-TEST-WORKSPACE-RUNBOOK.md`) is patched: the old
69
+ "seed the cast in users.json" step is replaced with the `testCast` config block plus a
70
+ re-provision checklist so a registry rebuild can't silently lose principal resolution
71
+ again. Dark by default — nothing activates without an explicit `testCast` block.
72
+
73
+ ## What to Tell Your User
74
+
75
+ <!-- audience: user, maturity: stable -->
76
+
77
+ If you switched on the stale-worktree auto-cleaner, it now genuinely runs: one
78
+ cleaning pass about 15 minutes after each server start, then daily as designed.
79
+ Previously a quirk meant frequent server restarts kept pushing its first run
80
+ into the future forever, so leftover work folders could quietly pile up into
81
+ tens of gigabytes even with the cleaner enabled. Its safety rules are untouched:
82
+ it still only removes folders whose work is fully merged and saved, still does
83
+ dry-run first, and it stays entirely off unless you enabled it.
84
+
85
+ - **Slack permission demo is more robust**: the test harness that proves the Slack
86
+ "who's allowed to do what" feature now keeps its demo cast in a safe place that a
87
+ routine cleanup of your real people-list can't wipe out. This is behind-the-scenes
88
+ test infrastructure — nothing changes in how I work for you day to day, and the
89
+ pretend demo roles are locked to the throwaway test workspace so they can never leak
90
+ into a real one.
91
+
92
+ ## Summary of New Capabilities
93
+
94
+ - The stale-worktree reaper actually fires on real deployments: a one-time
95
+ cleaning pass ~15 minutes after boot (tunable/disableable via
96
+ `monitoring.agentWorktreeReaper.initialPassDelayMs`), unchanged daily cadence
97
+ thereafter, and `initialPassPending` visibility on the existing report route.
98
+
99
+ | Capability | How to Use |
100
+ |-----------|-----------|
101
+ | Test-workspace-scoped Slack principal cast | Add a `testCast` block (with `testWorkspace: true`, `workspaceId`, `principals`) under the Slack `permissionGate` config — dark unless configured |
102
+
103
+ ## Evidence
104
+
105
+ - `tests/unit/agent-worktree-reaper.test.ts`: 47/47 green — 7 new tests (initial
106
+ pass fires exactly once at the configured delay and before the interval;
107
+ respects dry-run; disabled reaper sets no timers; `<= 0` restores
108
+ interval-only; `stop()` cancels the pending pass; 24h cadence unchanged after
109
+ the initial pass; snapshot reports `initialPassPending` honestly).
110
+ - Live incident data (2026-07-02, topic 30379): reaper enabled+armed with 25
111
+ reap-eligible worktrees and `lastPassAt: 0`; server uptime history shows no
112
+ 24h-continuous window for weeks.
113
+
114
+ Root cause is a live datapoint captured in `docs/audits/slack-permission-fp-review-2026-07.md`:
115
+ row 29 (2026-07-02 18:15:19, the only organic decision row) shows the workspace OWNER
116
+ seat's Slack UID resolving as `role: guest, registered: false` — the same UID that
117
+ resolved as `role: owner, registered: true` in every June scenario row, because the
118
+ 2026-07-01 rebuild removed the cast from `users.json`.
119
+
120
+ The fix is verified by `tests/unit/slack-test-workspace-principal-source.test.ts` (22
121
+ tests, all passing), which exercises BOTH sides of every boundary:
122
+
123
+ - matching workspace + listed uid → registered role; matching workspace + unlisted uid
124
+ → guest.
125
+ - non-matching workspace + listed uid → source invisible, and the resolved principal is
126
+ asserted EQUAL to the no-cast baseline (the scoping proof that roles can't leak into a
127
+ production workspace).
128
+ - missing `testWorkspace: true` → source disabled, zero seats, loud log line, and
129
+ byte-identical to no cast.
130
+ - non-fixture uid refused at load; cast cap enforced; production-registry-first
131
+ precedence; a throwing source skipped.
132
+ - authority-scope assertions: the source exposes only the read contract (no
133
+ write/registry/operator methods) and takes no state dir.
134
+
135
+ `tests/integration/slack-testcast-principal-pipeline.test.ts` (5 tests, all passing)
136
+ additionally drives the EXACT server.ts composition through the real inbound chokepoint
137
+ (`SlackAdapter._handleMessage`) and asserts the durable decision-ledger rows: the owner
138
+ seat resolves `owner, registered:true` through the live path (the row-29 regression),
139
+ the same seat is an unregistered guest when connected to any other workspace or before
140
+ `auth.test` verifies the connection, an unlisted uid stays a guest, and a production
141
+ record beats the cast for the same uid.
142
+
143
+ `npx tsc --noEmit` exits 0.