@turns/dev 0.0.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.
- package/README.md +74 -0
- package/dist/agent-anchor-client.js +319 -0
- package/dist/agent-anchor.js +1685 -0
- package/dist/auth.js +258 -0
- package/dist/daemon.js +134 -0
- package/dist/deepseek-trajectories.js +196 -0
- package/dist/directory-policy.js +41 -0
- package/dist/file-browsing.js +241 -0
- package/dist/git-association.js +136 -0
- package/dist/harness-registry.js +323 -0
- package/dist/index.js +300 -0
- package/dist/local-session-sources.js +1448 -0
- package/dist/native-record-compaction.js +84 -0
- package/dist/path-resolution.js +298 -0
- package/dist/question-mcp.js +126 -0
- package/dist/queue-worker.js +1053 -0
- package/dist/secret-redaction.js +181 -0
- package/dist/upload-sessions.js +2016 -0
- package/package.json +35 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-local secret filtering for outbound transcript data.
|
|
3
|
+
*
|
|
4
|
+
* The uploader keeps the original rows for local parsing, prompt matching, and
|
|
5
|
+
* harness operation. Only values about to cross the turns.dev socket are
|
|
6
|
+
* rewritten. This is intentionally a credential filter, not a PII filter:
|
|
7
|
+
* names, email addresses, paths, identifiers, and public keys remain intact.
|
|
8
|
+
*/
|
|
9
|
+
export const REDACTION_VERSION = 1;
|
|
10
|
+
export const REDACTED_SECRET = '[REDACTED_SECRET]';
|
|
11
|
+
const PLACEHOLDER = /^(?:x{8,}|\*{4,}|\.{3,}|<[^>]*>|\[[^\]]*\]|(?:your|my|the|some|example|sample|test|fake|dummy|placeholder|changeme|redacted)[-_a-z0-9]*)$/iu;
|
|
12
|
+
const MIN_CONTEXT_VALUE_LENGTH = 16;
|
|
13
|
+
const MIN_CONTEXT_ENTROPY = 3;
|
|
14
|
+
const STRUCTURAL_RULES = [
|
|
15
|
+
{
|
|
16
|
+
name: 'private_key_block',
|
|
17
|
+
pattern: /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----[\s\S]*?(?:-----END (?:[A-Z ]+ )?PRIVATE KEY-----|$)/gu,
|
|
18
|
+
replacement: () => '[REDACTED_PRIVATE_KEY]',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'url_credentials',
|
|
22
|
+
pattern: /\b([a-z][a-z0-9+.-]*:\/\/)([^\s/:@]+):([^\s/@]+)@/giu,
|
|
23
|
+
replacement: (_match, scheme, user) => `${scheme}${user}:${REDACTED_SECRET}@`,
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
const PREFIX_RULES = [
|
|
27
|
+
['anthropic_key', /\bsk-ant-[A-Za-z0-9_-]{20,}/gu],
|
|
28
|
+
['openai_project_key', /\bsk-proj-[A-Za-z0-9_-]{20,}/gu],
|
|
29
|
+
['openai_key', /\bsk-[A-Za-z0-9_-]{20,}/gu],
|
|
30
|
+
['github_pat', /\bgithub_pat_[A-Za-z0-9_]{20,}/gu],
|
|
31
|
+
['github_token', /\bgh[ouspr]_[A-Za-z0-9]{20,}/gu],
|
|
32
|
+
['npm_token', /\bnpm_[A-Za-z0-9]{30,}/gu],
|
|
33
|
+
['slack_token', /\bxox[baprs]-[A-Za-z0-9-]{10,}/gu],
|
|
34
|
+
['gitlab_pat', /\bglpat-[A-Za-z0-9_-]{16,}/gu],
|
|
35
|
+
['aws_access_key', /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/gu],
|
|
36
|
+
['google_api_key', /\bAIza[A-Za-z0-9_-]{35}\b/gu],
|
|
37
|
+
['google_oauth_token', /\bya29\.[A-Za-z0-9_-]{20,}/gu],
|
|
38
|
+
['huggingface_token', /\bhf_[A-Za-z0-9]{30,}/gu],
|
|
39
|
+
['digitalocean_token', /\bdop_v1_[a-f0-9]{60,}/gu],
|
|
40
|
+
['stripe_key', /\b[rs]k_(?:live|test)_[A-Za-z0-9]{20,}/gu],
|
|
41
|
+
['jwt', /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/gu],
|
|
42
|
+
];
|
|
43
|
+
const NON_SECRET_KEY = new RegExp([
|
|
44
|
+
'(?:^|[._-])(?:pub|public)[._-]?key',
|
|
45
|
+
'site[._-]?key',
|
|
46
|
+
'key[._-]?(?:id|name|file|path|type|size|length|algorithm|alg)\\b',
|
|
47
|
+
'(?:^|[._-])key[._-]?(?:pair|store|ring)',
|
|
48
|
+
'id$',
|
|
49
|
+
'fingerprint|thumbprint|checksum|digest|(?:^|[._-])hash',
|
|
50
|
+
].join('|'), 'iu');
|
|
51
|
+
const CREDENTIAL_KEY = /[A-Za-z0-9_.-]*(?:token|secret|key|password|passwd|pwd|credential|apikey|auth)[A-Za-z0-9_.-]*/iu;
|
|
52
|
+
const CREDENTIAL_WORD = /token|secret|key|password|passwd|pwd|credential|apikey|auth/iu;
|
|
53
|
+
const CONTEXT_RULE = new RegExp(`(["']?)(${CREDENTIAL_KEY.source})\\1(\\s*[:=]\\s*)(["']?)([^\\s"'\`,;)}\\]]{${MIN_CONTEXT_VALUE_LENGTH},})\\4`, 'giu');
|
|
54
|
+
const OPAQUE_VALUE = /^[A-Za-z0-9_\-+/.~]+={0,2}$/u;
|
|
55
|
+
const DOTTED_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)+$/u;
|
|
56
|
+
export function shannonEntropy(value) {
|
|
57
|
+
if (!value)
|
|
58
|
+
return 0;
|
|
59
|
+
const counts = new Map();
|
|
60
|
+
for (const character of value)
|
|
61
|
+
counts.set(character, (counts.get(character) || 0) + 1);
|
|
62
|
+
let entropy = 0;
|
|
63
|
+
for (const count of counts.values()) {
|
|
64
|
+
const probability = count / value.length;
|
|
65
|
+
entropy -= probability * Math.log2(probability);
|
|
66
|
+
}
|
|
67
|
+
return entropy;
|
|
68
|
+
}
|
|
69
|
+
function looksLikeIdentifier(value) {
|
|
70
|
+
const segments = value.split(/[_\-.]+|(?<=[a-z0-9])(?=[A-Z])/u).filter(Boolean);
|
|
71
|
+
return segments.length > 0 && segments.every((segment) => /^[A-Za-z]{2,}$/u.test(segment));
|
|
72
|
+
}
|
|
73
|
+
export function looksLikeSecretValue(value) {
|
|
74
|
+
if (typeof value !== 'string' || value.length < MIN_CONTEXT_VALUE_LENGTH)
|
|
75
|
+
return false;
|
|
76
|
+
if (PLACEHOLDER.test(value) || value.startsWith('[REDACTED'))
|
|
77
|
+
return false;
|
|
78
|
+
if (!OPAQUE_VALUE.test(value) || DOTTED_IDENTIFIER.test(value) || looksLikeIdentifier(value))
|
|
79
|
+
return false;
|
|
80
|
+
if (/^(?:[~.]{0,2}\/|[A-Za-z]:[\\/])/u.test(value))
|
|
81
|
+
return false;
|
|
82
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(value))
|
|
83
|
+
return false;
|
|
84
|
+
return shannonEntropy(value) >= MIN_CONTEXT_ENTROPY;
|
|
85
|
+
}
|
|
86
|
+
function escapeRegExp(value) {
|
|
87
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
88
|
+
}
|
|
89
|
+
/** Build a reusable, idempotent secret redactor. */
|
|
90
|
+
export function createSecretRedactor({ literals = [], onFinding = null, detectOnly = false } = {}) {
|
|
91
|
+
const knownLiterals = new Set();
|
|
92
|
+
let exactRules = [];
|
|
93
|
+
const addLiterals = (values) => {
|
|
94
|
+
for (const value of values || []) {
|
|
95
|
+
const literal = String(value || '');
|
|
96
|
+
if (literal.length >= 8)
|
|
97
|
+
knownLiterals.add(literal);
|
|
98
|
+
}
|
|
99
|
+
exactRules = [...knownLiterals]
|
|
100
|
+
.sort((left, right) => right.length - left.length)
|
|
101
|
+
.map((value, index) => [`known_literal_${index}`, new RegExp(escapeRegExp(value), 'gu')]);
|
|
102
|
+
};
|
|
103
|
+
addLiterals(literals);
|
|
104
|
+
const report = (rule, count) => {
|
|
105
|
+
if (count && typeof onFinding === 'function')
|
|
106
|
+
onFinding({ rule, count });
|
|
107
|
+
};
|
|
108
|
+
const redactText = (text) => {
|
|
109
|
+
if (typeof text !== 'string' || !text)
|
|
110
|
+
return text;
|
|
111
|
+
let output = text;
|
|
112
|
+
for (const [name, pattern] of exactRules) {
|
|
113
|
+
let count = 0;
|
|
114
|
+
output = output.replace(pattern, () => {
|
|
115
|
+
count += 1;
|
|
116
|
+
return REDACTED_SECRET;
|
|
117
|
+
});
|
|
118
|
+
report(name, count);
|
|
119
|
+
}
|
|
120
|
+
for (const rule of STRUCTURAL_RULES) {
|
|
121
|
+
let count = 0;
|
|
122
|
+
output = output.replace(rule.pattern, (...arguments_) => {
|
|
123
|
+
count += 1;
|
|
124
|
+
return rule.replacement(...arguments_);
|
|
125
|
+
});
|
|
126
|
+
report(rule.name, count);
|
|
127
|
+
}
|
|
128
|
+
for (const [name, pattern] of PREFIX_RULES) {
|
|
129
|
+
let count = 0;
|
|
130
|
+
output = output.replace(pattern, () => {
|
|
131
|
+
count += 1;
|
|
132
|
+
return REDACTED_SECRET;
|
|
133
|
+
});
|
|
134
|
+
report(name, count);
|
|
135
|
+
}
|
|
136
|
+
let contextual = 0;
|
|
137
|
+
// Avoid the contextual expression entirely for large tool output that does
|
|
138
|
+
// not even contain a credential word. Besides being the common case, this
|
|
139
|
+
// keeps filtering linear for multi-megabyte homogeneous stdout frames.
|
|
140
|
+
if (CREDENTIAL_WORD.test(output)) {
|
|
141
|
+
output = output.replace(CONTEXT_RULE, (match, keyQuote, key, separator, valueQuote, value) => {
|
|
142
|
+
if (NON_SECRET_KEY.test(key) || !looksLikeSecretValue(value))
|
|
143
|
+
return match;
|
|
144
|
+
contextual += 1;
|
|
145
|
+
return `${keyQuote}${key}${keyQuote}${separator}${valueQuote}${REDACTED_SECRET}${valueQuote}`;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
report('assigned_credential', contextual);
|
|
149
|
+
return detectOnly ? text : output;
|
|
150
|
+
};
|
|
151
|
+
const redactValue = (value, depth = 0, key = '') => {
|
|
152
|
+
if (depth > 24)
|
|
153
|
+
return value;
|
|
154
|
+
if (typeof value === 'string') {
|
|
155
|
+
if (CREDENTIAL_KEY.test(key) && !NON_SECRET_KEY.test(key) && looksLikeSecretValue(value)) {
|
|
156
|
+
report('assigned_credential', 1);
|
|
157
|
+
return detectOnly ? value : REDACTED_SECRET;
|
|
158
|
+
}
|
|
159
|
+
return redactText(value);
|
|
160
|
+
}
|
|
161
|
+
if (Array.isArray(value))
|
|
162
|
+
return value.map((item) => redactValue(item, depth + 1));
|
|
163
|
+
if (!value || typeof value !== 'object')
|
|
164
|
+
return value;
|
|
165
|
+
return Object.fromEntries(Object.entries(value).map(([childKey, child]) => [childKey, redactValue(child, depth + 1, childKey)]));
|
|
166
|
+
};
|
|
167
|
+
const redactJsonLine = (raw) => {
|
|
168
|
+
if (typeof raw !== 'string' || !raw)
|
|
169
|
+
return raw;
|
|
170
|
+
const trimmed = raw.trimStart();
|
|
171
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
172
|
+
try {
|
|
173
|
+
const redacted = redactValue(JSON.parse(raw));
|
|
174
|
+
return detectOnly ? raw : JSON.stringify(redacted);
|
|
175
|
+
}
|
|
176
|
+
catch { }
|
|
177
|
+
}
|
|
178
|
+
return redactText(raw);
|
|
179
|
+
};
|
|
180
|
+
return { redactText, redactValue, redactJsonLine, addLiterals, detectOnly };
|
|
181
|
+
}
|