MCPcopy Index your code
hub / github.com/garrytan/gstack / validateDecide

Function validateDecide

lib/gstack-decision.ts:103–164  ·  view source on GitHub ↗
(input: Partial<DecisionEvent>)

Source from the content-addressed store, hash-verified

101 * - a HIGH-tier secret (redact engine) in any free-text field.
102 */
103export function validateDecide(input: Partial<DecisionEvent>): ValidateResult {
104 if (!input.decision || typeof input.decision !== "string" || !input.decision.trim()) {
105 return { ok: false, error: "decision text is required" };
106 }
107 const scope = input.scope ?? "repo";
108 if (!DECISION_SCOPES.includes(scope)) {
109 return { ok: false, error: `invalid scope "${scope}"; must be ${DECISION_SCOPES.join("|")}` };
110 }
111 const source = input.source ?? "agent";
112 if (!DECISION_SOURCES.includes(source)) {
113 return { ok: false, error: `invalid source "${source}"; must be ${DECISION_SOURCES.join("|")}` };
114 }
115 if (input.confidence !== undefined) {
116 const c = Number(input.confidence);
117 if (!Number.isInteger(c) || c < 1 || c > 10) {
118 return { ok: false, error: "confidence must be integer 1-10" };
119 }
120 }
121
122 // Scan ALL stored free-text — incl. branch/issue, which are surfaced (and emitted raw
123 // via --json), so they must not carry secrets or injection either (Codex finding).
124 const freeText = [input.decision, input.rationale, input.alternatives_considered, input.branch, input.issue]
125 .filter((s): s is string => typeof s === "string")
126 .join("\n");
127
128 if (hasInjection(freeText)) {
129 return { ok: false, error: "decision contains instruction-like content (injection), rejected" };
130 }
131 const redacted = scan(freeText);
132 if (redacted.counts.HIGH > 0) {
133 return {
134 ok: false,
135 error: `decision contains a HIGH-tier secret (${redacted.counts.HIGH} finding(s)); rotate + remove it, do not log secrets`,
136 };
137 }
138 // MEDIUM = PII / credential-shaped content. The taxonomy says "confirm via
139 // AskUserQuestion", but this store is NON-INTERACTIVE and syncs cross-machine,
140 // so there is no confirm path — fail closed rather than silently persist + sync a
141 // secret that later resurfaces into agent context.
142 if (redacted.counts.MEDIUM > 0) {
143 return {
144 ok: false,
145 error: `decision contains MEDIUM-tier sensitive content (${redacted.counts.MEDIUM} finding(s): PII or credential-shaped). This store is non-interactive and syncs across machines, so it fails closed — remove or rephrase the value before logging.`,
146 };
147 }
148
149 const event: DecisionEvent = {
150 id: input.id || randomUUID(),
151 kind: "decide",
152 decision: input.decision.trim(),
153 rationale: input.rationale,
154 alternatives_considered: input.alternatives_considered,
155 scope,
156 branch: input.branch || undefined,
157 issue: input.issue || undefined,
158 date: input.date || new Date().toISOString(),
159 session: input.session,
160 source,

Callers 1

Calls 2

hasInjectionFunction · 0.90
scanFunction · 0.90

Tested by

no test coverage detected