instar 1.3.16 → 1.3.18

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,82 @@
1
+ /**
2
+ * defense.ts — framework-agnostic port of the feedback receiver's intake defenses.
3
+ *
4
+ * Ports the six defense-in-depth layers of `the-portal/pages/api/instar/feedback.ts`
5
+ * (rate-limit, agent fingerprint, honeypot, HMAC signature, input validation,
6
+ * dedup-key/type) out of the Next.js handler into pure/injectable functions, so the
7
+ * canonical front (whatever framework hosts it) reuses the exact same logic. The
8
+ * HTTP wiring + the app/framework placement is deliberately NOT here (it is the
9
+ * architecture decision in the spec's blocked list); this is the reusable core.
10
+ *
11
+ * Reference is TypeScript, so equivalence is by faithful transcription + exhaustive
12
+ * both-sides-of-boundary unit tests (not a cross-runtime parity harness — those
13
+ * apply to the Python processor ports). `now` is injected everywhere the reference
14
+ * used `Date.now()`, so tests are deterministic.
15
+ *
16
+ * Convergence finding folded in: `normalizeWebhookSecret` trims the secret at load
17
+ * so a trailing newline can't silently break the HMAC (the reference relied on a
18
+ * "use printf not echo" warning; this makes it structural).
19
+ */
20
+ export declare const VALID_TYPES: readonly ["bug", "feature", "improvement", "question", "hallucination", "other"];
21
+ export type FeedbackType = typeof VALID_TYPES[number];
22
+ export declare const SEMVER_RE: RegExp;
23
+ export declare const FEEDBACK_ID_RE: RegExp;
24
+ export declare const AGENT_NAME_RE: RegExp;
25
+ export declare const NODE_VERSION_RE: RegExp;
26
+ export declare const RATE_LIMITS: {
27
+ perHour: number;
28
+ perDay: number;
29
+ windowHourMs: number;
30
+ windowDayMs: number;
31
+ };
32
+ /** Trim the webhook secret so a trailing newline can't silently break the HMAC (convergence finding). */
33
+ export declare function normalizeWebhookSecret(secret: string | undefined): string | undefined;
34
+ export declare function isValidType(type: unknown): type is FeedbackType;
35
+ /** Port of extractSourceIp: first x-forwarded-for hop, else remoteAddress, else 'unknown'. */
36
+ export declare function extractSourceIp(headers: Record<string, string | string[] | undefined>, remoteAddress?: string): string;
37
+ /** Port of validateAgentFingerprint: UA must contain "instar/"; version header, if present, must be semver. */
38
+ export declare function validateAgentFingerprint(userAgent: string | undefined, headerVersion?: string): {
39
+ valid: boolean;
40
+ reason?: string;
41
+ };
42
+ /** Port of checkHoneypot: real agents never send `website`/`email`; presence ⇒ bot. */
43
+ export declare function checkHoneypot(body: Record<string, unknown>): boolean;
44
+ /**
45
+ * Port of verifySignature. HMAC-SHA256 over `${timestamp}.${JSON.stringify(body)}`,
46
+ * timing-safe compare, with a +5min / −1min replay window. `now` injected (ms).
47
+ */
48
+ export declare function verifySignature(args: {
49
+ signature?: string;
50
+ timestamp?: string;
51
+ body: unknown;
52
+ secret: string | undefined;
53
+ now: number;
54
+ }): boolean;
55
+ /** Port of the input-validation block (:242–269): title ≥3, description ≥10, valid type, semver instarVersion. */
56
+ export declare function validateFeedbackInput(body: Record<string, unknown>): {
57
+ valid: boolean;
58
+ error?: string;
59
+ };
60
+ /**
61
+ * Port of the in-memory sliding-window rate limiter (checkRateLimit + prune) as an
62
+ * injectable class. `now()` is injectable for deterministic tests; defaults to Date.now.
63
+ */
64
+ export declare class RateLimiter {
65
+ private readonly limits;
66
+ private readonly now;
67
+ private store;
68
+ private lastPrune;
69
+ private readonly pruneIntervalMs;
70
+ constructor(limits?: {
71
+ perHour: number;
72
+ perDay: number;
73
+ windowHourMs: number;
74
+ windowDayMs: number;
75
+ }, now?: () => number);
76
+ private prune;
77
+ check(ip: string): {
78
+ allowed: boolean;
79
+ retryAfterSec?: number;
80
+ };
81
+ }
82
+ //# sourceMappingURL=defense.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defense.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/receiver/defense.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,eAAO,MAAM,WAAW,kFAAmF,CAAC;AAC5G,MAAM,MAAM,YAAY,GAAG,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;AAEtD,eAAO,MAAM,SAAS,QAA8B,CAAC;AACrD,eAAO,MAAM,cAAc,QAAyB,CAAC;AACrD,eAAO,MAAM,aAAa,QAAiD,CAAC;AAC5E,eAAO,MAAM,eAAe,QAAsB,CAAC;AAEnD,eAAO,MAAM,WAAW;;;;;CAKvB,CAAC;AAEF,yGAAyG;AACzG,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAErF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,YAAY,CAE/D;AAED,8FAA8F;AAC9F,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAItH;AAED,+GAA+G;AAC/G,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CASnI;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAEpE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAkBV;AAED,kHAAkH;AAClH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAcvG;AAED;;;GAGG;AACH,qBAAa,WAAW;IAMpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IANtB,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;gBAG/B,MAAM;;;;;KAAc,EACpB,GAAG,GAAE,MAAM,MAAyB;IAGvD,OAAO,CAAC,KAAK;IAWb,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE;CAsBhE"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * defense.ts — framework-agnostic port of the feedback receiver's intake defenses.
3
+ *
4
+ * Ports the six defense-in-depth layers of `the-portal/pages/api/instar/feedback.ts`
5
+ * (rate-limit, agent fingerprint, honeypot, HMAC signature, input validation,
6
+ * dedup-key/type) out of the Next.js handler into pure/injectable functions, so the
7
+ * canonical front (whatever framework hosts it) reuses the exact same logic. The
8
+ * HTTP wiring + the app/framework placement is deliberately NOT here (it is the
9
+ * architecture decision in the spec's blocked list); this is the reusable core.
10
+ *
11
+ * Reference is TypeScript, so equivalence is by faithful transcription + exhaustive
12
+ * both-sides-of-boundary unit tests (not a cross-runtime parity harness — those
13
+ * apply to the Python processor ports). `now` is injected everywhere the reference
14
+ * used `Date.now()`, so tests are deterministic.
15
+ *
16
+ * Convergence finding folded in: `normalizeWebhookSecret` trims the secret at load
17
+ * so a trailing newline can't silently break the HMAC (the reference relied on a
18
+ * "use printf not echo" warning; this makes it structural).
19
+ */
20
+ import { createHmac, timingSafeEqual } from 'node:crypto';
21
+ export const VALID_TYPES = ['bug', 'feature', 'improvement', 'question', 'hallucination', 'other'];
22
+ export const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
23
+ export const FEEDBACK_ID_RE = /^fb-[a-z0-9-]{6,36}$/;
24
+ export const AGENT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9 _-]{0,98}[a-zA-Z0-9]$/;
25
+ export const NODE_VERSION_RE = /^v?\d+\.\d+\.\d+$/;
26
+ export const RATE_LIMITS = {
27
+ perHour: 10,
28
+ perDay: 50,
29
+ windowHourMs: 60 * 60 * 1000,
30
+ windowDayMs: 24 * 60 * 60 * 1000,
31
+ };
32
+ /** Trim the webhook secret so a trailing newline can't silently break the HMAC (convergence finding). */
33
+ export function normalizeWebhookSecret(secret) {
34
+ return secret == null ? secret : secret.trim();
35
+ }
36
+ export function isValidType(type) {
37
+ return typeof type === 'string' && VALID_TYPES.includes(type);
38
+ }
39
+ /** Port of extractSourceIp: first x-forwarded-for hop, else remoteAddress, else 'unknown'. */
40
+ export function extractSourceIp(headers, remoteAddress) {
41
+ const xff = headers['x-forwarded-for'];
42
+ const xffStr = Array.isArray(xff) ? xff[0] : xff;
43
+ return xffStr?.split(',')[0]?.trim() || remoteAddress || 'unknown';
44
+ }
45
+ /** Port of validateAgentFingerprint: UA must contain "instar/"; version header, if present, must be semver. */
46
+ export function validateAgentFingerprint(userAgent, headerVersion) {
47
+ const ua = userAgent || '';
48
+ if (!ua.toLowerCase().includes('instar/')) {
49
+ return { valid: false, reason: 'missing-ua' };
50
+ }
51
+ if (headerVersion && !SEMVER_RE.test(headerVersion)) {
52
+ return { valid: false, reason: 'invalid-version-header' };
53
+ }
54
+ return { valid: true };
55
+ }
56
+ /** Port of checkHoneypot: real agents never send `website`/`email`; presence ⇒ bot. */
57
+ export function checkHoneypot(body) {
58
+ return Boolean(body.website || body.email);
59
+ }
60
+ /**
61
+ * Port of verifySignature. HMAC-SHA256 over `${timestamp}.${JSON.stringify(body)}`,
62
+ * timing-safe compare, with a +5min / −1min replay window. `now` injected (ms).
63
+ */
64
+ export function verifySignature(args) {
65
+ const { signature, timestamp, body, secret, now } = args;
66
+ if (!signature || !timestamp || !secret)
67
+ return false;
68
+ const age = now - parseInt(timestamp, 10);
69
+ if (isNaN(age) || age > 300_000 || age < -60_000)
70
+ return false;
71
+ const payload = `${timestamp}.${JSON.stringify(body)}`;
72
+ const expected = createHmac('sha256', secret).update(payload).digest('hex');
73
+ try {
74
+ const sigBuf = Buffer.from(signature, 'hex');
75
+ const expBuf = Buffer.from(expected, 'hex');
76
+ if (sigBuf.length !== expBuf.length)
77
+ return false;
78
+ return timingSafeEqual(new Uint8Array(sigBuf), new Uint8Array(expBuf));
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ /** Port of the input-validation block (:242–269): title ≥3, description ≥10, valid type, semver instarVersion. */
85
+ export function validateFeedbackInput(body) {
86
+ if (!body.type || !isValidType(body.type)) {
87
+ return { valid: false, error: 'Invalid request format' };
88
+ }
89
+ if (!body.title || typeof body.title !== 'string' || body.title.trim().length < 3) {
90
+ return { valid: false, error: 'Invalid request format' };
91
+ }
92
+ if (!body.description || typeof body.description !== 'string' || body.description.trim().length < 10) {
93
+ return { valid: false, error: 'Invalid request format' };
94
+ }
95
+ if (body.instarVersion && !SEMVER_RE.test(body.instarVersion)) {
96
+ return { valid: false, error: 'Invalid request format' };
97
+ }
98
+ return { valid: true };
99
+ }
100
+ /**
101
+ * Port of the in-memory sliding-window rate limiter (checkRateLimit + prune) as an
102
+ * injectable class. `now()` is injectable for deterministic tests; defaults to Date.now.
103
+ */
104
+ export class RateLimiter {
105
+ limits;
106
+ now;
107
+ store = new Map();
108
+ lastPrune = 0;
109
+ pruneIntervalMs = 10 * 60 * 1000;
110
+ constructor(limits = RATE_LIMITS, now = () => Date.now()) {
111
+ this.limits = limits;
112
+ this.now = now;
113
+ }
114
+ prune() {
115
+ const now = this.now();
116
+ if (now - this.lastPrune < this.pruneIntervalMs)
117
+ return;
118
+ this.lastPrune = now;
119
+ const cutoff = now - this.limits.windowDayMs;
120
+ this.store.forEach((entry, key) => {
121
+ entry.timestamps = entry.timestamps.filter(t => t > cutoff);
122
+ if (entry.timestamps.length === 0)
123
+ this.store.delete(key);
124
+ });
125
+ }
126
+ check(ip) {
127
+ this.prune();
128
+ const now = this.now();
129
+ const entry = this.store.get(ip) || { timestamps: [] };
130
+ const hourAgo = now - this.limits.windowHourMs;
131
+ const dayAgo = now - this.limits.windowDayMs;
132
+ const hourCount = entry.timestamps.filter(t => t > hourAgo).length;
133
+ const dayCount = entry.timestamps.filter(t => t > dayAgo).length;
134
+ if (hourCount >= this.limits.perHour) {
135
+ const oldest = entry.timestamps.filter(t => t > hourAgo).sort()[0];
136
+ return { allowed: false, retryAfterSec: Math.ceil((oldest + this.limits.windowHourMs - now) / 1000) };
137
+ }
138
+ if (dayCount >= this.limits.perDay) {
139
+ const oldest = entry.timestamps.filter(t => t > dayAgo).sort()[0];
140
+ return { allowed: false, retryAfterSec: Math.ceil((oldest + this.limits.windowDayMs - now) / 1000) };
141
+ }
142
+ entry.timestamps.push(now);
143
+ this.store.set(ip, entry);
144
+ return { allowed: true };
145
+ }
146
+ }
147
+ //# sourceMappingURL=defense.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defense.js","sourceRoot":"","sources":["../../../src/feedback-factory/receiver/defense.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"MentorStageBForensics.d.ts","sourceRoot":"","sources":["../../src/scheduler/MentorStageBForensics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,uCAAuC,CAAC;AAU/C;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAiB9E;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CAgCvF;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/C;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAS/F"}
1
+ {"version":3,"file":"MentorStageBForensics.d.ts","sourceRoot":"","sources":["../../src/scheduler/MentorStageBForensics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,uCAAuC,CAAC;AA6B/C;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA0B9E;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CAgCvF;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/C;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAS/F"}
@@ -14,10 +14,28 @@
14
14
  import { ISSUE_BUCKETS, ISSUE_SEVERITIES, } from '../monitoring/FrameworkIssueLedger.js';
15
15
  const MAX_FINDINGS_PER_RUN = 10; // bound a single tick's output
16
16
  const MAX_TITLE = 200;
17
- /** A normalized slug for a conservative dedupKey when the model omits one. */
17
+ /** A normalized slug for a dedupKey from a model-supplied stable id (keeps it as-is, kebabed). */
18
18
  function slug(s) {
19
19
  return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'untitled';
20
20
  }
21
+ /**
22
+ * Derive a conservative dedupKey from a title when the model omits one. Strips
23
+ * VOLATILE tokens that would split one issue across runs — standalone numbers,
24
+ * timeout/percent/version literals, and hex-ish ids — keeping only the stable
25
+ * symptom words. (§13.3: titles are operator-dependent; this is the last-resort
26
+ * fallback — the prompt asks the model for a stable id directly.)
27
+ */
28
+ function deriveStableSlug(title) {
29
+ const stable = title
30
+ .toLowerCase()
31
+ .replace(/\bv?\d+(?:\.\d+)+\b/g, ' ') // version strings (v1.3.14, 1.2.3)
32
+ .replace(/\b\d+\s*(?:ms|s|m|h|%|k|kb|mb|tokens?|turns?)\b/g, ' ') // 8s, 42%, 15k, 1 turn
33
+ .replace(/\b[0-9a-f]{6,}\b/g, ' ') // hex ids / shas
34
+ .replace(/\b\d+\b/g, ' ') // any remaining standalone numbers
35
+ .replace(/\s+/g, ' ')
36
+ .trim();
37
+ return slug(stable || title);
38
+ }
21
39
  /**
22
40
  * Build the Stage-B forensic prompt. The model is asked to classify ONLY what
23
41
  * the signals actually evidence into the three buckets, and to output a strict
@@ -36,6 +54,15 @@ export function buildForensicPrompt(framework, signals) {
36
54
  ``,
37
55
  `Output ONLY a JSON array (no markdown, no prose). Each element:`,
38
56
  ` {"bucket": "...", "title": "<short>", "severity": "low|medium|high", "dedupKey": "<stable-id>"}`,
57
+ ``,
58
+ `CRITICAL — the dedupKey must be STABLE across runs. It is a short lowercase-kebab identifier`,
59
+ `for the ROOT SYMPTOM, NOT a rephrasing of your title. Use the SAME id you would use if you saw`,
60
+ `this exact issue again next time. Base it on the symptom class + the component, e.g.`,
61
+ `"feedback-webhook-429", "inputguard-review-timeout", "codex-single-turn-exit". Do NOT put`,
62
+ `version numbers, timestamps, session ids, counts, or wording variations in the dedupKey —`,
63
+ `those drift and would split one issue into many. Two reports of the same root problem MUST`,
64
+ `produce the same dedupKey.`,
65
+ ``,
39
66
  `If the signals evidence no concrete issue, output []. Never invent issues.`,
40
67
  ``,
41
68
  `--- Signals ---`,
@@ -84,7 +111,7 @@ export function parseForensicFindings(raw, framework) {
84
111
  : 'medium';
85
112
  const dedupKey = typeof o.dedupKey === 'string' && o.dedupKey.trim()
86
113
  ? `${framework}::${slug(o.dedupKey)}`
87
- : `${framework}::${slug(title)}`;
114
+ : `${framework}::${deriveStableSlug(title)}`;
88
115
  out.push({ bucket: bucket, title, severity, dedupKey, signature: title });
89
116
  }
90
117
  return out;
@@ -1 +1 @@
1
- {"version":3,"file":"MentorStageBForensics.js","sourceRoot":"","sources":["../../src/scheduler/MentorStageBForensics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MentorStageBForensics.js","sourceRoot":"","sources":["../../src/scheduler/MentorStageBForensics.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.16",
3
+ "version": "1.3.18",
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-05-27T06:41:15.968Z",
5
- "instarVersion": "1.3.16",
4
+ "generatedAt": "2026-05-27T07:09:40.342Z",
5
+ "instarVersion": "1.3.18",
6
6
  "entryCount": 192,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,34 @@
1
+ # Instar Upgrade Guide — NEXT
2
+
3
+ <!-- bump: patch -->
4
+
5
+ ## What Changed
6
+
7
+ **Framework-Onboarding Mentor System — forensic dedup stability.** A live validation run surfaced
8
+ that the same root issue could be logged 2–3 times across ticks under different IDs, because when the
9
+ model omitted a stable identifier the system derived one from the (drifting) wording — the exact
10
+ title-dependence the design doc warns against. Fixed two ways: the forensic prompt now demands a
11
+ stable, symptom-based identifier (no version numbers, counts, ids, or wording variants), and the
12
+ fallback derivation strips volatile tokens before building a key. Live re-run confirmed cross-tick
13
+ merging now works (16 → 12 issues over the same 3 iterations). Still dormant.
14
+
15
+ ## What to Tell Your User
16
+
17
+ - The mentor's notebook now keeps one entry per real problem even when the wording wobbles between
18
+ inspections — no more slow drizzle of near-duplicate entries.
19
+
20
+ ## Summary of New Capabilities
21
+
22
+ | Capability | How to Use |
23
+ |-----------|-----------|
24
+ | Stable forensic dedup | Automatic — the same root issue collapses to one ledger entry across ticks |
25
+
26
+ ## Evidence
27
+
28
+ Found by running the real loop through multiple iterations against real data (the iterate-and-harden
29
+ loop working as designed). Fix proven by: 3 new unit tests asserting the model-supplied stable key is
30
+ preferred, that two phrasings of one issue differing only in volatile tokens (`8s` vs `8000ms`)
31
+ collapse to the **same** dedupKey, and that version/percent/hex tokens are stripped from the derived
32
+ fallback; plus a live re-run on Echo's real server.log + Codey's real rollouts showing cross-tick
33
+ merge dropped duplicates from 16 → 12 over the same 3 iterations. 13 unit tests total; affected
34
+ push-config suite green vs canonical main.
@@ -0,0 +1,27 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: patch -->
4
+
5
+ ## What Changed
6
+
7
+ Sixth increment of the **Feedback Factory Migration** (Dawn → Echo; spec `docs/specs/feedback-factory-migration.md`, approved). Ports the **receiver intake defenses** — the six security layers of the feedback front door — out of the reference Next.js handler (`the-portal/pages/api/instar/feedback.ts`) into framework-agnostic TypeScript at `src/feedback-factory/receiver/defense.ts`.
8
+
9
+ The layers: a per-IP sliding-window rate limiter (10/hour, 50/day), the agent fingerprint check (User-Agent must say `instar/`, version header must be semver), the honeypot (real agents never send `website`/`email`), HMAC signature verification with a 5-minute replay window, input validation (title/description length, valid type, semver), and the type/dedup helpers. Pure/injectable functions plus one self-contained `RateLimiter` class. **Not wired into any route yet** — the HTTP/app placement is a separate decision; this is the reusable core.
10
+
11
+ A convergence finding is folded in: the webhook secret is trimmed at load, so a trailing newline can't silently break the signature check (replacing a "be careful how you set it" warning with structure).
12
+
13
+ ## What to Tell Your User
14
+
15
+ - The security guards on the feedback front door — rate limiting, bot traps, signature checks, input validation — are now ported into our own codebase, ready to attach to the new front door when we stand it up.
16
+ - One sharp-edge removed for good: a stray newline in the signing secret used to be able to silently break verification; now it's trimmed automatically.
17
+
18
+ ## Summary of New Capabilities
19
+
20
+ | Capability | How to Use |
21
+ |-----------|-----------|
22
+ | Receiver intake defenses (TS port) | Internal module `src/feedback-factory/receiver/defense.ts` — not yet wired |
23
+
24
+ ## Evidence
25
+
26
+ - The reference is TypeScript, so equivalence is by faithful transcription plus exhaustive both-sides-of-boundary tests (16 unit tests): rate limit at 10 vs 11 and after a window slide and per-IP; fingerprint with/without `instar/` and valid/invalid version; honeypot present/absent; HMAC valid/wrong/missing and both replay-window edges (+5min/−1min); validation at title 2 vs 3 and description 9 vs 10. The clock is injected everywhere, so the time-based windows are tested deterministically.
27
+ - The HMAC payload formula, replay thresholds, and validation bounds are copied verbatim from the reference the field agents already sign against.
@@ -0,0 +1,33 @@
1
+ # Side-Effects Review — feedback receiver intake defenses (Phase 1, increment 6)
2
+
3
+ **Slug:** `feedback-factory-receiver-defense`
4
+ **Date:** `2026-05-27`
5
+ **Author:** Echo (autonomous)
6
+ **Spec:** `docs/specs/feedback-factory-migration.md` (converged v2, approved by Justin 2026-05-26)
7
+ **Scope:** The six intake defense layers of the receiver, ported as pure/injectable functions. NOT the Next.js/Vercel HTTP wiring (the app-placement architecture decision — spec's blocked list).
8
+
9
+ ## Summary of the change
10
+
11
+ Ports the defense-in-depth logic of `the-portal/pages/api/instar/feedback.ts` out of the Next.js handler into `src/feedback-factory/receiver/defense.ts` as framework-agnostic functions: `RateLimiter` (in-memory sliding window, 10/hr + 50/day, injectable clock), `validateAgentFingerprint` (UA must contain `instar/`; version-header semver), `checkHoneypot` (website/email ⇒ bot), `verifySignature` (HMAC-SHA256 over `${timestamp}.${JSON.stringify(body)}`, timing-safe, +5min/−1min replay window, injectable `now`), `validateFeedbackInput` (title ≥3, description ≥10, valid type, semver version), plus the regex constants + `isValidType` + `extractSourceIp`. **Not wired into any route yet** — no behavioral change.
12
+
13
+ Convergence finding folded in: `normalizeWebhookSecret` trims the secret at load so a trailing newline can't silently break the HMAC (replaces the reference's "use printf not echo" warning with structure).
14
+
15
+ ## Equivalence verification
16
+
17
+ The reference is TypeScript (not Python), so equivalence is by **faithful transcription + exhaustive both-sides-of-boundary unit tests**, not a cross-runtime parity harness (those apply only to the Python processor ports). `now` is injected everywhere the reference used `Date.now()`, so the rate-limit window + replay window are tested deterministically (10 allowed → 11th blocked; window slide; per-IP isolation; replay-window edges at +5min/−1min; HMAC valid/wrong/missing). The HMAC formula, replay thresholds, and validation bounds are copied verbatim from the reference.
18
+
19
+ ## Seven-dimension review
20
+
21
+ 1. **Over/under-reach** — Pure/injectable functions + one stateful class (`RateLimiter`) that owns its own Map (no module-level global, unlike the reference — a deliberate improvement for testability + multi-instance safety). Not wired into any route.
22
+ 2. **Level-of-abstraction fit** — `src/feedback-factory/receiver/` — the receiver layer, separate from the processor. The HTTP/framework wiring is intentionally excluded (blocked architecture decision); this is the reusable core that any host calls.
23
+ 3. **Signal vs Authority** — N/A; these are validators/guards returning verdicts. The HTTP handler (later) decides the response code.
24
+ 4. **Interactions** — None. New isolated module; nothing imports it yet.
25
+ 5. **Rollback cost** — Trivial: delete the module + tests.
26
+ 6. **Migration parity** — N/A. New internal library code; no agent-installed file touched.
27
+ 7. **Failure modes** — (a) HMAC payload-construction divergence from what agents sign → the formula is transcribed verbatim (`${timestamp}.${JSON.stringify(body)}`); the sender side already uses this. (b) Trailing-newline secret → structurally trimmed (`normalizeWebhookSecret`) + tested. (c) Rate-limit clock non-determinism → injectable `now`, tested via window-slide. (d) Replay-window off-by-one → both edges (+5min/−1min) tested.
28
+
29
+ ## Tests
30
+
31
+ - Tier-1 unit (CI): `tests/unit/feedback-factory/defense.test.ts` — 16 tests across all six layers + secret normalization + boundaries.
32
+ - No cross-runtime parity harness (reference is TS; equivalence by transcription + boundary tests — noted above).
33
+ - No integration/E2E this increment: the HTTP wiring/app placement is the blocked architecture decision; these functions attach to a route when that's decided. Reasoned, documented.
@@ -0,0 +1,49 @@
1
+ # Side-Effects Review — Forensic dedupKey stability (§19.4 follow-on)
2
+
3
+ **Spec:** `docs/specs/FRAMEWORK-ONBOARDING-MENTOR-SPEC.md` (converged 5 iters, approved by Justin)
4
+ **Change:** Hardens Stage-B forensic dedupKey stability so the same root issue produces the same
5
+ key across ticks (§13.3: "title is too operator-dependent"). Found by **live validation**: running
6
+ the real forensics through 3 iterations on real data logged the same issue 2–3× under different
7
+ keys, because the LLM-omitted-key fallback derived from the (drifting) title. Fix: (1) the prompt
8
+ now demands a stable, symptom-based lowercase-kebab `dedupKey` with explicit "no versions / numbers /
9
+ ids / wording variants" guidance; (2) the fallback derivation strips volatile tokens
10
+ (numbers, version/percent/unit literals, hex ids) before slugging.
11
+ **Files:** `src/scheduler/MentorStageBForensics.ts`, `tests/unit/MentorStageBForensics.test.ts`,
12
+ `upgrades/NEXT.md`.
13
+
14
+ ## Principle check (Phase 1)
15
+
16
+ Decision point? No. This sharpens an identifier (dedup quality) on a signal-only path. No gating.
17
+
18
+ ## The seven questions
19
+
20
+ 1. **Over-block.** Over-aggressive token stripping could in theory merge two genuinely-distinct
21
+ issues whose titles differ only in numbers. Mitigated: the strip only applies to the *fallback*
22
+ (when the LLM omits a key); the LLM-supplied symptom key is preferred. And §13.3's design makes
23
+ false merges the conservative-acceptable direction only on `dedupKey` while `signature` (the full
24
+ title) preserves the distinction for probable-dup review.
25
+ 2. **Under-block.** LLM forensics are inherently variable run-to-run, so cross-tick merging is
26
+ improved (live-measured 16→12 issues over the same 3 iterations) but not perfect; the residual
27
+ is handled by the `signature`-based probable-dup review (§13.3), unchanged.
28
+ 3. **Level-of-abstraction fit.** Pure-module change (prompt + key derivation); no I/O. Correct layer.
29
+ 4. **Signal vs authority.** Unchanged — signal-only.
30
+ 5. **Interactions.** Only changes the `dedupKey` a finding carries; the ledger's existing
31
+ conservative auto-merge on `(framework, dedupKey)` does the rest (no ledger change).
32
+ 6. **External surfaces.** None — internal forensic logic.
33
+ 7. **Rollback cost.** Trivial — revert the prompt + derivation change.
34
+
35
+ ## Phase 5 — second-pass
36
+
37
+ Not required — a dedup-quality refinement on a pure, tested module; no decision/spawn/session surface.
38
+
39
+ ## Validation
40
+
41
+ Live re-run on Echo's real server.log + Codey's real rollouts: before, 3 iterations produced 16
42
+ issues (near-zero cross-tick merge); after, the same 3 iterations produced 12 (iterations 2–3 each
43
+ merged ~half their findings into existing issues) — the cross-tick merge now demonstrably works.
44
+
45
+ ## Testing
46
+
47
+ 13 unit tests (+3): prefers the model-supplied stable key; fallback strips volatile tokens so
48
+ phrasing/number variants of one issue collapse to the same key; version/percent/hex tokens stripped.
49
+ Affected push-config suite green vs canonical main.