hub-proposal-policy.mjs
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠ breaking
1 day ago
| 1 | /** |
| 2 | * Proposal LLM + evaluation policy for self-hosted Hub (data/hub_proposal_policy.json). |
| 3 | * Precedence per field: explicit env (1/true or 0/false) overrides file; else file; else default false. |
| 4 | * |
| 5 | * Env keys: |
| 6 | * - HUB_PROPOSAL_EVALUATION_REQUIRED |
| 7 | * - KNOWTATION_HUB_PROPOSAL_REVIEW_HINTS |
| 8 | * - KNOWTATION_HUB_PROPOSAL_ENRICH |
| 9 | */ |
| 10 | |
| 11 | import fs from 'fs'; |
| 12 | import path from 'path'; |
| 13 | |
| 14 | const POLICY_FILE = 'hub_proposal_policy.json'; |
| 15 | |
| 16 | /** @param {unknown} v */ |
| 17 | function envTriState(v) { |
| 18 | if (v === '1' || v === 'true') return true; |
| 19 | if (v === '0' || v === 'false') return false; |
| 20 | return null; |
| 21 | } |
| 22 | |
| 23 | /** |
| 24 | * @param {string} dataDir |
| 25 | * @returns {{ proposal_evaluation_required?: boolean, review_hints_enabled?: boolean, enrich_enabled?: boolean }} |
| 26 | */ |
| 27 | export function readProposalPolicyFile(dataDir) { |
| 28 | const fp = path.join(dataDir, POLICY_FILE); |
| 29 | if (!fs.existsSync(fp)) return {}; |
| 30 | try { |
| 31 | const j = JSON.parse(fs.readFileSync(fp, 'utf8')); |
| 32 | if (!j || typeof j !== 'object') return {}; |
| 33 | const out = {}; |
| 34 | if (typeof j.proposal_evaluation_required === 'boolean') out.proposal_evaluation_required = j.proposal_evaluation_required; |
| 35 | if (typeof j.review_hints_enabled === 'boolean') out.review_hints_enabled = j.review_hints_enabled; |
| 36 | if (typeof j.enrich_enabled === 'boolean') out.enrich_enabled = j.enrich_enabled; |
| 37 | return out; |
| 38 | } catch { |
| 39 | return {}; |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * @param {string} dataDir |
| 45 | * @returns {boolean} |
| 46 | */ |
| 47 | export function getProposalEvaluationRequired(dataDir) { |
| 48 | const fromEnv = envTriState(process.env.HUB_PROPOSAL_EVALUATION_REQUIRED); |
| 49 | if (fromEnv !== null) return fromEnv; |
| 50 | const file = readProposalPolicyFile(dataDir); |
| 51 | return file.proposal_evaluation_required === true; |
| 52 | } |
| 53 | |
| 54 | /** |
| 55 | * @param {string} dataDir |
| 56 | * @returns {boolean} |
| 57 | */ |
| 58 | export function getProposalReviewHintsEnabled(dataDir) { |
| 59 | const fromEnv = envTriState(process.env.KNOWTATION_HUB_PROPOSAL_REVIEW_HINTS); |
| 60 | if (fromEnv !== null) return fromEnv; |
| 61 | return readProposalPolicyFile(dataDir).review_hints_enabled === true; |
| 62 | } |
| 63 | |
| 64 | /** |
| 65 | * @param {string} dataDir |
| 66 | * @returns {boolean} |
| 67 | */ |
| 68 | export function getProposalEnrichEnabled(dataDir) { |
| 69 | const fromEnv = envTriState(process.env.KNOWTATION_HUB_PROPOSAL_ENRICH); |
| 70 | if (fromEnv !== null) return fromEnv; |
| 71 | return readProposalPolicyFile(dataDir).enrich_enabled === true; |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * When true, that field is fixed by env and must not be edited via Settings file API. |
| 76 | * @returns {{ |
| 77 | * proposal_evaluation_required: boolean, |
| 78 | * review_hints_enabled: boolean, |
| 79 | * enrich_enabled: boolean, |
| 80 | * }} |
| 81 | */ |
| 82 | export function proposalPolicyEnvLocked() { |
| 83 | return { |
| 84 | proposal_evaluation_required: envTriState(process.env.HUB_PROPOSAL_EVALUATION_REQUIRED) !== null, |
| 85 | review_hints_enabled: envTriState(process.env.KNOWTATION_HUB_PROPOSAL_REVIEW_HINTS) !== null, |
| 86 | enrich_enabled: envTriState(process.env.KNOWTATION_HUB_PROPOSAL_ENRICH) !== null, |
| 87 | }; |
| 88 | } |
| 89 | |
| 90 | /** |
| 91 | * Merge partial policy into hub_proposal_policy.json (only keys present in partial; skips env-locked keys). |
| 92 | * @param {string} dataDir |
| 93 | * @param {{ |
| 94 | * proposal_evaluation_required?: boolean, |
| 95 | * review_hints_enabled?: boolean, |
| 96 | * enrich_enabled?: boolean, |
| 97 | * }} partial |
| 98 | */ |
| 99 | export function writeProposalPolicyMerge(dataDir, partial) { |
| 100 | const locks = proposalPolicyEnvLocked(); |
| 101 | const fp = path.join(dataDir, POLICY_FILE); |
| 102 | let existing = {}; |
| 103 | if (fs.existsSync(fp)) { |
| 104 | try { |
| 105 | const j = JSON.parse(fs.readFileSync(fp, 'utf8')); |
| 106 | if (j && typeof j === 'object') existing = j; |
| 107 | } catch { |
| 108 | existing = {}; |
| 109 | } |
| 110 | } |
| 111 | const next = { ...existing }; |
| 112 | if (partial.proposal_evaluation_required !== undefined && !locks.proposal_evaluation_required) { |
| 113 | next.proposal_evaluation_required = Boolean(partial.proposal_evaluation_required); |
| 114 | } |
| 115 | if (partial.review_hints_enabled !== undefined && !locks.review_hints_enabled) { |
| 116 | next.review_hints_enabled = Boolean(partial.review_hints_enabled); |
| 117 | } |
| 118 | if (partial.enrich_enabled !== undefined && !locks.enrich_enabled) { |
| 119 | next.enrich_enabled = Boolean(partial.enrich_enabled); |
| 120 | } |
| 121 | fs.mkdirSync(dataDir, { recursive: true }); |
| 122 | fs.writeFileSync(fp, JSON.stringify(next, null, 2), 'utf8'); |
| 123 | } |
File History
2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠
1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6
docs: accept Calendar Events v0 spec with Phase 0 security …
Human
2 days ago