mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 10:18:42 +00:00
First real builtin under Phase 04 infrastructure. Registers two DB rows
(user_prompt_submit + pre_tool_use) on startup, runs the embedded JS with
source-tier gate + dispatcher dotted-path allowlist.
- internal/hooks/builtin/pii-redactor.js (ES5.1, goja-compatible):
handle(event) returns updatedInput only when email or E.164-ish phone
actually matched. Redacts rawInput + toolInput.{command,query,content}.
- builtins.yaml: pii-redactor entry (version 1, priority 900, timeout 2s,
on_timeout=allow — missing redaction < blocking conversation).
- Drop _placeholder.js; pii-redactor.js is now the first real //go:embed match.
- Export builtin.Source(name) so handler tests load embedded JS without
going through Load()/cache state.
- Tests:
* loader_test: registry contains pii-redactor after Load().
* script_pii_redactor_test (handler-level, real goja runtime): email
masked in rawInput; +E164 phone masked; toolInput.command redacted,
toolInput.path left alone (not in mutable_fields); no PII → no
mutation; redact(redact(s)) == redact(s).
* dispatcher_test: 3 dotted-path walker cases (toolInput.command only
lets that key through; rawInput-only doesn't let forged toolInput
override original event keys; toolInput wildcard merges all keys).
Bench: ~242µs/op on 1 KiB Apple M4 Max, well under <1ms target.
57 lines
1.7 KiB
JavaScript
57 lines
1.7 KiB
JavaScript
// Builtin: PII redactor (email + phone)
|
|
// Version: 1 — matches builtins.yaml
|
|
// Runs on: user_prompt_submit, pre_tool_use
|
|
// Mutates: rawInput, toolInput.command, toolInput.query, toolInput.content
|
|
//
|
|
// ES5.1 only — goja runtime. No let/const, no arrow functions, no template literals.
|
|
|
|
var EMAIL_RE = /[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}/g;
|
|
// E.164-ish: optional +, leading 1-9, 7-14 more digits; guarded so embedded
|
|
// digits inside long tokens (ids, paths, filenames) are left alone.
|
|
var PHONE_RE = /(^|[^0-9+])(\+?[1-9][0-9]{7,14})(?=[^0-9]|$)/g;
|
|
|
|
function redact(s) {
|
|
if (typeof s !== "string" || s.length === 0) return s;
|
|
var out = s.replace(EMAIL_RE, "[REDACTED_EMAIL]");
|
|
out = out.replace(PHONE_RE, function (_m, lead) {
|
|
return lead + "[REDACTED_PHONE]";
|
|
});
|
|
return out;
|
|
}
|
|
|
|
function handle(event) {
|
|
var changed = false;
|
|
var updated = { toolInput: {} };
|
|
|
|
if (typeof event.rawInput === "string") {
|
|
var r = redact(event.rawInput);
|
|
if (r !== event.rawInput) {
|
|
updated.rawInput = r;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
var fields = ["command", "query", "content"];
|
|
var ti = event.toolInput || {};
|
|
for (var i = 0; i < fields.length; i++) {
|
|
var k = fields[i];
|
|
var v = ti[k];
|
|
if (typeof v === "string") {
|
|
var rv = redact(v);
|
|
if (rv !== v) {
|
|
updated.toolInput[k] = rv;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!changed) {
|
|
return { decision: "allow" };
|
|
}
|
|
var tiKeys = Object.keys(updated.toolInput);
|
|
if (tiKeys.length === 0) {
|
|
delete updated.toolInput;
|
|
}
|
|
return { decision: "allow", updatedInput: updated };
|
|
}
|