@polylogicai/polycode 1.1.0
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.
- package/LICENSE +24 -0
- package/README.md +107 -0
- package/bin/polycode.mjs +317 -0
- package/lib/agency-receipt.mjs +45 -0
- package/lib/agentic.mjs +505 -0
- package/lib/canon.mjs +123 -0
- package/lib/commitment.mjs +59 -0
- package/lib/compiler.mjs +166 -0
- package/lib/context-builder.mjs +79 -0
- package/lib/hooks.mjs +118 -0
- package/lib/inference-router.mjs +67 -0
- package/lib/intent.mjs +31 -0
- package/lib/repl-ui.mjs +91 -0
- package/lib/slash-commands.mjs +83 -0
- package/lib/witness/conservativity.mjs +90 -0
- package/lib/witness/g-fidelity.mjs +80 -0
- package/lib/witness/ground-truth.mjs +56 -0
- package/lib/witness/index.mjs +70 -0
- package/lib/witness/rule-compliance.mjs +82 -0
- package/lib/witness/secret-scrubber.mjs +51 -0
- package/package.json +45 -0
- package/rules/default.yaml +58 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// lib/witness/conservativity.mjs
|
|
2
|
+
// Conservativity primitive. Ported from ~/polybrain-kernel/src/witness/conservativity.mjs.
|
|
3
|
+
// Pure recall-based token overlap + numeric tolerance. No LLM.
|
|
4
|
+
// Returns {verdict: 'PASS' | 'FAIL' | 'PENDING', detail: string}.
|
|
5
|
+
//
|
|
6
|
+
// Recall = |claim tokens that appear in substrate| / |claim tokens|.
|
|
7
|
+
// The recall discipline is "does the substrate support every meaningful token
|
|
8
|
+
// in the claim." Numeric claims are matched within a relative tolerance.
|
|
9
|
+
|
|
10
|
+
const RECALL_MIN_DEFAULT = 0.6;
|
|
11
|
+
const NUMERIC_TOLERANCE_DEFAULT = 0.02;
|
|
12
|
+
|
|
13
|
+
const STOPWORDS = new Set([
|
|
14
|
+
'the','a','an','of','in','on','at','to','for','with','by','is','are','was','were','be','been','being',
|
|
15
|
+
'and','or','but','if','then','this','that','these','those','it','as','from','into','over','under','than',
|
|
16
|
+
'so','such','not','no','yes','also','can','will','would','could','should','may','might','do','does','did',
|
|
17
|
+
'has','have','had','i','you','he','she','we','they','them','his','her','their','our','my','your',
|
|
18
|
+
'about','here','there','now','been','being','any','all','some','one','two','three','four','five',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function tokenize(s) {
|
|
22
|
+
return (s || '')
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[^a-z0-9.\- ]+/g, ' ')
|
|
25
|
+
.split(/\s+/)
|
|
26
|
+
.filter((t) => t && t.length > 2 && !STOPWORDS.has(t));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function extractNumerics(s) {
|
|
30
|
+
const re = /-?\d{1,3}(?:,\d{3})+(?:\.\d+)?|-?\d+\.?\d*(?:[eE][-+]?\d+)?|-?\.\d+/g;
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const m of (s || '').matchAll(re)) {
|
|
33
|
+
const raw = m[0].replace(/,/g, '');
|
|
34
|
+
const n = Number(raw);
|
|
35
|
+
if (!Number.isNaN(n)) out.push(n);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function recall(claimTokens, substrateTokens) {
|
|
41
|
+
const claimSet = new Set(claimTokens);
|
|
42
|
+
if (claimSet.size === 0) return 0;
|
|
43
|
+
const substrateSet = new Set(substrateTokens);
|
|
44
|
+
let present = 0;
|
|
45
|
+
for (const t of claimSet) if (substrateSet.has(t)) present++;
|
|
46
|
+
return present / claimSet.size;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function numericMatch(claimNums, substrateNums, tolerance) {
|
|
50
|
+
if (claimNums.length === 0) return true;
|
|
51
|
+
for (const c of claimNums) {
|
|
52
|
+
let matched = false;
|
|
53
|
+
for (const s of substrateNums) {
|
|
54
|
+
if (Number.isInteger(c) && Number.isInteger(s) && c === s) { matched = true; break; }
|
|
55
|
+
const denom = Math.max(Math.abs(c), 1e-9);
|
|
56
|
+
if (Math.abs(c - s) / denom <= tolerance) { matched = true; break; }
|
|
57
|
+
}
|
|
58
|
+
if (!matched) return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function conservativity({ claim, substrate }, opts = {}) {
|
|
64
|
+
const text = claim || '';
|
|
65
|
+
const sub = substrate || '';
|
|
66
|
+
const recallMin = opts.recall_min ?? RECALL_MIN_DEFAULT;
|
|
67
|
+
const tolerance = opts.numeric_tolerance ?? NUMERIC_TOLERANCE_DEFAULT;
|
|
68
|
+
|
|
69
|
+
if (!sub.trim()) return { verdict: 'PENDING', detail: 'empty substrate' };
|
|
70
|
+
|
|
71
|
+
const claimTokens = tokenize(text);
|
|
72
|
+
if (claimTokens.length === 0) return { verdict: 'PENDING', detail: 'no checkable tokens in claim' };
|
|
73
|
+
|
|
74
|
+
const substrateTokens = tokenize(sub);
|
|
75
|
+
const r = recall(claimTokens, substrateTokens);
|
|
76
|
+
const claimNums = extractNumerics(text);
|
|
77
|
+
const substrateNums = extractNumerics(sub);
|
|
78
|
+
const nums = numericMatch(claimNums, substrateNums, tolerance);
|
|
79
|
+
|
|
80
|
+
if (!nums) {
|
|
81
|
+
return { verdict: 'FAIL', detail: 'numeric mismatch (claim contains numbers not grounded in substrate)' };
|
|
82
|
+
}
|
|
83
|
+
if (r >= recallMin) {
|
|
84
|
+
return { verdict: 'PASS', detail: `recall ${r.toFixed(2)} >= ${recallMin}, numerics matched` };
|
|
85
|
+
}
|
|
86
|
+
// Low recall is a weak signal in polycode's interactive flow because the claim
|
|
87
|
+
// and substrate are at different granularities. PEND rather than FAIL so the
|
|
88
|
+
// commitment is not refuted on benign token distance.
|
|
89
|
+
return { verdict: 'PENDING', detail: `recall ${r.toFixed(2)} < ${recallMin}, numerics matched` };
|
|
90
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// lib/witness/g-fidelity.mjs
|
|
2
|
+
// G_fidelity. The fifth witness primitive. Checks whether the generator's output
|
|
3
|
+
// is grounded in the canon. Produces a scalar in [0, 1] plus a verdict.
|
|
4
|
+
//
|
|
5
|
+
// Method: extract content tokens from the claim, compute recall against the
|
|
6
|
+
// substrate of the compiled packet plus recent canon rows within the active
|
|
7
|
+
// intent. If recall is high, G_fidelity is high, and the composer upgrades
|
|
8
|
+
// the commitment toward VERIFIED. If recall is low, G_fidelity is low, and
|
|
9
|
+
// the composer downgrades the commitment toward PENDING or REFUTED.
|
|
10
|
+
//
|
|
11
|
+
// The multiplicative composition `I = V_witness × G_fidelity` lives in the
|
|
12
|
+
// composer (lib/witness/index.mjs). This file only computes the scalar.
|
|
13
|
+
|
|
14
|
+
const RECALL_STRONG = 0.8;
|
|
15
|
+
const RECALL_WEAK = 0.5;
|
|
16
|
+
|
|
17
|
+
const STOPWORDS = new Set([
|
|
18
|
+
'the','a','an','of','in','on','at','to','for','with','by','is','are','was','were','be','been','being',
|
|
19
|
+
'and','or','but','if','then','this','that','these','those','it','as','from','into','over','under','than',
|
|
20
|
+
'so','such','not','no','yes','also','can','will','would','could','should','may','might','do','does','did',
|
|
21
|
+
'has','have','had','i','you','he','she','we','they','them','his','her','their','our','my','your',
|
|
22
|
+
'about','here','there','now','any','all','some','one','two','three','four','five',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function tokenize(s) {
|
|
26
|
+
return (s || '')
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9.\-_/ ]+/g, ' ')
|
|
29
|
+
.split(/\s+/)
|
|
30
|
+
.filter((t) => t && t.length > 2 && !STOPWORDS.has(t));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function gFidelity({ claim, packet, canon, intentId }) {
|
|
34
|
+
if (!claim || typeof claim !== 'string') {
|
|
35
|
+
return { verdict: 'PENDING', detail: 'no claim to check', scalar: 0.5 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const claimTokens = tokenize(claim);
|
|
39
|
+
if (claimTokens.length === 0) {
|
|
40
|
+
return { verdict: 'PENDING', detail: 'no checkable tokens in claim', scalar: 0.5 };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const substrateParts = [];
|
|
44
|
+
if (packet && typeof packet === 'string') substrateParts.push(packet);
|
|
45
|
+
if (canon && intentId) {
|
|
46
|
+
const rows = canon.queryByIntent(intentId, 30);
|
|
47
|
+
for (const r of rows) {
|
|
48
|
+
if (r.type === 'witnessed_commitment' && r.payload) {
|
|
49
|
+
substrateParts.push(String(r.payload.claim || ''));
|
|
50
|
+
substrateParts.push(String(r.payload.tool_result_snippet || ''));
|
|
51
|
+
} else if (r.type === 'user_turn' && r.payload) {
|
|
52
|
+
substrateParts.push(String(r.payload.message || ''));
|
|
53
|
+
} else if (r.type === 'user_intent' && r.payload) {
|
|
54
|
+
substrateParts.push(String(r.payload.text || ''));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const substrate = substrateParts.join(' ');
|
|
59
|
+
|
|
60
|
+
if (!substrate.trim()) {
|
|
61
|
+
return { verdict: 'PENDING', detail: 'no substrate available', scalar: 0.5 };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const substrateTokens = new Set(tokenize(substrate));
|
|
65
|
+
let present = 0;
|
|
66
|
+
const claimSet = new Set(claimTokens);
|
|
67
|
+
for (const t of claimSet) if (substrateTokens.has(t)) present++;
|
|
68
|
+
const scalar = present / claimSet.size;
|
|
69
|
+
|
|
70
|
+
// v1.1 policy: g_fidelity never FAILs on token overlap alone, because interactive
|
|
71
|
+
// agent messages frequently contain meta-phrasing ("let me write the file",
|
|
72
|
+
// "I'll check the path") that has low overlap with any concrete substrate. A
|
|
73
|
+
// FAIL here would over-refute legitimate commitments. We emit PASS above the
|
|
74
|
+
// strong threshold and PENDING otherwise, reserving FAIL for future v1.2
|
|
75
|
+
// typed-claim-graph checks (where claim triples can be structurally refuted).
|
|
76
|
+
if (scalar >= RECALL_STRONG) {
|
|
77
|
+
return { verdict: 'PASS', detail: `g_fidelity ${scalar.toFixed(2)} >= ${RECALL_STRONG}`, scalar };
|
|
78
|
+
}
|
|
79
|
+
return { verdict: 'PENDING', detail: `g_fidelity ${scalar.toFixed(2)} < ${RECALL_STRONG}`, scalar };
|
|
80
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// lib/witness/ground-truth.mjs
|
|
2
|
+
// Structurally non-LLM ground-truth check. Returns {verdict, detail}.
|
|
3
|
+
// For file-based tools, verify the file actually exists after the operation.
|
|
4
|
+
// For bash, check the exit code. Pure async I/O, no model calls.
|
|
5
|
+
|
|
6
|
+
import { access } from 'node:fs/promises';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
|
|
9
|
+
export async function groundTruth({ toolName, toolArgs, toolResult, cwd }) {
|
|
10
|
+
if (!toolName) return { verdict: 'PENDING', detail: 'no tool call to ground-check' };
|
|
11
|
+
|
|
12
|
+
if (toolName === 'read_file') {
|
|
13
|
+
const path = toolArgs?.path;
|
|
14
|
+
if (!path) return { verdict: 'FAIL', detail: 'read_file called without path' };
|
|
15
|
+
if (typeof toolResult === 'string' && toolResult.startsWith('error:')) {
|
|
16
|
+
return { verdict: 'FAIL', detail: `read_file error: ${toolResult.slice(0, 80)}` };
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
await access(resolve(cwd || process.cwd(), path));
|
|
20
|
+
return { verdict: 'PASS', detail: `file exists: ${path}` };
|
|
21
|
+
} catch {
|
|
22
|
+
return { verdict: 'FAIL', detail: `file not found: ${path}` };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (toolName === 'write_file' || toolName === 'edit_file') {
|
|
27
|
+
const path = toolArgs?.path;
|
|
28
|
+
if (!path) return { verdict: 'FAIL', detail: `${toolName} called without path` };
|
|
29
|
+
if (typeof toolResult === 'string' && toolResult.startsWith('error:')) {
|
|
30
|
+
return { verdict: 'FAIL', detail: `${toolName} error: ${toolResult.slice(0, 80)}` };
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await access(resolve(cwd || process.cwd(), path));
|
|
34
|
+
return { verdict: 'PASS', detail: `wrote file exists: ${path}` };
|
|
35
|
+
} catch {
|
|
36
|
+
return { verdict: 'FAIL', detail: `${toolName} did not produce file: ${path}` };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (toolName === 'bash') {
|
|
41
|
+
if (typeof toolResult === 'string' && toolResult.startsWith('exit=')) {
|
|
42
|
+
return { verdict: 'FAIL', detail: 'bash command exited non-zero' };
|
|
43
|
+
}
|
|
44
|
+
return { verdict: 'PASS', detail: 'bash returned output' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (toolName === 'glob' || toolName === 'grep') {
|
|
48
|
+
return { verdict: 'PASS', detail: `${toolName} returned result` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (toolName === 'task_done') {
|
|
52
|
+
return { verdict: 'PASS', detail: 'task_done summary accepted' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { verdict: 'PENDING', detail: `no ground-truth check for ${toolName}` };
|
|
56
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// lib/witness/index.mjs
|
|
2
|
+
// Verification layer. Every tool call and final message runs through a small
|
|
3
|
+
// set of pure-Node checks before being recorded to the session log. The checks
|
|
4
|
+
// are deterministic and have no network dependencies of their own. If any
|
|
5
|
+
// check fails, the record is marked REFUTED and the agent is told to try a
|
|
6
|
+
// different approach. If none fail, the record is marked VERIFIED.
|
|
7
|
+
//
|
|
8
|
+
// Checks in this release:
|
|
9
|
+
// conservativity token overlap and numeric consistency vs the tool output
|
|
10
|
+
// ground_truth file existence, URL resolution, tool exit code
|
|
11
|
+
// rule_compliance forbidden libraries, commands, and patterns
|
|
12
|
+
// g_fidelity generator output grounding vs the compiled context
|
|
13
|
+
|
|
14
|
+
import { conservativity } from './conservativity.mjs';
|
|
15
|
+
import { groundTruth } from './ground-truth.mjs';
|
|
16
|
+
import { ruleCompliance } from './rule-compliance.mjs';
|
|
17
|
+
import { gFidelity } from './g-fidelity.mjs';
|
|
18
|
+
|
|
19
|
+
export async function witness({
|
|
20
|
+
claim,
|
|
21
|
+
substrate,
|
|
22
|
+
toolName,
|
|
23
|
+
toolArgs,
|
|
24
|
+
toolResult,
|
|
25
|
+
cwd,
|
|
26
|
+
packet,
|
|
27
|
+
canon,
|
|
28
|
+
intentId,
|
|
29
|
+
rules,
|
|
30
|
+
}) {
|
|
31
|
+
const primitives = {};
|
|
32
|
+
|
|
33
|
+
primitives.conservativity = conservativity({
|
|
34
|
+
claim,
|
|
35
|
+
substrate: substrate || (typeof toolResult === 'string' ? toolResult : ''),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
primitives.ground_truth = await groundTruth({ toolName, toolArgs, toolResult, cwd });
|
|
39
|
+
|
|
40
|
+
primitives.rule_compliance = ruleCompliance({
|
|
41
|
+
claim,
|
|
42
|
+
toolName,
|
|
43
|
+
toolArgs,
|
|
44
|
+
toolResult,
|
|
45
|
+
rules,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
primitives.g_fidelity = gFidelity({
|
|
49
|
+
claim,
|
|
50
|
+
packet,
|
|
51
|
+
canon,
|
|
52
|
+
intentId,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
primitives.falsification = {
|
|
56
|
+
verdict: 'PENDING',
|
|
57
|
+
detail: 'v1.1 stub; Wikipedia full-text port arrives in v1.2',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const verdicts = Object.values(primitives).map((p) => p.verdict);
|
|
61
|
+
const hasFail = verdicts.some((v) => v === 'FAIL');
|
|
62
|
+
const hasPass = verdicts.some((v) => v === 'PASS');
|
|
63
|
+
|
|
64
|
+
let verdict;
|
|
65
|
+
if (hasFail) verdict = 'REFUTED';
|
|
66
|
+
else if (hasPass) verdict = 'VERIFIED';
|
|
67
|
+
else verdict = 'PENDING';
|
|
68
|
+
|
|
69
|
+
return { verdict, primitives };
|
|
70
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// lib/witness/rule-compliance.mjs
|
|
2
|
+
// Rule compliance check. Non-LLM. Scans tool call arguments and agent claims
|
|
3
|
+
// against an explicit rule set loaded from rules/default.yaml or a user-
|
|
4
|
+
// supplied rules file. Returns PASS if nothing matches the forbidden lists,
|
|
5
|
+
// FAIL on the first match, and the FAIL reason identifies which rule fired.
|
|
6
|
+
// The agent is allowed to propose anything; this layer enforces the rules
|
|
7
|
+
// before any commitment appends to the session log.
|
|
8
|
+
|
|
9
|
+
const DEFAULT_FORBIDDEN_COMMANDS = [
|
|
10
|
+
'sudo rm',
|
|
11
|
+
'rm -rf /',
|
|
12
|
+
'rm -rf ~',
|
|
13
|
+
'rm -rf /*',
|
|
14
|
+
':(){:|:&};:',
|
|
15
|
+
'> /dev/sda',
|
|
16
|
+
'dd if=/dev/zero',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function normalize(s) {
|
|
20
|
+
return String(s || '').toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function matchesLibraryImport(text, lib) {
|
|
24
|
+
const t = normalize(text);
|
|
25
|
+
const needles = [
|
|
26
|
+
`import ${lib}`,
|
|
27
|
+
`from ${lib}`,
|
|
28
|
+
`from ${lib} import`,
|
|
29
|
+
`require('${lib}')`,
|
|
30
|
+
`require("${lib}")`,
|
|
31
|
+
`import "${lib}"`,
|
|
32
|
+
`"${lib}":`,
|
|
33
|
+
`'${lib}':`,
|
|
34
|
+
];
|
|
35
|
+
for (const n of needles) if (t.includes(n.toLowerCase())) return true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ruleCompliance({ claim, toolName, toolArgs, toolResult, rules = {} }) {
|
|
40
|
+
const forbiddenLibs = Array.isArray(rules?.forbidden_libraries) ? rules.forbidden_libraries : [];
|
|
41
|
+
const forbiddenCmds = Array.isArray(rules?.forbidden_commands)
|
|
42
|
+
? rules.forbidden_commands
|
|
43
|
+
: DEFAULT_FORBIDDEN_COMMANDS;
|
|
44
|
+
const forbiddenPatterns = Array.isArray(rules?.forbidden_patterns) ? rules.forbidden_patterns : [];
|
|
45
|
+
|
|
46
|
+
const candidateTexts = [
|
|
47
|
+
claim || '',
|
|
48
|
+
toolName === 'bash' ? String(toolArgs?.command || '') : '',
|
|
49
|
+
toolName === 'write_file' ? String(toolArgs?.content || '') : '',
|
|
50
|
+
toolName === 'edit_file' ? String(toolArgs?.new_string || '') : '',
|
|
51
|
+
typeof toolResult === 'string' ? toolResult : '',
|
|
52
|
+
];
|
|
53
|
+
const combined = candidateTexts.join(' ');
|
|
54
|
+
|
|
55
|
+
for (const lib of forbiddenLibs) {
|
|
56
|
+
for (const t of candidateTexts) {
|
|
57
|
+
if (t && matchesLibraryImport(t, lib)) {
|
|
58
|
+
return { verdict: 'FAIL', detail: `forbidden library referenced: ${lib}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const combinedLower = normalize(combined);
|
|
64
|
+
for (const cmd of forbiddenCmds) {
|
|
65
|
+
if (combinedLower.includes(normalize(cmd))) {
|
|
66
|
+
return { verdict: 'FAIL', detail: `forbidden command referenced: ${cmd}` };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const pattern of forbiddenPatterns) {
|
|
71
|
+
try {
|
|
72
|
+
const re = new RegExp(pattern, 'i');
|
|
73
|
+
if (re.test(combined)) {
|
|
74
|
+
return { verdict: 'FAIL', detail: `forbidden pattern matched: ${pattern}` };
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// ignore invalid regex in the rules file
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { verdict: 'PASS', detail: 'no rule violations detected' };
|
|
82
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// lib/witness/secret-scrubber.mjs
|
|
2
|
+
// Secret scrubber. Runs on the bounded turn context before any network call
|
|
3
|
+
// to the generator, and again on every tool result before it reaches the
|
|
4
|
+
// terminal or the session log. If a known secret pattern is detected, the
|
|
5
|
+
// scrubber redacts the matching bytes in place and records the match type.
|
|
6
|
+
// When the context scrubber fires pre-dispatch, the turn is aborted and
|
|
7
|
+
// recorded as secret_bleed_blocked with no network call made.
|
|
8
|
+
//
|
|
9
|
+
// Pattern set covers AWS, Anthropic, Groq, OpenAI, GitHub, Google API,
|
|
10
|
+
// Slack, Stripe, JWTs, and PEM private-key blocks.
|
|
11
|
+
|
|
12
|
+
const SECRET_PATTERNS = [
|
|
13
|
+
{ name: 'AWS_ACCESS_KEY', regex: /AKIA[0-9A-Z]{16}/ },
|
|
14
|
+
{ name: 'AWS_SECRET_KEY', regex: /aws_secret_access_key[^A-Za-z0-9]{0,5}[A-Za-z0-9/+=]{30,}/i },
|
|
15
|
+
{ name: 'ANTHROPIC_API_KEY', regex: /sk-ant-api\d{2}-[A-Za-z0-9_\-]{80,}/ },
|
|
16
|
+
{ name: 'GROQ_API_KEY', regex: /gsk_[A-Za-z0-9]{40,}/ },
|
|
17
|
+
{ name: 'OPENAI_API_KEY', regex: /sk-[A-Za-z0-9]{40,}/ },
|
|
18
|
+
{ name: 'OPENAI_PROJECT_KEY', regex: /sk-proj-[A-Za-z0-9_\-]{40,}/ },
|
|
19
|
+
{ name: 'GITHUB_TOKEN', regex: /gh[pousr]_[A-Za-z0-9]{36,}/ },
|
|
20
|
+
{ name: 'GOOGLE_API_KEY', regex: /AIza[0-9A-Za-z_\-]{35}/ },
|
|
21
|
+
{ name: 'SLACK_TOKEN', regex: /xox[baprs]-[A-Za-z0-9\-]{10,}/ },
|
|
22
|
+
{ name: 'STRIPE_KEY', regex: /sk_live_[A-Za-z0-9]{24,}/ },
|
|
23
|
+
{ name: 'PRIVATE_KEY_BLOCK', regex: /-----BEGIN (RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----/ },
|
|
24
|
+
{ name: 'JWT', regex: /eyJ[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}/ },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function scrubSecrets(text) {
|
|
28
|
+
if (!text || typeof text !== 'string') {
|
|
29
|
+
return { findings: [], blocked: false, redacted: text || '' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const findings = [];
|
|
33
|
+
let redacted = text;
|
|
34
|
+
for (const { name, regex } of SECRET_PATTERNS) {
|
|
35
|
+
const globalRegex = new RegExp(regex.source, 'g');
|
|
36
|
+
const matches = text.match(globalRegex);
|
|
37
|
+
if (matches && matches.length > 0) {
|
|
38
|
+
findings.push({ pattern: name, count: matches.length });
|
|
39
|
+
redacted = redacted.replace(globalRegex, `[REDACTED_${name}]`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
findings,
|
|
45
|
+
blocked: findings.length > 0,
|
|
46
|
+
redacted,
|
|
47
|
+
reason: findings.length > 0 ? `secrets detected: ${findings.map((f) => f.pattern).join(', ')}` : null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { SECRET_PATTERNS };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@polylogicai/polycode",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "An agentic coding CLI. Runs on your machine with your keys. Every turn is appended to a SHA-256 chained session log, so your history is auditable, replayable, and portable.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "bin/polycode.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"polycode": "bin/polycode.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"lib",
|
|
13
|
+
"rules",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node bin/polycode.mjs",
|
|
19
|
+
"test": "node test/run-all.mjs"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.0.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@anthropic-ai/sdk": "^0.89.0",
|
|
26
|
+
"dotenv": "^16.4.5",
|
|
27
|
+
"groq-sdk": "^0.15.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"agent",
|
|
31
|
+
"cli",
|
|
32
|
+
"coding",
|
|
33
|
+
"polycode",
|
|
34
|
+
"polylogic",
|
|
35
|
+
"llm",
|
|
36
|
+
"groq",
|
|
37
|
+
"anthropic"
|
|
38
|
+
],
|
|
39
|
+
"author": "Polylogic AI",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"homepage": "https://polylogicai.com/polycode",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# polycode default rules. Copy to ~/.polycode/rules.yaml to customize.
|
|
2
|
+
# Rules are read on startup. Edit this file to change polycode's behavior
|
|
3
|
+
# without touching the source code.
|
|
4
|
+
|
|
5
|
+
voice:
|
|
6
|
+
no_em_dashes: true
|
|
7
|
+
no_hype_words: true
|
|
8
|
+
|
|
9
|
+
witness:
|
|
10
|
+
conservativity_min_overlap: 0.6
|
|
11
|
+
conservativity_numeric_tolerance: 0.02
|
|
12
|
+
g_fidelity_strong: 0.8
|
|
13
|
+
g_fidelity_weak: 0.5
|
|
14
|
+
|
|
15
|
+
context:
|
|
16
|
+
max_prior_turns: 4
|
|
17
|
+
max_prior_commits: 5
|
|
18
|
+
|
|
19
|
+
trust_weights:
|
|
20
|
+
survived: 3
|
|
21
|
+
verified: 1
|
|
22
|
+
refuted: -2
|
|
23
|
+
challenged: -1
|
|
24
|
+
pending: -0.1
|
|
25
|
+
|
|
26
|
+
compiler:
|
|
27
|
+
primary: claude-haiku-4-5-20251001
|
|
28
|
+
fallback: pure-node
|
|
29
|
+
target_packet_tokens: 4000
|
|
30
|
+
summary_row_limit: 400
|
|
31
|
+
|
|
32
|
+
generator:
|
|
33
|
+
primary: moonshotai/kimi-k2-instruct
|
|
34
|
+
fallback: llama-3.3-70b-versatile
|
|
35
|
+
|
|
36
|
+
forbidden_libraries:
|
|
37
|
+
- requests
|
|
38
|
+
|
|
39
|
+
forbidden_commands:
|
|
40
|
+
- sudo rm
|
|
41
|
+
- rm -rf /
|
|
42
|
+
- rm -rf ~
|
|
43
|
+
- rm -rf /*
|
|
44
|
+
- dd if=/dev/zero
|
|
45
|
+
- /etc/passwd
|
|
46
|
+
- /etc/shadow
|
|
47
|
+
- /etc/sudoers
|
|
48
|
+
- /root/
|
|
49
|
+
- .ssh/
|
|
50
|
+
- id_rsa
|
|
51
|
+
- id_ed25519
|
|
52
|
+
- authorized_keys
|
|
53
|
+
- known_hosts
|
|
54
|
+
|
|
55
|
+
forbidden_patterns:
|
|
56
|
+
- 'eval\s*\(\s*(input|request|stdin)'
|
|
57
|
+
- 'exec\s*\(\s*(input|request|stdin)'
|
|
58
|
+
- '\.\./\.\./'
|