thumbgate 1.35.0 → 1.37.1

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.
Files changed (196) hide show
  1. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  2. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  3. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  4. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  5. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  6. package/.claude-plugin/plugin.json +1 -1
  7. package/.well-known/llms.txt +1 -0
  8. package/.well-known/mcp/server-card.json +1 -1
  9. package/CONTRIBUTING.md +95 -0
  10. package/README.md +195 -632
  11. package/THIRD_PARTY_NOTICES.md +89 -0
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/forge/forge.yaml +3 -3
  14. package/adapters/future-agi/.mcp.json +8 -0
  15. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  16. package/adapters/future-agi/config.toml +3 -0
  17. package/adapters/future-agi/future-agi-bridge.js +9 -0
  18. package/adapters/future-agi/opencode.json +8 -0
  19. package/adapters/herdr/herdr-plugin.toml +18 -0
  20. package/adapters/mcp/server-stdio.js +238 -25
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/adapters/workos/WORKOS.md +52 -0
  23. package/bin/cli.js +336 -3
  24. package/bin/futureagi-bridge +9 -0
  25. package/config/gate-templates.json +581 -4
  26. package/config/gates/actor-critic-audit.json +34 -0
  27. package/config/gates/default.json +9 -3
  28. package/config/gates/five-walls-governance.json +34 -0
  29. package/config/gates/future-agi-guardrails.json +34 -0
  30. package/config/gates/radware-threat-defense-2026.json +61 -0
  31. package/config/gates/simatree-data-governance.json +33 -0
  32. package/config/mcp-allowlists.json +4 -0
  33. package/config/merge-quality-checks.json +6 -0
  34. package/config/model-candidates.json +312 -29
  35. package/config/model-tiers.json +18 -0
  36. package/config/post-deploy-marketing-pages.json +10 -0
  37. package/config/progressive/01-wire-only.json +11 -0
  38. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  39. package/config/progressive/03-one-lesson.json +10 -0
  40. package/config/progressive/04-warn-fires.json +11 -0
  41. package/config/progressive/05-strict-optional.json +11 -0
  42. package/config/progressive/README.md +15 -0
  43. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  44. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  45. package/conformance/provider-attestation/vectors.json +320 -0
  46. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  47. package/openapi/openapi.yaml +15 -0
  48. package/package.json +390 -148
  49. package/public/about.html +2 -2
  50. package/public/ai-malpractice-prevention.html +7 -7
  51. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  52. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  53. package/public/blog/git-at-agent-scale.html +374 -0
  54. package/public/blog/no-llm-in-the-gate.html +133 -0
  55. package/public/blog.html +80 -0
  56. package/public/case-studies.html +16 -1
  57. package/public/compare.html +28 -0
  58. package/public/diagnostic.html +216 -7
  59. package/public/docs/connectors.html +39 -0
  60. package/public/federal.html +2 -2
  61. package/public/founders.html +639 -0
  62. package/public/index.html +87 -9
  63. package/public/install.html +8 -8
  64. package/public/learn.html +39 -0
  65. package/public/numbers.html +2 -2
  66. package/public/peter.html +310 -0
  67. package/public/platform-partners.html +119 -0
  68. package/public/pricing.html +24 -3
  69. package/public/privacy.html +117 -0
  70. package/public/pro.html +17 -0
  71. package/public/support.html +62 -0
  72. package/public/terms.html +130 -0
  73. package/public/third-party-notices.html +95 -0
  74. package/public/yt.html +351 -0
  75. package/scripts/action-receipts.js +133 -3
  76. package/scripts/adaptive-governance-arena.js +349 -0
  77. package/scripts/admin-override.js +205 -0
  78. package/scripts/agent-action-inventory.js +869 -0
  79. package/scripts/agent-audit-trace.js +42 -2
  80. package/scripts/agent-egress-policy.js +1117 -0
  81. package/scripts/agent-memory-lifecycle.js +141 -2
  82. package/scripts/agent-operations-planner.js +441 -1
  83. package/scripts/agent-readiness.js +68 -0
  84. package/scripts/agent-security-central.js +647 -0
  85. package/scripts/allowlist-bridge-honesty.js +417 -0
  86. package/scripts/async-job-runner.js +102 -11
  87. package/scripts/audit-trail.js +212 -0
  88. package/scripts/billing.js +1 -1
  89. package/scripts/broker-execution-receipts.js +719 -0
  90. package/scripts/budget-aware-gates-proof.js +423 -0
  91. package/scripts/claude-feedback-sync.js +29 -3
  92. package/scripts/claw-harness-production.js +237 -0
  93. package/scripts/cli-schema.js +163 -1
  94. package/scripts/codex-runbook-flywheel.js +318 -0
  95. package/scripts/context-footprint.js +186 -0
  96. package/scripts/contextfs.js +143 -61
  97. package/scripts/dashboard.js +251 -32
  98. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  99. package/scripts/docker-sandbox-planner.js +18 -0
  100. package/scripts/double-blind-eval-protocol.js +252 -0
  101. package/scripts/edotenv-rl-gateway.js +259 -0
  102. package/scripts/ensure-production-search-corpus.js +162 -0
  103. package/scripts/eval-holdout.js +311 -0
  104. package/scripts/feedback-aggregate.js +21 -2
  105. package/scripts/feedback-loop.js +87 -5
  106. package/scripts/feedback-quality.js +9 -0
  107. package/scripts/file-ledger-lock.js +4 -1
  108. package/scripts/financial-control-plane.js +41 -1
  109. package/scripts/find-dormant-requires.js +118 -0
  110. package/scripts/fs-utils.js +84 -8
  111. package/scripts/gates-engine.js +810 -63
  112. package/scripts/generate-case-study-outreach.js +24 -15
  113. package/scripts/git-at-scale.js +628 -0
  114. package/scripts/governance-conflict-audit.js +1650 -0
  115. package/scripts/governance-difficulty-curriculum.js +328 -0
  116. package/scripts/graphrag-retrieval.js +275 -0
  117. package/scripts/gurobi-optimizer.js +324 -0
  118. package/scripts/gurobi_optimizer.py +485 -0
  119. package/scripts/harness-selector.js +82 -1
  120. package/scripts/hidden-entry-points.js +284 -0
  121. package/scripts/human-escalation.js +199 -1
  122. package/scripts/hybrid-feedback-context.js +152 -19
  123. package/scripts/intent-governed-execution.js +602 -0
  124. package/scripts/intervention-policy.js +123 -20
  125. package/scripts/jit-harness-compose.js +628 -0
  126. package/scripts/jsonl-watcher.js +10 -0
  127. package/scripts/lesson-embedding-index.js +95 -12
  128. package/scripts/lesson-retrieval.js +105 -19
  129. package/scripts/local-model-profile.js +19 -2
  130. package/scripts/mailer/resend-mailer.js +1 -1
  131. package/scripts/matryoshka-embedding.js +235 -0
  132. package/scripts/mcp-oauth.js +42 -4
  133. package/scripts/mcp-session-handles.js +1016 -0
  134. package/scripts/mcp-wiring-doctor.js +314 -0
  135. package/scripts/memory-firewall.js +115 -2
  136. package/scripts/memory-scope-readiness.js +299 -0
  137. package/scripts/memory-vs-rag-route.js +161 -0
  138. package/scripts/model-tier-router.js +148 -21
  139. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  140. package/scripts/openui-catalog-compose-honesty.js +593 -0
  141. package/scripts/operational-integrity.js +19 -1
  142. package/scripts/override-audit.js +213 -0
  143. package/scripts/package-manager-honesty-doctor.js +458 -0
  144. package/scripts/pr-manager.js +63 -1
  145. package/scripts/prove-herdr-adapter.js +52 -0
  146. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  147. package/scripts/prove-workos.js +73 -0
  148. package/scripts/provider-attestation-conformance.js +192 -0
  149. package/scripts/provider-receipt-contract.js +136 -0
  150. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  151. package/scripts/radware-threat-defense.js +280 -0
  152. package/scripts/rag-embedding-identity.js +221 -0
  153. package/scripts/rag-precision-guardrails.js +112 -2
  154. package/scripts/remote-feedback-capture.js +159 -0
  155. package/scripts/research-agent-harness.js +256 -0
  156. package/scripts/rsi-safety-hillclimb.js +200 -0
  157. package/scripts/rule-sprawl.js +188 -0
  158. package/scripts/schedule-manager.js +147 -0
  159. package/scripts/self-heal.js +8 -0
  160. package/scripts/session-lease.js +415 -0
  161. package/scripts/simatree-data-governance.js +347 -0
  162. package/scripts/slo-alert-engine.js +172 -7
  163. package/scripts/solver-parity.js +539 -0
  164. package/scripts/stealth-memory-injection-gate.js +333 -0
  165. package/scripts/switchyard-router.js +366 -0
  166. package/scripts/telemetry-analytics.js +84 -27
  167. package/scripts/temporal-decay-weighting.js +138 -0
  168. package/scripts/test-all.js +165 -0
  169. package/scripts/token-savings.js +42 -0
  170. package/scripts/tool-kpi-tracker.js +108 -5
  171. package/scripts/tool-registry.js +193 -5
  172. package/scripts/universal-claim-evaluator.js +14 -2
  173. package/scripts/vector-store.js +279 -9
  174. package/scripts/workflow-notebook.js +391 -0
  175. package/scripts/workflow-sentinel.js +111 -12
  176. package/scripts/workos-production-guard.js +260 -0
  177. package/scripts/workspace-search-route.js +515 -0
  178. package/server.json +2 -2
  179. package/src/agent-identity-boundary.js +76 -0
  180. package/src/agent-retrieval-cache.js +155 -0
  181. package/src/alert-noise-ledger.js +502 -0
  182. package/src/api/server.js +724 -153
  183. package/src/git-fast-cache.js +220 -0
  184. package/src/git-wal-sync.js +156 -0
  185. package/src/hash-anchored-edit.js +82 -0
  186. package/src/hermes-platform-protocol.js +475 -0
  187. package/src/hermes-sync-plane.js +241 -0
  188. package/src/index.js +30 -1
  189. package/src/iso42001-compliance-guard.js +97 -0
  190. package/src/latency-budget.js +244 -0
  191. package/src/mcp-writeguard.js +316 -0
  192. package/src/miminions-adapter.js +106 -0
  193. package/src/pipeline-compass.js +104 -0
  194. package/src/ppl-alert-pipeline.js +284 -0
  195. package/src/rendezvous-router.js +90 -0
  196. package/src/security-questionnaire.js +195 -0
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hidden-entry-points scorecard — misconfigs attackers / runaway agents rely on.
6
+ *
7
+ * Process steal from a BrightTALK weekly rec digest (not BrightTALK, SailPoint,
8
+ * Strike48, Abnormal, A-LIGN, or ISO 42001 product):
9
+ * - "Watch everything" is not the digest — rank by interest, drop vendor theater
10
+ * - Hidden entry points = PreToolUse / identity / dynamic-tool misconfigs
11
+ * - Going agentic without a pre-action gate is the SOC pitfall
12
+ *
13
+ * Existing surfaces: agent-security-central hook drift + claw evaluateClawPretool.
14
+ * Not a new SOC / ISO 42001 SKU. Not BrightTALK affiliation.
15
+ *
16
+ * Usage:
17
+ * node scripts/hidden-entry-points.js
18
+ * node scripts/hidden-entry-points.js --json
19
+ * npm run hidden:entry
20
+ */
21
+
22
+ const fs = require('node:fs');
23
+ const path = require('node:path');
24
+
25
+ const SCHEMA = 'thumbgate.hidden_entry_points.v1';
26
+ const DEFAULT_INTERESTS = ['hidden-entry', 'identity', 'agentic-soc', 'ai-security'];
27
+ const DEFAULT_MAX_DIGEST = 5;
28
+
29
+ /**
30
+ * Fixture set: load-all is "watch every webinar"; digest is interest-ranked.
31
+ * `iso42001-theater` and vendor webinars are noise (not attacker entry points).
32
+ */
33
+ const ENTRY_POINTS = [
34
+ {
35
+ id: 'pretooluse-unwired',
36
+ theme: 'hidden-entry',
37
+ label: 'PreToolUse hook not on the wire',
38
+ attackerReliesOn: true,
39
+ noise: false,
40
+ open: true,
41
+ severity: 9,
42
+ },
43
+ {
44
+ id: 'dynamic-tool-ungated',
45
+ theme: 'agentic-soc',
46
+ label: 'Runtime dynamic tool creation without approval',
47
+ attackerReliesOn: true,
48
+ noise: false,
49
+ open: true,
50
+ severity: 8,
51
+ },
52
+ {
53
+ id: 'computer-use-ungated',
54
+ theme: 'hidden-entry',
55
+ label: 'Screen / computer-use without a pre-action gate',
56
+ attackerReliesOn: true,
57
+ noise: false,
58
+ open: true,
59
+ severity: 8,
60
+ },
61
+ {
62
+ id: 'agent-identity-missing',
63
+ theme: 'identity',
64
+ label: 'Tool calls without agent_identity',
65
+ attackerReliesOn: true,
66
+ noise: false,
67
+ open: true,
68
+ severity: 7,
69
+ },
70
+ {
71
+ id: 'review-volume-as-control',
72
+ theme: 'agentic-soc',
73
+ label: 'Human review volume treated as the control',
74
+ attackerReliesOn: true,
75
+ noise: false,
76
+ open: true,
77
+ severity: 6,
78
+ },
79
+ {
80
+ id: 'iso42001-theater',
81
+ theme: 'compliance',
82
+ label: 'Treat ISO 42001 marketing as an attestation',
83
+ attackerReliesOn: false,
84
+ noise: true,
85
+ open: false,
86
+ severity: 1,
87
+ },
88
+ {
89
+ id: 'vendor-webinar-sailpoint',
90
+ theme: 'identity',
91
+ label: 'SailPoint / BrightTALK vendor webinar as a product claim',
92
+ attackerReliesOn: false,
93
+ noise: true,
94
+ open: false,
95
+ severity: 0,
96
+ },
97
+ ];
98
+
99
+ function rankDigest(entries, options = {}) {
100
+ const interests = Array.isArray(options.interests) && options.interests.length
101
+ ? options.interests
102
+ : DEFAULT_INTERESTS;
103
+ const maxDigest = Number.isFinite(options.maxDigest) && options.maxDigest > 0
104
+ ? options.maxDigest
105
+ : DEFAULT_MAX_DIGEST;
106
+ const interestSet = new Set(interests);
107
+ return [...entries]
108
+ .filter((e) => !e.noise && interestSet.has(e.theme))
109
+ .sort((a, b) => (b.severity || 0) - (a.severity || 0))
110
+ .slice(0, maxDigest);
111
+ }
112
+
113
+ function applyLiveHookDrift(entries, projectRoot, homeDir, assessHookDriftFn) {
114
+ if (!projectRoot) return { entries, error: null };
115
+ try {
116
+ const assessHookDrift = typeof assessHookDriftFn === 'function'
117
+ ? assessHookDriftFn
118
+ : require('./agent-security-central').assessHookDrift;
119
+ const assessment = assessHookDrift(projectRoot, homeDir);
120
+ const drifted = Boolean(assessment && assessment.drifted);
121
+ return {
122
+ entries: entries.map((e) => (
123
+ e.id === 'pretooluse-unwired' ? { ...e, open: drifted } : e
124
+ )),
125
+ error: null,
126
+ };
127
+ } catch (err) {
128
+ return {
129
+ entries,
130
+ error: `Live hook drift assessment failed: ${err.message}`,
131
+ };
132
+ }
133
+ }
134
+
135
+ function runHiddenEntryScorecard(options = {}) {
136
+ const base = Array.isArray(options.entries) && options.entries.length
137
+ ? options.entries
138
+ : ENTRY_POINTS;
139
+ const { entries, error: liveError } = applyLiveHookDrift(
140
+ base,
141
+ options.projectRoot,
142
+ options.homeDir,
143
+ options.assessHookDrift
144
+ );
145
+ const loadAll = entries.map((e) => e.id);
146
+ const digest = rankDigest(entries, options);
147
+ const openAttacker = entries.filter((e) => e.attackerReliesOn && e.open);
148
+ const noiseDropped = entries.filter((e) => e.noise).map((e) => e.id);
149
+ const digestIds = digest.map((e) => e.id);
150
+ const digestHasNoise = digest.some((e) => e.noise);
151
+ const digestHasClosedTheater = digest.some((e) => !e.attackerReliesOn);
152
+
153
+ const failures = [];
154
+ if (liveError) {
155
+ failures.push(liveError);
156
+ }
157
+ if (openAttacker.length === 0) {
158
+ failures.push('no open hidden entry — fixture is not a misconfig case');
159
+ }
160
+ if (digest.length === 0) {
161
+ failures.push('interest digest is empty');
162
+ }
163
+ if (digest.length >= loadAll.length) {
164
+ failures.push('digest did not rank — load-all still shown');
165
+ }
166
+ if (digestHasNoise || digestHasClosedTheater) {
167
+ failures.push('digest still contains vendor/ISO theater');
168
+ }
169
+ if (!digestIds.includes('pretooluse-unwired') && openAttacker.some((e) => e.id === 'pretooluse-unwired')) {
170
+ failures.push('open PreToolUse miss not in digest');
171
+ }
172
+
173
+ let mode = 'simulation';
174
+ if (options.projectRoot) {
175
+ mode = liveError ? 'live-assessment-failed' : 'live-overlay';
176
+ }
177
+
178
+ return {
179
+ schema: SCHEMA,
180
+ mode,
181
+ generatedAt: new Date().toISOString(),
182
+ autoApply: false,
183
+ humanOversightRequired: true,
184
+ reviewVolumeIsNotTheControl: true,
185
+ capturedRevenueUsd: 0,
186
+ affiliation: 'none',
187
+ iso42001Certified: false,
188
+ process: {
189
+ source: 'BrightTALK weekly-rec process — not BrightTALK/SailPoint/ISO 42001 product',
190
+ interestRankedDigest: true,
191
+ unifiedAlertManager: false,
192
+ },
193
+ interests: options.interests || DEFAULT_INTERESTS,
194
+ loadAll,
195
+ digest: digestIds,
196
+ openAttacker: openAttacker.map((e) => e.id),
197
+ noiseDropped,
198
+ summary: {
199
+ ok: failures.length === 0,
200
+ failures,
201
+ },
202
+ disclaimers: [
203
+ 'SIMULATION unless --project. Does not install hooks.',
204
+ 'Not BrightTALK, SailPoint, Strike48, Abnormal, A-LIGN, or ISO 42001.',
205
+ 'Review volume is not the control — PreToolUse is. capturedRevenueUsd is 0.',
206
+ 'ThumbGate is not ISO 42001 certified. Do not treat this scorecard as an attestation.',
207
+ ],
208
+ };
209
+ }
210
+
211
+ function formatReport(report) {
212
+ return [
213
+ '# Hidden entry points vs interest digest',
214
+ '',
215
+ `Result: ${report.summary.ok ? 'PASS' : 'FAIL'} open=${(report.openAttacker || []).length}`,
216
+ '',
217
+ `| Path | Count |`,
218
+ `|------|------:|`,
219
+ `| Load-all webinars | ${report.loadAll.length} |`,
220
+ `| Interest digest | ${report.digest.length} |`,
221
+ `| Open attacker entries | ${report.openAttacker.length} |`,
222
+ '',
223
+ `Digest: ${(report.digest || []).join(', ') || '—'}`,
224
+ `Noise dropped: ${(report.noiseDropped || []).join(', ') || '—'}`,
225
+ `iso42001Certified=${report.iso42001Certified} autoApply=${report.autoApply} capturedRevenueUsd=${report.capturedRevenueUsd}`,
226
+ '',
227
+ ...report.disclaimers.map((d) => `- ${d}`),
228
+ '',
229
+ ].join('\n');
230
+ }
231
+
232
+ function mainCli(argv = process.argv.slice(2)) {
233
+ const json = argv.includes('--json');
234
+ const writeIdx = argv.indexOf('--write');
235
+ let writePath = null;
236
+ if (writeIdx >= 0) {
237
+ const candidate = argv[writeIdx + 1];
238
+ if (!candidate || candidate.startsWith('-')) {
239
+ process.stderr.write('Error: --write requires a valid file path\n');
240
+ return 1;
241
+ }
242
+ writePath = candidate;
243
+ }
244
+ const projectIdx = argv.indexOf('--project');
245
+ let projectRoot = null;
246
+ if (projectIdx >= 0) {
247
+ const candidate = argv[projectIdx + 1];
248
+ if (!candidate || candidate.startsWith('-')) {
249
+ process.stderr.write('Error: --project requires a valid directory path\n');
250
+ return 1;
251
+ }
252
+ projectRoot = candidate;
253
+ }
254
+ const report = runHiddenEntryScorecard(projectRoot ? { projectRoot } : {});
255
+
256
+ if (writePath) {
257
+ const abs = path.resolve(writePath);
258
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
259
+ const jsonPath = /\.json$/i.test(abs) ? abs : `${abs}.json`;
260
+ fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
261
+ }
262
+
263
+ if (json) {
264
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
265
+ } else {
266
+ process.stdout.write(formatReport(report));
267
+ }
268
+ return report.summary.ok ? 0 : 1;
269
+ }
270
+
271
+ module.exports = {
272
+ SCHEMA,
273
+ DEFAULT_INTERESTS,
274
+ ENTRY_POINTS,
275
+ rankDigest,
276
+ applyLiveHookDrift,
277
+ runHiddenEntryScorecard,
278
+ formatReport,
279
+ mainCli,
280
+ };
281
+
282
+ if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
283
+ process.exitCode = mainCli();
284
+ }
@@ -49,6 +49,7 @@ function requestEscalation(input = {}, options = {}) {
49
49
  const ttlMs = Math.min(MAX_TTL_MS, Math.max(1, finiteNumber(input.ttlMs, DEFAULT_TTL_MS)));
50
50
  const idempotencyKey = requiredString(input.idempotencyKey || taskId, 'idempotencyKey');
51
51
  const approvalContextDigest = optionalDigest(input.approvalContextDigest, 'approvalContextDigest');
52
+ const requiredReviewerRole = optionalString(input.requiredReviewerRole);
52
53
  const request = {
53
54
  escalationId: input.escalationId || `esc_${crypto.randomUUID()}`,
54
55
  idempotencyKey,
@@ -63,6 +64,7 @@ function requestEscalation(input = {}, options = {}) {
63
64
  eventType: 'requested',
64
65
  };
65
66
  if (approvalContextDigest) request.approvalContextDigest = approvalContextDigest;
67
+ if (requiredReviewerRole) request.requiredReviewerRole = requiredReviewerRole;
66
68
  return withEscalationLock(options, () => {
67
69
  const ledger = readLedger(options);
68
70
  assertLedgerHealthy(ledger);
@@ -107,6 +109,9 @@ function decideEscalation(input = {}, options = {}) {
107
109
  if (!current) throw escalationError(`unknown escalation '${escalationId}'`);
108
110
  if (current.status !== 'pending') throw escalationError(`escalation '${escalationId}' is already ${current.status}`);
109
111
  if (sameIdentity(current.requester, actor)) throw escalationError('requester cannot decide their own escalation');
112
+ if (current.requiredReviewerRole && actor.role !== current.requiredReviewerRole) {
113
+ throw escalationError(`authenticated reviewer must have role '${current.requiredReviewerRole}'`);
114
+ }
110
115
 
111
116
  const now = options.now || new Date();
112
117
  const event = {
@@ -145,6 +150,8 @@ function getVerifiedApprovalUnlocked(escalationId, options = {}) {
145
150
  const events = ledger.events.filter((event) => event.escalationId === escalationId);
146
151
  const requested = events.find((event) => event.eventType === 'requested');
147
152
  const decided = events.findLast((event) => event.eventType === 'decided');
153
+ const consumed = events.findLast((event) => event.eventType === 'consumed');
154
+ if (consumed && options.allowConsumed !== true) return null;
148
155
  if (!requested || !decided || decided.status !== 'approved') return null;
149
156
  if (decided.taskId !== requested.taskId) throw escalationError('approval task does not match its request');
150
157
  if ((requested.approvalContextDigest || null) !== (decided.approvalContextDigest || null)) {
@@ -153,6 +160,9 @@ function getVerifiedApprovalUnlocked(escalationId, options = {}) {
153
160
  if (decided.actor?.kind !== 'human' || sameIdentity(requested.requester, decided.actor)) {
154
161
  throw escalationError('approval is not from an independent human actor');
155
162
  }
163
+ if (requested.requiredReviewerRole && decided.actor.role !== requested.requiredReviewerRole) {
164
+ throw escalationError('approval is not from the required reviewer role');
165
+ }
156
166
  const verificationKey = optionalString(
157
167
  options.approvalVerificationKey || process.env.THUMBGATE_HUMAN_REVIEWER_KEY
158
168
  );
@@ -162,6 +172,33 @@ function getVerifiedApprovalUnlocked(escalationId, options = {}) {
162
172
  return { ...requested, ...decided };
163
173
  }
164
174
 
175
+ function consumeVerifiedApproval(escalationId, input = {}, options = {}) {
176
+ const consumer = requiredIdentity(input.consumer, 'consumer');
177
+ return withEscalationLock(options, () => {
178
+ const ledger = readLedger(options);
179
+ assertLedgerHealthy(ledger);
180
+ const events = ledger.events.filter((event) => event.escalationId === escalationId);
181
+ const existingConsumption = events.findLast((event) => event.eventType === 'consumed');
182
+ if (existingConsumption) {
183
+ return { consumed: false, replayed: true, consumption: existingConsumption };
184
+ }
185
+ const approval = getVerifiedApprovalUnlocked(escalationId, { ...options, allowConsumed: true });
186
+ if (!approval || Date.parse(approval.expiresAt) <= (options.now || new Date()).getTime()) {
187
+ return { consumed: false, replayed: false, consumption: null };
188
+ }
189
+ const consumedAt = (options.now || new Date()).toISOString();
190
+ const consumption = appendEventUnlocked({
191
+ escalationId,
192
+ taskId: approval.taskId,
193
+ eventType: 'consumed',
194
+ consumer,
195
+ consumedAt,
196
+ approvalContextDigest: approval.approvalContextDigest || null,
197
+ }, options, ledger.events);
198
+ return { consumed: true, replayed: false, approval, consumption };
199
+ });
200
+ }
201
+
165
202
  function listEscalations(options = {}) {
166
203
  return projectEscalations(readEvents(options), options);
167
204
  }
@@ -454,7 +491,7 @@ function approvalPayload(event) {
454
491
  taskId: event.taskId,
455
492
  status: event.status,
456
493
  decision: event.decision,
457
- actor: event.actor,
494
+ actor: event.actor ? { ...event.actor } : event.actor,
458
495
  reason: event.reason,
459
496
  decidedAt: event.decidedAt,
460
497
  };
@@ -470,6 +507,8 @@ function requiredIdentity(value, field) {
470
507
  };
471
508
  const displayName = optionalString(value.displayName);
472
509
  if (displayName) identity.displayName = displayName;
510
+ const role = optionalString(value.role);
511
+ if (role) identity.role = role;
473
512
  return identity;
474
513
  }
475
514
 
@@ -521,6 +560,7 @@ function eventComparableHash(event) {
521
560
  evidence: event.evidence,
522
561
  };
523
562
  if (event.approvalContextDigest) comparable.approvalContextDigest = event.approvalContextDigest;
563
+ if (event.requiredReviewerRole) comparable.requiredReviewerRole = event.requiredReviewerRole;
524
564
  return crypto.createHash('sha256').update(stableStringify(comparable)).digest('hex');
525
565
  }
526
566
 
@@ -548,6 +588,160 @@ function escalationError(message) {
548
588
  return error;
549
589
  }
550
590
 
591
+ function createMemoryGrantStore() {
592
+ const consumed = new Set();
593
+ return {
594
+ consume(grantId) {
595
+ const id = String(grantId || '').trim();
596
+ if (!id) return { ok: false, reason: 'missing_grant_id' };
597
+ if (consumed.has(id)) return { ok: false, reason: 'grant_already_consumed' };
598
+ consumed.add(id);
599
+ return { ok: true };
600
+ },
601
+ };
602
+ }
603
+
604
+ function createDurableGrantStore(options = {}) {
605
+ const storePath = options.grantStorePath || path.join(
606
+ getFeedbackPaths(options).FEEDBACK_DIR,
607
+ 'harness-grants.jsonl'
608
+ );
609
+ return {
610
+ consume(grantId) {
611
+ const id = String(grantId || '').trim();
612
+ if (!id) return { ok: false, reason: 'missing_grant_id' };
613
+ return withFileLedgerLock(`${storePath}.lock`, () => {
614
+ let raw = '';
615
+ try {
616
+ raw = fs.readFileSync(storePath, 'utf8');
617
+ } catch (error) {
618
+ if (error.code !== 'ENOENT') throw error;
619
+ }
620
+ for (const line of raw.split('\n')) {
621
+ if (!line.trim()) continue;
622
+ try {
623
+ if (JSON.parse(line).grantId === id) {
624
+ return { ok: false, reason: 'grant_already_consumed' };
625
+ }
626
+ } catch {
627
+ return { ok: false, reason: 'grant_store_corrupt' };
628
+ }
629
+ }
630
+ fs.mkdirSync(path.dirname(storePath), { recursive: true });
631
+ fs.appendFileSync(storePath, `${JSON.stringify({
632
+ grantId: id,
633
+ consumedAt: (options.now || new Date()).toISOString(),
634
+ })}\n`);
635
+ return { ok: true };
636
+ }, {
637
+ now: options.now,
638
+ lockStaleMs: options.lockStaleMs,
639
+ errorFactory: (message) => escalationError(message),
640
+ });
641
+ },
642
+ };
643
+ }
644
+ const ALLOW_ONCE_GRANT_MODES = new Set(['once', 'allow-once', 'allowed-once', 'allow_once']);
645
+ const ALLOW_ALWAYS_GRANT_MODES = new Set(['always', 'allow-always', 'allow_always']);
646
+
647
+ /**
648
+ * DeepSeek-style harness grant.
649
+ * Missing or throwing/failing approver fails closed (never silent-allow).
650
+ * Grants are allow-once only; allow-always is rejected.
651
+ */
652
+ function evaluateHarnessGrant(input = {}, options = {}) {
653
+ const grantMode = String(input.grantMode || input.mode || 'once').trim().toLowerCase();
654
+ if (ALLOW_ALWAYS_GRANT_MODES.has(grantMode)) {
655
+ return { allowed: false, failClosed: true, reason: 'allow_always_forbidden' };
656
+ }
657
+ if (!ALLOW_ONCE_GRANT_MODES.has(grantMode)) {
658
+ return { allowed: false, failClosed: true, reason: 'unsupported_grant_mode' };
659
+ }
660
+ if (input.approver == null) {
661
+ return { allowed: false, failClosed: true, reason: 'missing_approver' };
662
+ }
663
+
664
+ let verdict;
665
+ try {
666
+ verdict = typeof input.approver === 'function' ? input.approver(input.action) : input.approver;
667
+ } catch (error) {
668
+ return {
669
+ allowed: false,
670
+ failClosed: true,
671
+ reason: 'approver_failed',
672
+ detail: String(error && error.message ? error.message : error),
673
+ };
674
+ }
675
+ if (verdict && typeof verdict.then === 'function') {
676
+ return {
677
+ allowed: false,
678
+ failClosed: true,
679
+ reason: 'approver_failed',
680
+ detail: 'async approver must be resolved by caller',
681
+ };
682
+ }
683
+ if (!verdict || verdict.allow !== true) {
684
+ return {
685
+ allowed: false,
686
+ failClosed: true,
687
+ reason: (verdict && verdict.reason) || 'approver_denied',
688
+ };
689
+ }
690
+
691
+ const grantStore = options.grantStore || createDurableGrantStore(options);
692
+ const grantId = String(input.grantId || input.actionId || '').trim();
693
+ if (!grantId) {
694
+ return { allowed: false, failClosed: true, reason: 'missing_grant_id' };
695
+ }
696
+ let consumed;
697
+ try {
698
+ consumed = grantStore.consume(grantId);
699
+ } catch (error) {
700
+ return {
701
+ allowed: false,
702
+ failClosed: true,
703
+ reason: 'grant_store_failed',
704
+ detail: String(error && error.message ? error.message : error),
705
+ };
706
+ }
707
+ if (!consumed || !consumed.ok) {
708
+ return { allowed: false, failClosed: true, reason: (consumed && consumed.reason) || 'grant_already_consumed' };
709
+ }
710
+
711
+ const facts = {
712
+ actionId: grantId,
713
+ toolName: input.toolName || (input.action && input.action.toolName) || 'harness_action',
714
+ toolInput: (input.action && input.action.toolInput) || input.toolInput || {},
715
+ decision: 'allow',
716
+ grantMode: 'allow-once',
717
+ principal: input.principal || 'harness',
718
+ };
719
+
720
+ if (options.recordReceipt === false) {
721
+ return { allowed: true, failClosed: true, grantMode: 'allow-once', grantId, facts };
722
+ }
723
+
724
+ try {
725
+ const { recordReceipt } = require('./action-receipts');
726
+ const receipt = recordReceipt({
727
+ actionId: facts.actionId,
728
+ toolName: facts.toolName,
729
+ toolInput: facts.toolInput,
730
+ decision: facts.decision,
731
+ principal: facts.principal,
732
+ outcome: { testOutcome: 'approved-once' },
733
+ }, options);
734
+ return { allowed: true, failClosed: true, grantMode: 'allow-once', grantId, facts, receipt };
735
+ } catch (error) {
736
+ return {
737
+ allowed: false,
738
+ failClosed: true,
739
+ reason: 'receipt_record_failed',
740
+ detail: String(error && error.message ? error.message : error),
741
+ };
742
+ }
743
+ }
744
+
551
745
  function isCliInvocation() {
552
746
  return Boolean(process.argv[1]) && path.resolve(process.argv[1]) === __filename;
553
747
  }
@@ -565,7 +759,11 @@ if (isCliInvocation()) {
565
759
 
566
760
  module.exports = {
567
761
  calculateEscalationMetrics,
762
+ consumeVerifiedApproval,
763
+ createDurableGrantStore,
764
+ createMemoryGrantStore,
568
765
  decideEscalation,
766
+ evaluateHarnessGrant,
569
767
  getEscalation,
570
768
  getEscalationsHeadPath,
571
769
  getEscalationsJournalPath,