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.
- package/dist/feedback-factory/receiver/defense.d.ts +82 -0
- package/dist/feedback-factory/receiver/defense.d.ts.map +1 -0
- package/dist/feedback-factory/receiver/defense.js +147 -0
- package/dist/feedback-factory/receiver/defense.js.map +1 -0
- package/dist/scheduler/MentorStageBForensics.d.ts.map +1 -1
- package/dist/scheduler/MentorStageBForensics.js +29 -2
- package/dist/scheduler/MentorStageBForensics.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.17.md +34 -0
- package/upgrades/1.3.18.md +27 -0
- package/upgrades/side-effects/feedback-factory-receiver-defense.md +33 -0
- package/upgrades/side-effects/mentor-forensic-dedup-stability.md +49 -0
|
@@ -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;
|
|
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
|
|
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}::${
|
|
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":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,aAAa,EACb,gBAAgB,GAIjB,MAAM,uCAAuC,CAAC;AAE/C,MAAM,oBAAoB,GAAG,EAAE,CAAC,CAAC,+BAA+B;AAChE,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB,
|
|
1
|
+
{"version":3,"file":"MentorStageBForensics.js","sourceRoot":"","sources":["../../src/scheduler/MentorStageBForensics.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-05-
|
|
5
|
-
"instarVersion": "1.3.
|
|
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.
|