hub-proposal-create-augment.mjs
86 lines 3.4 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago
1 /**
2 * Merge org evaluation policy + deterministic review triggers into a proposal create payload.
3 * Used by self-hosted Hub and hosted gateway (POST /api/v1/proposals body).
4 *
5 * HOSTED-WRITE-EVAL E1: after triggers, Scooling personal self-apply class gets
6 * evaluation_status=passed (never self-pass elevated / auto-flagged).
7 *
8 * SEC-KN-2 / Pass 2 P2: client-supplied evaluation_status / evaluated_by / evaluated_at
9 * are stripped from every create body before policy/triggers/E1. Only server-side
10 * evaluation (policy pending, E1 self-pass, or the evaluate endpoint) may set them.
11 */
12
13 import { getProposalEvaluationRequired } from './hub-proposal-policy.mjs';
14 import { loadReviewTriggers, applyReviewTriggers } from './hub-proposal-review-triggers.mjs';
15 import { applyPersonalSelfApplyEvaluationE1 } from './hub-proposal-personal-self-apply.mjs';
16
17 /** Client-forgeable evaluation audit fields — never trusted on create. */
18 export const CLIENT_EVALUATION_CREATE_FIELDS = Object.freeze([
19 'evaluation_status',
20 'evaluated_by',
21 'evaluated_at',
22 ]);
23
24 /**
25 * Remove client-supplied evaluation fields from a proposal-create body.
26 * Mutates a shallow copy; does not mutate the input object.
27 *
28 * @param {Record<string, unknown>} body
29 * @returns {Record<string, unknown>}
30 */
31 export function stripClientEvaluationFields(body) {
32 if (!body || typeof body !== 'object' || Buffer.isBuffer(body)) return body;
33 const next = { ...body };
34 for (const key of CLIENT_EVALUATION_CREATE_FIELDS) {
35 delete next[key];
36 }
37 return next;
38 }
39
40 /**
41 * @param {Record<string, unknown>} body - parsed JSON body (mutated copy returned)
42 * @param {string} dataDir
43 * @param {{
44 * evaluationRequired?: boolean,
45 * evaluatedBy?: string,
46 * evaluatedAt?: string,
47 * sessionBound?: boolean,
48 * authorActorId?: string|null,
49 * }} [policyOptions] - when `evaluationRequired` is boolean, skip file/env read
50 * @returns {Record<string, unknown>}
51 */
52 export function augmentProposalCreateRequestBody(body, dataDir, policyOptions = {}) {
53 if (!body || typeof body !== 'object' || Buffer.isBuffer(body)) return body;
54 const policyPending =
55 typeof policyOptions.evaluationRequired === 'boolean'
56 ? policyOptions.evaluationRequired
57 : getProposalEvaluationRequired(dataDir);
58 const triggers = loadReviewTriggers(dataDir);
59 const labels = Array.isArray(body.labels) ? body.labels : [];
60 const applied = applyReviewTriggers(triggers, {
61 path: String(body.path ?? ''),
62 body: String(body.body ?? ''),
63 intent: String(body.intent ?? ''),
64 labels,
65 });
66 // SEC-KN-2: discard client evaluation fields before any server assignment.
67 let next = stripClientEvaluationFields(body);
68 const needPending = policyPending || applied.forcePending;
69 if (needPending) {
70 next.evaluation_status = 'pending';
71 }
72 if (applied.review_queue) next.review_queue = applied.review_queue;
73 if (applied.review_severity) next.review_severity = applied.review_severity;
74 if (applied.auto_flag_reasons.length) {
75 next.auto_flag_reasons_json = JSON.stringify(applied.auto_flag_reasons);
76 next.auto_flag_reasons = applied.auto_flag_reasons;
77 }
78 // E1 AFTER trigger merge — elevated/auto-flag still fail P6 and stay pending.
79 next = applyPersonalSelfApplyEvaluationE1(next, {
80 evaluatedBy: policyOptions.evaluatedBy,
81 evaluatedAt: policyOptions.evaluatedAt,
82 sessionBound: policyOptions.sessionBound,
83 authorActorId: policyOptions.authorActorId ?? policyOptions.evaluatedBy,
84 });
85 return next;
86 }
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago