@jinn-network/core 0.1.0-canary.3afae198

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 (155) hide show
  1. package/dist/canonical-json.d.ts +1 -0
  2. package/dist/canonical-json.js +67 -0
  3. package/dist/captured-task.d.ts +140 -0
  4. package/dist/captured-task.js +122 -0
  5. package/dist/contribution-store.d.ts +97 -0
  6. package/dist/contribution-store.js +770 -0
  7. package/dist/corpus-read/acquire.d.ts +39 -0
  8. package/dist/corpus-read/acquire.js +212 -0
  9. package/dist/corpus-read/cache.d.ts +14 -0
  10. package/dist/corpus-read/cache.js +17 -0
  11. package/dist/corpus-read/capture-meta.d.ts +16 -0
  12. package/dist/corpus-read/capture-meta.js +18 -0
  13. package/dist/corpus-read/create-corpus.d.ts +2 -0
  14. package/dist/corpus-read/create-corpus.js +97 -0
  15. package/dist/corpus-read/fetch-artifact.d.ts +21 -0
  16. package/dist/corpus-read/fetch-artifact.js +32 -0
  17. package/dist/corpus-read/fetch.d.ts +12 -0
  18. package/dist/corpus-read/fetch.js +24 -0
  19. package/dist/corpus-read/http-discovery.d.ts +9 -0
  20. package/dist/corpus-read/http-discovery.js +128 -0
  21. package/dist/corpus-read/index.d.ts +10 -0
  22. package/dist/corpus-read/index.js +10 -0
  23. package/dist/corpus-read/ipfs.d.ts +14 -0
  24. package/dist/corpus-read/ipfs.js +81 -0
  25. package/dist/corpus-read/route-resolver.d.ts +16 -0
  26. package/dist/corpus-read/route-resolver.js +19 -0
  27. package/dist/corpus-read/types.d.ts +177 -0
  28. package/dist/corpus-read/types.js +42 -0
  29. package/dist/envelope.d.ts +134 -0
  30. package/dist/envelope.js +162 -0
  31. package/dist/evidence-adapter.d.ts +26 -0
  32. package/dist/evidence-adapter.js +321 -0
  33. package/dist/evidence-filesystem.d.ts +40 -0
  34. package/dist/evidence-filesystem.js +267 -0
  35. package/dist/evidence-index.d.ts +117 -0
  36. package/dist/evidence-index.js +1083 -0
  37. package/dist/evidence-store-lock.d.ts +2 -0
  38. package/dist/evidence-store-lock.js +208 -0
  39. package/dist/execution-envelope.d.ts +3142 -0
  40. package/dist/execution-envelope.js +203 -0
  41. package/dist/index.d.ts +12 -0
  42. package/dist/index.js +12 -0
  43. package/dist/manifest.d.ts +75 -0
  44. package/dist/manifest.js +184 -0
  45. package/dist/paired.d.ts +68 -0
  46. package/dist/paired.js +92 -0
  47. package/dist/scrub/apply-dispositions.d.ts +74 -0
  48. package/dist/scrub/apply-dispositions.js +309 -0
  49. package/dist/scrub/build.d.ts +75 -0
  50. package/dist/scrub/build.js +143 -0
  51. package/dist/scrub/checksummed-instruments-detector.d.ts +13 -0
  52. package/dist/scrub/checksummed-instruments-detector.js +119 -0
  53. package/dist/scrub/data/bip39-english.d.ts +2 -0
  54. package/dist/scrub/data/bip39-english.js +2051 -0
  55. package/dist/scrub/data/gitleaks-rules.d.ts +56 -0
  56. package/dist/scrub/data/gitleaks-rules.js +67 -0
  57. package/dist/scrub/emit-scrub.d.ts +52 -0
  58. package/dist/scrub/emit-scrub.js +122 -0
  59. package/dist/scrub/eval/findings-from-scrub.d.ts +18 -0
  60. package/dist/scrub/eval/findings-from-scrub.js +99 -0
  61. package/dist/scrub/eval/fixtures.d.ts +14 -0
  62. package/dist/scrub/eval/fixtures.js +159 -0
  63. package/dist/scrub/eval/index.d.ts +6 -0
  64. package/dist/scrub/eval/index.js +6 -0
  65. package/dist/scrub/eval/local-corpus.d.ts +15 -0
  66. package/dist/scrub/eval/local-corpus.js +29 -0
  67. package/dist/scrub/eval/metrics.d.ts +19 -0
  68. package/dist/scrub/eval/metrics.js +77 -0
  69. package/dist/scrub/eval/run-bench.d.ts +8 -0
  70. package/dist/scrub/eval/run-bench.js +125 -0
  71. package/dist/scrub/eval/types.d.ts +69 -0
  72. package/dist/scrub/eval/types.js +7 -0
  73. package/dist/scrub/finding.d.ts +38 -0
  74. package/dist/scrub/finding.js +7 -0
  75. package/dist/scrub/git-identity-detector.d.ts +16 -0
  76. package/dist/scrub/git-identity-detector.js +116 -0
  77. package/dist/scrub/gitleaks-detector.d.ts +13 -0
  78. package/dist/scrub/gitleaks-detector.js +45 -0
  79. package/dist/scrub/gliner-detector.d.ts +65 -0
  80. package/dist/scrub/gliner-detector.js +129 -0
  81. package/dist/scrub/index.d.ts +26 -0
  82. package/dist/scrub/index.js +26 -0
  83. package/dist/scrub/ip-address-detector.d.ts +12 -0
  84. package/dist/scrub/ip-address-detector.js +81 -0
  85. package/dist/scrub/key-policy.d.ts +30 -0
  86. package/dist/scrub/key-policy.js +72 -0
  87. package/dist/scrub/known-identity-detector.d.ts +66 -0
  88. package/dist/scrub/known-identity-detector.js +284 -0
  89. package/dist/scrub/layer2.d.ts +18 -0
  90. package/dist/scrub/layer2.js +28 -0
  91. package/dist/scrub/ml-pii-stage.d.ts +42 -0
  92. package/dist/scrub/ml-pii-stage.js +119 -0
  93. package/dist/scrub/pii-build.d.ts +47 -0
  94. package/dist/scrub/pii-build.js +67 -0
  95. package/dist/scrub/pipeline.d.ts +77 -0
  96. package/dist/scrub/pipeline.js +225 -0
  97. package/dist/scrub/plain-patterns-stage.d.ts +33 -0
  98. package/dist/scrub/plain-patterns-stage.js +88 -0
  99. package/dist/scrub/policy.d.ts +28 -0
  100. package/dist/scrub/policy.js +156 -0
  101. package/dist/scrub/provenance.d.ts +53 -0
  102. package/dist/scrub/provenance.js +83 -0
  103. package/dist/scrub/reject-classes-detector.d.ts +11 -0
  104. package/dist/scrub/reject-classes-detector.js +195 -0
  105. package/dist/scrub/reject-publish-error.d.ts +30 -0
  106. package/dist/scrub/reject-publish-error.js +47 -0
  107. package/dist/scrub/review-queue.d.ts +90 -0
  108. package/dist/scrub/review-queue.js +259 -0
  109. package/dist/scrub/secretlint-stage.d.ts +21 -0
  110. package/dist/scrub/secretlint-stage.js +264 -0
  111. package/dist/scrub/transformers-detector.d.ts +41 -0
  112. package/dist/scrub/transformers-detector.js +77 -0
  113. package/dist/scrub/types.d.ts +36 -0
  114. package/dist/scrub/types.js +1 -0
  115. package/dist/scrub/url-credentials-detector.d.ts +9 -0
  116. package/dist/scrub/url-credentials-detector.js +66 -0
  117. package/dist/session-provenance.d.ts +75 -0
  118. package/dist/session-provenance.js +20 -0
  119. package/dist/skill-artifact.d.ts +442 -0
  120. package/dist/skill-artifact.js +136 -0
  121. package/dist/trajectory/hash-chain.d.ts +18 -0
  122. package/dist/trajectory/hash-chain.js +47 -0
  123. package/dist/trajectory/index.d.ts +4 -0
  124. package/dist/trajectory/index.js +4 -0
  125. package/dist/trajectory/schema.d.ts +681 -0
  126. package/dist/trajectory/schema.js +119 -0
  127. package/dist/trajectory/transcript-parsers/aider-history.d.ts +42 -0
  128. package/dist/trajectory/transcript-parsers/aider-history.js +181 -0
  129. package/dist/trajectory/transcript-parsers/claude-code-jsonl.d.ts +37 -0
  130. package/dist/trajectory/transcript-parsers/claude-code-jsonl.js +142 -0
  131. package/dist/trajectory/transcript-parsers/codex-session.d.ts +42 -0
  132. package/dist/trajectory/transcript-parsers/codex-session.js +235 -0
  133. package/dist/trajectory/transcript-parsers/continue-devdata.d.ts +41 -0
  134. package/dist/trajectory/transcript-parsers/continue-devdata.js +190 -0
  135. package/dist/trajectory/transcript-parsers/cursor-sqlite.d.ts +38 -0
  136. package/dist/trajectory/transcript-parsers/cursor-sqlite.js +118 -0
  137. package/dist/trajectory/transcript-parsers/gemini-session.d.ts +35 -0
  138. package/dist/trajectory/transcript-parsers/gemini-session.js +137 -0
  139. package/dist/trajectory/transcript-parsers/index.d.ts +7 -0
  140. package/dist/trajectory/transcript-parsers/index.js +7 -0
  141. package/dist/trajectory/transcript-parsers/types.d.ts +101 -0
  142. package/dist/trajectory/transcript-parsers/types.js +32 -0
  143. package/dist/trajectory/transcript-to-spans/attrs.d.ts +4 -0
  144. package/dist/trajectory/transcript-to-spans/attrs.js +30 -0
  145. package/dist/trajectory/transcript-to-spans/claude-code-stream-json.d.ts +8 -0
  146. package/dist/trajectory/transcript-to-spans/claude-code-stream-json.js +162 -0
  147. package/dist/trajectory/transcript-to-spans/codex-exec-json.d.ts +8 -0
  148. package/dist/trajectory/transcript-to-spans/codex-exec-json.js +253 -0
  149. package/dist/trajectory/transcript-to-spans/index.d.ts +4 -0
  150. package/dist/trajectory/transcript-to-spans/index.js +4 -0
  151. package/dist/trajectory/transcript-to-spans/types.d.ts +18 -0
  152. package/dist/trajectory/transcript-to-spans/types.js +1 -0
  153. package/dist/window.d.ts +12 -0
  154. package/dist/window.js +5 -0
  155. package/package.json +72 -0
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Scrub provenance for the published redaction manifest (#1974 / design §6.7).
3
+ *
4
+ * Locked Q5: additive optional fields on the existing redaction-manifest shape
5
+ * (`schemaVersion` 2) — no TraceEnvelope rev. Old envelopes remain readable.
6
+ */
7
+ import { sha256 } from '@noble/hashes/sha2.js';
8
+ import { bytesToHex } from '@noble/hashes/utils.js';
9
+ import { canonicalJson } from '../canonical-json.js';
10
+ /** Manifest schema version that includes optional provenance fields. */
11
+ export const REDACTION_MANIFEST_SCHEMA_VERSION = 2;
12
+ /**
13
+ * Stable key for one class × applied disposition in `perClassCounts`.
14
+ * Reject-publish dispositions are recorded as `reject`.
15
+ */
16
+ export function perClassCountKey(scrubClass, disposition) {
17
+ return `${scrubClass}:${disposition}`;
18
+ }
19
+ /** Map a policy disposition onto the counted kind (or null for pass). */
20
+ export function countedDisposition(disposition) {
21
+ if (disposition === 'pass')
22
+ return null;
23
+ if (disposition === 'reject-publish')
24
+ return 'reject';
25
+ return disposition;
26
+ }
27
+ /**
28
+ * sha256 hex over a canonical JSON of allowlist entries sorted by
29
+ * (value, kind). Provenance notes are excluded so the digest is about
30
+ * *what* is allowlisted, not the human rationale string.
31
+ */
32
+ export function computeAllowlistDigest(entries) {
33
+ const normalized = [...entries]
34
+ .map((e) => ({ value: e.value.toLowerCase(), kind: e.kind }))
35
+ .sort((a, b) => {
36
+ const byValue = a.value.localeCompare(b.value);
37
+ return byValue !== 0 ? byValue : a.kind.localeCompare(b.kind);
38
+ });
39
+ return sha256Hex(canonicalJson(normalized));
40
+ }
41
+ /**
42
+ * Reproducible policy/composition hash: same inputs → same hex digest.
43
+ * Payload is RFC 8785 canonical JSON of policy + sorted detectors + model
44
+ * id + sorted labels + allowlist digest, then sha256.
45
+ */
46
+ export function computePolicyHash(input) {
47
+ const detectors = [...input.detectors]
48
+ .map((d) => ({ name: d.name, version: d.version }))
49
+ .sort((a, b) => {
50
+ const byName = a.name.localeCompare(b.name);
51
+ return byName !== 0 ? byName : a.version.localeCompare(b.version);
52
+ });
53
+ const labels = [...input.labels].sort((a, b) => a.localeCompare(b));
54
+ const payload = {
55
+ allowlistDigest: input.allowlistDigest,
56
+ detectors,
57
+ labels,
58
+ modelId: input.modelId,
59
+ policy: input.policy,
60
+ };
61
+ return sha256Hex(canonicalJson(payload));
62
+ }
63
+ /** Increment one class:disposition counter (mutates `counts`). */
64
+ export function incrementPerClassCount(counts, scrubClass, disposition) {
65
+ const kind = countedDisposition(disposition);
66
+ if (!kind)
67
+ return;
68
+ const key = perClassCountKey(scrubClass, kind);
69
+ counts[key] = (counts[key] ?? 0) + 1;
70
+ }
71
+ /** Sum two per-class count maps (missing → empty). */
72
+ export function mergePerClassCounts(a, b) {
73
+ if (!a && !b)
74
+ return undefined;
75
+ const out = { ...(a ?? {}) };
76
+ for (const [key, n] of Object.entries(b ?? {})) {
77
+ out[key] = (out[key] ?? 0) + n;
78
+ }
79
+ return out;
80
+ }
81
+ function sha256Hex(utf8) {
82
+ return bytesToHex(sha256(new TextEncoder().encode(utf8)));
83
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Reject-publish content detectors — A4 / content-A5 (#1972 / design §3.2).
3
+ *
4
+ * A4: 64-hex private-key material in key context; BIP-39 12/24-word mnemonic runs.
5
+ * A5: env-block dumps (`KEY=value` line runs inside tool output).
6
+ *
7
+ * Bare git SHAs (40-hex) and `0x`+64 tx digests are carved out — never swept.
8
+ */
9
+ import { type KeyPolicy } from './key-policy.js';
10
+ import type { Detector } from './finding.js';
11
+ export declare function rejectClassesDetector(policy: KeyPolicy): Detector;
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Reject-publish content detectors — A4 / content-A5 (#1972 / design §3.2).
3
+ *
4
+ * A4: 64-hex private-key material in key context; BIP-39 12/24-word mnemonic runs.
5
+ * A5: env-block dumps (`KEY=value` line runs inside tool output).
6
+ *
7
+ * Bare git SHAs (40-hex) and `0x`+64 tx digests are carved out — never swept.
8
+ */
9
+ import { BIP39_ENGLISH } from './data/bip39-english.js';
10
+ import { classifyKey } from './key-policy.js';
11
+ const VERSION = '1.0.0';
12
+ let bip39Words = null;
13
+ function loadBip39() {
14
+ if (bip39Words)
15
+ return bip39Words;
16
+ bip39Words = new Set(BIP39_ENGLISH);
17
+ return bip39Words;
18
+ }
19
+ /**
20
+ * Keywords that put a nearby 64-hex run in private-key context.
21
+ * Deliberately excludes bare "key" / "hash" / "digest" / "sha".
22
+ */
23
+ const KEY_CONTEXT = /\b(?:private[_\s-]?key|privkey|secret[_\s-]?key|wallet[_\s-]?key|eth(?:ereum)?[_\s-]?private|signing[_\s-]?key|hex[_\s-]?(?:private|secret)|mnemonic|seed[_\s-]?phrase|xprv|wif)\b/i;
24
+ /** Optional 0x-prefixed or bare 64-hex. */
25
+ const HEX64 = /\b(?:0x)?([a-fA-F0-9]{64})\b/g;
26
+ /** ENV-style assignment line. */
27
+ const ENV_LINE = /^[A-Z][A-Z0-9_]*=.*$/;
28
+ const ENV_KEY = /^([A-Z][A-Z0-9_]*)=/;
29
+ /** Git's GIT_CONFIG_* override tutorial shape — not a secret dump (#2005). */
30
+ const GIT_CONFIG_ENV_KEY = /^GIT_CONFIG(?:_|$)/i;
31
+ const MIN_ENV_RUN = 3;
32
+ export function rejectClassesDetector(policy) {
33
+ const meta = { name: 'reject-classes', version: VERSION };
34
+ const wordlist = loadBip39();
35
+ return {
36
+ ...meta,
37
+ detect(attributes) {
38
+ const findings = [];
39
+ for (const [key, value] of Object.entries(attributes)) {
40
+ if (typeof value !== 'string' || classifyKey(key, policy) !== 'content')
41
+ continue;
42
+ findings.push(...scanPrivateKeys(value, key, meta));
43
+ findings.push(...scanMnemonics(value, key, meta, wordlist));
44
+ findings.push(...scanEnvBlocks(value, key, meta));
45
+ }
46
+ return findings;
47
+ },
48
+ };
49
+ }
50
+ function scanPrivateKeys(text, key, detector) {
51
+ if (!KEY_CONTEXT.test(text))
52
+ return [];
53
+ // Reset lastIndex after .test on global-less regex — KEY_CONTEXT has /i only.
54
+ const findings = [];
55
+ const re = new RegExp(HEX64.source, HEX64.flags);
56
+ let m;
57
+ while ((m = re.exec(text)) !== null) {
58
+ const full = m[0];
59
+ // Carve-out: 0x+64 is a tx digest (C2), not a private key span by shape alone.
60
+ // Only fire when the match is bare 64-hex (no 0x) — wallet hex keys are typically
61
+ // written without the 0x prefix next to "private key" context, while receipts use 0x.
62
+ if (full.startsWith('0x') || full.startsWith('0X'))
63
+ continue;
64
+ // Windowed context: require a key-context keyword within ±80 chars.
65
+ const windowStart = Math.max(0, m.index - 80);
66
+ const windowEnd = Math.min(text.length, m.index + full.length + 80);
67
+ const window = text.slice(windowStart, windowEnd);
68
+ if (!KEY_CONTEXT.test(window))
69
+ continue;
70
+ findings.push({
71
+ class: 'A4',
72
+ span: { key, start: m.index, end: m.index + full.length },
73
+ confidence: 'VERY_HIGH',
74
+ evidence: ['private-key-hex64'],
75
+ detector,
76
+ });
77
+ }
78
+ return findings;
79
+ }
80
+ function scanMnemonics(text, key, detector, wordlist) {
81
+ const findings = [];
82
+ // Tokenize on whitespace / commas / quotes while keeping offsets.
83
+ const tokenRe = /[A-Za-z]+/g;
84
+ const tokens = [];
85
+ let tm;
86
+ while ((tm = tokenRe.exec(text)) !== null) {
87
+ tokens.push({
88
+ word: tm[0].toLowerCase(),
89
+ start: tm.index,
90
+ end: tm.index + tm[0].length,
91
+ });
92
+ }
93
+ let i = 0;
94
+ while (i < tokens.length) {
95
+ if (!wordlist.has(tokens[i].word)) {
96
+ i += 1;
97
+ continue;
98
+ }
99
+ let j = i;
100
+ while (j < tokens.length && wordlist.has(tokens[j].word))
101
+ j += 1;
102
+ const runTokens = tokens.slice(i, j);
103
+ // Sliding windows: a longer wordlist run (e.g. "phrase" + 12 mnemonic words)
104
+ // still contains a 12/24 mnemonic.
105
+ for (const len of [24, 12]) {
106
+ if (runTokens.length < len)
107
+ continue;
108
+ for (let start = 0; start + len <= runTokens.length; start += 1) {
109
+ const window = runTokens.slice(start, start + len);
110
+ let contiguous = true;
111
+ for (let k = 0; k < window.length - 1; k += 1) {
112
+ const gap = text.slice(window[k].end, window[k + 1].start);
113
+ if (!/^[\s,;|"']+$/.test(gap)) {
114
+ contiguous = false;
115
+ break;
116
+ }
117
+ }
118
+ if (!contiguous)
119
+ continue;
120
+ findings.push({
121
+ class: 'A4',
122
+ span: { key, start: window[0].start, end: window[len - 1].end },
123
+ confidence: 'VERY_HIGH',
124
+ evidence: [`bip39-mnemonic-${len}`],
125
+ detector,
126
+ });
127
+ // One hit per run length is enough — avoid overlapping spam.
128
+ break;
129
+ }
130
+ }
131
+ i = j;
132
+ }
133
+ return findings;
134
+ }
135
+ function scanEnvBlocks(text, key, detector) {
136
+ const findings = [];
137
+ const lines = text.split('\n');
138
+ let offset = 0;
139
+ let runStart = -1;
140
+ let runStartOffset = 0;
141
+ let runEndOffset = 0;
142
+ let runCount = 0;
143
+ const flush = () => {
144
+ if (runCount >= MIN_ENV_RUN && runStart >= 0) {
145
+ findings.push({
146
+ class: 'A5',
147
+ span: { key, start: runStartOffset, end: runEndOffset },
148
+ confidence: 'VERY_HIGH',
149
+ evidence: ['env-block'],
150
+ detector,
151
+ });
152
+ }
153
+ runStart = -1;
154
+ runCount = 0;
155
+ };
156
+ for (let i = 0; i < lines.length; i += 1) {
157
+ const line = lines[i];
158
+ const lineLen = line.length + (i < lines.length - 1 ? 1 : 0); // +1 for \n except last
159
+ const trimmed = line.trim();
160
+ // Allow optional leading export / whitespace.
161
+ const candidate = trimmed.replace(/^export\s+/, '');
162
+ if (isEnvBlockAssignment(candidate)) {
163
+ if (runStart < 0) {
164
+ runStart = i;
165
+ runStartOffset = offset + (line.length - line.trimStart().length);
166
+ }
167
+ runCount += 1;
168
+ runEndOffset = offset + line.length;
169
+ }
170
+ else if ((trimmed === '' || isGitConfigEnvAssignment(candidate)) && runCount > 0) {
171
+ // Blank and GIT_CONFIG tutorial lines inside a dump keep the run open,
172
+ // but do not increment it — only flush on another non-env line.
173
+ }
174
+ else {
175
+ flush();
176
+ }
177
+ offset += lineLen;
178
+ }
179
+ flush();
180
+ return findings;
181
+ }
182
+ function isEnvBlockAssignment(candidate) {
183
+ if (!ENV_LINE.test(candidate))
184
+ return false;
185
+ const keyMatch = ENV_KEY.exec(candidate);
186
+ if (!keyMatch)
187
+ return false;
188
+ return !GIT_CONFIG_ENV_KEY.test(keyMatch[1]);
189
+ }
190
+ function isGitConfigEnvAssignment(candidate) {
191
+ if (!ENV_LINE.test(candidate))
192
+ return false;
193
+ const keyMatch = ENV_KEY.exec(candidate);
194
+ return keyMatch !== null && GIT_CONFIG_ENV_KEY.test(keyMatch[1]);
195
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Loud abort for reject-publish disposition (#1972 / design §3.2 A4/A5).
3
+ *
4
+ * Catastrophic content must fail closed with a class-named error — never
5
+ * silent redact-and-continue.
6
+ */
7
+ import type { Finding, ScrubClass } from './finding.js';
8
+ import { type PolicyTable } from './policy.js';
9
+ export declare class RejectPublishError extends Error {
10
+ readonly scrubClass: ScrubClass;
11
+ readonly findings: Finding[];
12
+ constructor(scrubClass: ScrubClass, findings?: Finding[]);
13
+ }
14
+ /**
15
+ * Findings whose policy disposition is reject-publish.
16
+ *
17
+ * A5 `drop-key` findings are structural removals, not catastrophic content:
18
+ * the key-policy detector has already removed the entire attribute before
19
+ * this guard runs. Only content-level A5 findings (such as an env-block
20
+ * embedded in tool output) must abort the publish.
21
+ */
22
+ export declare function rejectPublishFindings(findings: Finding[] | undefined, policy?: PolicyTable): Finding[];
23
+ /**
24
+ * Throw a class-named {@link RejectPublishError} when reject-publish fired.
25
+ * No-op when the result only has ordinary redacts / check-mode rejects.
26
+ */
27
+ export declare function assertNoRejectPublish(result: {
28
+ rejected?: boolean;
29
+ findings?: Finding[];
30
+ }, policy?: PolicyTable): void;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Loud abort for reject-publish disposition (#1972 / design §3.2 A4/A5).
3
+ *
4
+ * Catastrophic content must fail closed with a class-named error — never
5
+ * silent redact-and-continue.
6
+ */
7
+ import { resolveDisposition } from './policy.js';
8
+ import { DEFAULT_POLICY } from './policy.js';
9
+ export class RejectPublishError extends Error {
10
+ scrubClass;
11
+ findings;
12
+ constructor(scrubClass, findings = []) {
13
+ const evidence = findings[0]?.evidence?.join(', ') ?? scrubClass;
14
+ super(`reject-publish: scrub class ${scrubClass} — publish aborted (${evidence})`);
15
+ this.name = 'RejectPublishError';
16
+ this.scrubClass = scrubClass;
17
+ this.findings = findings;
18
+ }
19
+ }
20
+ /**
21
+ * Findings whose policy disposition is reject-publish.
22
+ *
23
+ * A5 `drop-key` findings are structural removals, not catastrophic content:
24
+ * the key-policy detector has already removed the entire attribute before
25
+ * this guard runs. Only content-level A5 findings (such as an env-block
26
+ * embedded in tool output) must abort the publish.
27
+ */
28
+ export function rejectPublishFindings(findings, policy = DEFAULT_POLICY) {
29
+ if (!findings?.length)
30
+ return [];
31
+ return findings.filter((f) => !f.evidence.includes('drop-key') &&
32
+ resolveDisposition(f.class, f.confidence, policy) === 'reject-publish');
33
+ }
34
+ /**
35
+ * Throw a class-named {@link RejectPublishError} when reject-publish fired.
36
+ * No-op when the result only has ordinary redacts / check-mode rejects.
37
+ */
38
+ export function assertNoRejectPublish(result, policy = DEFAULT_POLICY) {
39
+ const hits = rejectPublishFindings(result.findings, policy);
40
+ if (hits.length === 0)
41
+ return;
42
+ // Prefer A4 over A5 when both fire (funds-controlling material first).
43
+ const primary = hits.find((f) => f.class === 'A4') ??
44
+ hits.find((f) => f.class === 'A5') ??
45
+ hits[0];
46
+ throw new RejectPublishError(primary.class, hits.filter((f) => f.class === primary.class));
47
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Operator-local review queue for scrub `flag` dispositions (#1973 / design §6.6).
3
+ *
4
+ * Persistence: `~/.jinn-client/scrub-review/queue.jsonl` (one JSON object per
5
+ * line). Traces never leave the machine — this queue is local-only.
6
+ *
7
+ * Unattended publish lanes fail closed while any matching flag remains
8
+ * `pending` (see {@link assertNoUnresolvedFlags}).
9
+ */
10
+ import type { Finding } from './finding.js';
11
+ import { type PolicyTable } from './policy.js';
12
+ export type ReviewDecision = 'approve-instance' | 'redact-instance' | 'add-to-allowlist' | 'add-to-identity-pack';
13
+ export type ReviewItemStatus = 'pending' | 'resolved';
14
+ export interface ReviewQueueItem {
15
+ id: string;
16
+ finding: Finding;
17
+ context: {
18
+ attributeKey: string;
19
+ snippet: string;
20
+ };
21
+ createdAt: string;
22
+ status: ReviewItemStatus;
23
+ resolution?: {
24
+ decision: ReviewDecision;
25
+ resolvedAt: string;
26
+ };
27
+ }
28
+ export interface ReviewQueueStore {
29
+ listFlagged(filter?: {
30
+ status?: ReviewItemStatus;
31
+ }): ReviewQueueItem[];
32
+ resolveFlag(id: string, decision: ReviewDecision): ReviewQueueItem;
33
+ enqueue(items: Array<Omit<ReviewQueueItem, 'id' | 'createdAt' | 'status'>>): ReviewQueueItem[];
34
+ /** Fingerprint → latest resolved decision (if any). */
35
+ resolutionFor(finding: Finding): ReviewDecision | undefined;
36
+ }
37
+ export declare const DEFAULT_REVIEW_QUEUE_PATH: string;
38
+ /** Stable fingerprint for matching a finding across scrub runs. */
39
+ export declare function findingFingerprint(finding: Finding): string;
40
+ /** In-memory review queue for unit tests (no disk I/O). */
41
+ export declare function createMemoryReviewQueueStore(seed?: ReviewQueueItem[]): ReviewQueueStore;
42
+ /**
43
+ * File-backed review queue. Latest write wins per id; fingerprints look at the
44
+ * newest resolved entry.
45
+ */
46
+ export declare function createReviewQueueStore(path?: string): ReviewQueueStore;
47
+ export declare function listFlagged(filter?: {
48
+ status?: ReviewItemStatus;
49
+ }): ReviewQueueItem[];
50
+ export declare function resolveFlag(id: string, decision: ReviewDecision): ReviewQueueItem;
51
+ export declare function enqueueFlags(items: Array<Omit<ReviewQueueItem, 'id' | 'createdAt' | 'status'>>, store?: ReviewQueueStore): ReviewQueueItem[];
52
+ /**
53
+ * Thrown when redact-mode scrub leaves unresolved `flag` dispositions.
54
+ * Unattended publish must not proceed over an open flag (design §6.5).
55
+ */
56
+ export declare class UnresolvedFlagError extends Error {
57
+ readonly findings: Finding[];
58
+ readonly queueIds: string[];
59
+ constructor(findings: Finding[], queueIds?: string[]);
60
+ }
61
+ export interface FlagDispositionOptions {
62
+ policy?: PolicyTable;
63
+ /** Attributes bag used to build review-queue context snippets. */
64
+ attributes?: Record<string, unknown>;
65
+ store?: ReviewQueueStore;
66
+ /**
67
+ * When true (default for redact-mode publish), enqueue + throw if any flag
68
+ * remains unresolved. Check-mode distill sets this false (rejection is via
69
+ * redactions / rejected bit instead).
70
+ */
71
+ failClosed?: boolean;
72
+ }
73
+ /**
74
+ * Apply review-queue resolutions to flag findings, enqueue new pending flags,
75
+ * and optionally fail closed.
76
+ *
77
+ * - `approve-instance` / allowlist / identity-pack → treat as pass (no throw)
78
+ * - `redact-instance` → caller should promote to redact (returned in `toRedact`)
79
+ * - unresolved → enqueue + throw when `failClosed`
80
+ */
81
+ export declare function processFlagFindings(findings: Finding[], opts?: FlagDispositionOptions): {
82
+ unresolved: Finding[];
83
+ toRedact: Finding[];
84
+ toPass: Finding[];
85
+ enqueued: ReviewQueueItem[];
86
+ };
87
+ /**
88
+ * Fail closed when unresolved flags remain after {@link processFlagFindings}.
89
+ */
90
+ export declare function assertNoUnresolvedFlags(findings: Finding[], opts?: FlagDispositionOptions): void;