hub-proposal-create-augment.mjs
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