@pinta-ai/pinta-opencode 0.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/CHANGELOG.md +17 -0
- package/LICENSE +136 -0
- package/README.md +121 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +49 -0
- package/dist/config.js.map +1 -0
- package/dist/core/guard.d.ts +29 -0
- package/dist/core/guard.js +50 -0
- package/dist/core/guard.js.map +1 -0
- package/dist/core/otlp.d.ts +51 -0
- package/dist/core/otlp.js +135 -0
- package/dist/core/otlp.js.map +1 -0
- package/dist/core/redact.d.ts +57 -0
- package/dist/core/redact.js +141 -0
- package/dist/core/redact.js.map +1 -0
- package/dist/core/retry-queue.d.ts +14 -0
- package/dist/core/retry-queue.js +26 -0
- package/dist/core/retry-queue.js.map +1 -0
- package/dist/core/trace.d.ts +16 -0
- package/dist/core/trace.js +50 -0
- package/dist/core/trace.js.map +1 -0
- package/dist/core/transport.d.ts +20 -0
- package/dist/core/transport.js +69 -0
- package/dist/core/transport.js.map +1 -0
- package/dist/env-file.d.ts +4 -0
- package/dist/env-file.js +53 -0
- package/dist/env-file.js.map +1 -0
- package/dist/plugin.d.ts +27 -0
- package/dist/plugin.js +85 -0
- package/dist/plugin.js.map +1 -0
- package/dist/telemetry.d.ts +37 -0
- package/dist/telemetry.js +53 -0
- package/dist/telemetry.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface RedactOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Optional context hint that enables context-gated patterns. Currently
|
|
4
|
+
* only "bash" enables `cli_password_short` (-p<pass>) since matching it
|
|
5
|
+
* outside Bash command text is too noisy.
|
|
6
|
+
*/
|
|
7
|
+
context?: "bash";
|
|
8
|
+
}
|
|
9
|
+
/** Tier 3 cap: per-string byte limit before truncation. */
|
|
10
|
+
export declare const MAX_BYTES = 102400;
|
|
11
|
+
/**
|
|
12
|
+
* Truncate input by UTF-8 byte length. Appends `…[TRUNCATED:<origByteLen>]`
|
|
13
|
+
* to indicate elision. Returns input unchanged when within cap.
|
|
14
|
+
*
|
|
15
|
+
* Uses Buffer for accurate byte counting; slices on byte boundary then
|
|
16
|
+
* decodes — may produce a U+FFFD if the boundary lands mid-codepoint, which
|
|
17
|
+
* is acceptable for telemetry.
|
|
18
|
+
*/
|
|
19
|
+
export declare function truncate(input: string): string;
|
|
20
|
+
export interface Pattern {
|
|
21
|
+
/** Token type printed inside `[REDACTED:<type>]`. */
|
|
22
|
+
type: string;
|
|
23
|
+
/** Regex with global flag (`g`). Multiline (`m`) for line-anchored ones. */
|
|
24
|
+
regex: RegExp;
|
|
25
|
+
/**
|
|
26
|
+
* Capture group index whose substring is replaced. 0 (default) = whole match.
|
|
27
|
+
* Use a positive group when the pattern brackets context that should be
|
|
28
|
+
* preserved (e.g. `postgres://user:<pwd>@` keeps the URL shape intact).
|
|
29
|
+
*/
|
|
30
|
+
captureGroup?: number;
|
|
31
|
+
/**
|
|
32
|
+
* If set, this pattern only applies when `RedactOptions.context` equals
|
|
33
|
+
* the listed value. Used for false-positive-prone shapes.
|
|
34
|
+
*/
|
|
35
|
+
requireContext?: "bash";
|
|
36
|
+
}
|
|
37
|
+
export declare const PATTERNS: ReadonlyArray<Pattern>;
|
|
38
|
+
interface Match {
|
|
39
|
+
start: number;
|
|
40
|
+
end: number;
|
|
41
|
+
replaceStart: number;
|
|
42
|
+
replaceEnd: number;
|
|
43
|
+
type: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function collectMatches(input: string, opts: RedactOptions): Match[];
|
|
46
|
+
export declare function resolveOverlaps(matches: Match[]): Match[];
|
|
47
|
+
export declare function applyMatches(input: string, matches: Match[]): string;
|
|
48
|
+
/**
|
|
49
|
+
* Mask high-confidence secret patterns in `input`. Returns a new string with
|
|
50
|
+
* matched substrings replaced by `[REDACTED:<type>]`.
|
|
51
|
+
*
|
|
52
|
+
* - `opts.context = "bash"` enables context-gated patterns.
|
|
53
|
+
* - Truncation is NOT applied here — see `truncate()`. The caller decides
|
|
54
|
+
* the order (spec §3 Tier 3: truncate first, then redact).
|
|
55
|
+
*/
|
|
56
|
+
export declare function redact(input: string, opts?: RedactOptions): string;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** Tier 3 cap: per-string byte limit before truncation. */
|
|
2
|
+
export const MAX_BYTES = 102400;
|
|
3
|
+
/**
|
|
4
|
+
* Truncate input by UTF-8 byte length. Appends `…[TRUNCATED:<origByteLen>]`
|
|
5
|
+
* to indicate elision. Returns input unchanged when within cap.
|
|
6
|
+
*
|
|
7
|
+
* Uses Buffer for accurate byte counting; slices on byte boundary then
|
|
8
|
+
* decodes — may produce a U+FFFD if the boundary lands mid-codepoint, which
|
|
9
|
+
* is acceptable for telemetry.
|
|
10
|
+
*/
|
|
11
|
+
export function truncate(input) {
|
|
12
|
+
const buf = Buffer.from(input, "utf-8");
|
|
13
|
+
if (buf.length <= MAX_BYTES)
|
|
14
|
+
return input;
|
|
15
|
+
const head = buf.subarray(0, MAX_BYTES).toString("utf-8");
|
|
16
|
+
return `${head}…[TRUNCATED:${buf.length}]`;
|
|
17
|
+
}
|
|
18
|
+
export const PATTERNS = [
|
|
19
|
+
{ type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
|
|
20
|
+
{
|
|
21
|
+
type: "aws_secret_key",
|
|
22
|
+
// Context word `aws_secret`/`AWS_SECRET` (with optional separator) followed
|
|
23
|
+
// by an assignment-ish character then a 40-char base64-ish blob.
|
|
24
|
+
regex: /(?:aws[_-]?secret(?:[_-]?(?:access)?[_-]?key)?)\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})(?![A-Za-z0-9/+=])/gi,
|
|
25
|
+
captureGroup: 1,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: "gcp_service_account",
|
|
29
|
+
// Whole JSON blob starting with the service-account discriminator.
|
|
30
|
+
regex: /\{[\s\S]{0,200}?"type"\s*:\s*"service_account"[\s\S]*?\}/g,
|
|
31
|
+
},
|
|
32
|
+
{ type: "github_token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
|
|
33
|
+
{ type: "gitlab_token", regex: /glpat-[A-Za-z0-9_-]{20}/g },
|
|
34
|
+
{ type: "slack_token", regex: /xox[abrsp]-[0-9A-Za-z-]{10,}/g },
|
|
35
|
+
{ type: "openai_key", regex: /sk-(?:proj-)?[A-Za-z0-9_-]{40,}/g },
|
|
36
|
+
{ type: "anthropic_key", regex: /sk-ant-[A-Za-z0-9_-]{50,}/g },
|
|
37
|
+
{ type: "stripe_key", regex: /(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{20,}/g },
|
|
38
|
+
{
|
|
39
|
+
type: "jwt",
|
|
40
|
+
regex: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: "private_key_block",
|
|
44
|
+
regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
45
|
+
},
|
|
46
|
+
{ type: "bearer_token", regex: /bearer\s+([A-Za-z0-9._~+/=-]{12,})/gi, captureGroup: 1 },
|
|
47
|
+
{ type: "basic_auth", regex: /basic\s+([A-Za-z0-9+/=]{12,})/gi, captureGroup: 1 },
|
|
48
|
+
{
|
|
49
|
+
type: "db_url_password",
|
|
50
|
+
regex: /\b(?:postgres|postgresql|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/]+:([^@\s]+)@/gi,
|
|
51
|
+
captureGroup: 1,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: "cli_password_flag",
|
|
55
|
+
regex: /(?:--password|--pass|--pwd)[=\s]([^\s'"]+)/g,
|
|
56
|
+
captureGroup: 1,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: "cli_password_short",
|
|
60
|
+
// mysql -p<pass>; only on bash context.
|
|
61
|
+
regex: /\s-p([^\s'"]+)/g,
|
|
62
|
+
captureGroup: 1,
|
|
63
|
+
requireContext: "bash",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: "env_var_secret",
|
|
67
|
+
// Known false positive: trailing `[A-Z0-9_]*` is greedy, so names like
|
|
68
|
+
// `OPENAI_API_KEY_DESCRIPTION=Used` still match. Acceptable for Bronze.
|
|
69
|
+
regex: /^(?:export\s+)?([A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD|API_KEY)[A-Z0-9_]*)\s*=\s*["']?([^\s"'\n]+)/gm,
|
|
70
|
+
captureGroup: 2,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
export function collectMatches(input, opts) {
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const pattern of PATTERNS) {
|
|
76
|
+
if (pattern.requireContext && pattern.requireContext !== opts.context)
|
|
77
|
+
continue;
|
|
78
|
+
const re = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
79
|
+
let m;
|
|
80
|
+
while ((m = re.exec(input)) !== null) {
|
|
81
|
+
const cg = pattern.captureGroup ?? 0;
|
|
82
|
+
const captured = m[cg];
|
|
83
|
+
if (captured === undefined) {
|
|
84
|
+
if (m.index === re.lastIndex)
|
|
85
|
+
re.lastIndex++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const start = m.index;
|
|
89
|
+
const end = m.index + m[0].length;
|
|
90
|
+
const replaceStart = start + m[0].indexOf(captured);
|
|
91
|
+
const replaceEnd = replaceStart + captured.length;
|
|
92
|
+
out.push({ start, end, replaceStart, replaceEnd, type: pattern.type });
|
|
93
|
+
if (m.index === re.lastIndex)
|
|
94
|
+
re.lastIndex++; // guard against zero-length
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
export function resolveOverlaps(matches) {
|
|
100
|
+
const sorted = [...matches].sort((a, b) => {
|
|
101
|
+
if (a.start !== b.start)
|
|
102
|
+
return a.start - b.start;
|
|
103
|
+
return (b.end - b.start) - (a.end - a.start); // longer whole-match wins on tie
|
|
104
|
+
});
|
|
105
|
+
const kept = [];
|
|
106
|
+
let lastEnd = -1;
|
|
107
|
+
for (const m of sorted) {
|
|
108
|
+
if (m.start < lastEnd)
|
|
109
|
+
continue; // overlapping later match dropped
|
|
110
|
+
kept.push(m);
|
|
111
|
+
lastEnd = m.end;
|
|
112
|
+
}
|
|
113
|
+
return kept;
|
|
114
|
+
}
|
|
115
|
+
export function applyMatches(input, matches) {
|
|
116
|
+
// Apply right-to-left so earlier indices remain valid.
|
|
117
|
+
const sorted = [...matches].sort((a, b) => b.replaceStart - a.replaceStart);
|
|
118
|
+
let out = input;
|
|
119
|
+
for (const m of sorted) {
|
|
120
|
+
out = out.slice(0, m.replaceStart) + `[REDACTED:${m.type}]` + out.slice(m.replaceEnd);
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Mask high-confidence secret patterns in `input`. Returns a new string with
|
|
126
|
+
* matched substrings replaced by `[REDACTED:<type>]`.
|
|
127
|
+
*
|
|
128
|
+
* - `opts.context = "bash"` enables context-gated patterns.
|
|
129
|
+
* - Truncation is NOT applied here — see `truncate()`. The caller decides
|
|
130
|
+
* the order (spec §3 Tier 3: truncate first, then redact).
|
|
131
|
+
*/
|
|
132
|
+
export function redact(input, opts = {}) {
|
|
133
|
+
if (input.length === 0)
|
|
134
|
+
return input;
|
|
135
|
+
const all = collectMatches(input, opts);
|
|
136
|
+
if (all.length === 0)
|
|
137
|
+
return input;
|
|
138
|
+
const kept = resolveOverlaps(all);
|
|
139
|
+
return applyMatches(input, kept);
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/core/redact.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OtlpPayload } from "./otlp.js";
|
|
2
|
+
/**
|
|
3
|
+
* In-memory retry buffer. The plugin is a long-lived in-process module
|
|
4
|
+
* (verified H-C1), so failed OTLP payloads are buffered in memory and flushed
|
|
5
|
+
* on the next event. Oldest entries are dropped past the cap. (Disk persistence
|
|
6
|
+
* across instance restarts is an optional future enhancement — SPEC §6.)
|
|
7
|
+
*/
|
|
8
|
+
export declare class RetryQueue {
|
|
9
|
+
private entries;
|
|
10
|
+
enqueue(payload: OtlpPayload): void;
|
|
11
|
+
/** Remove and return all buffered payloads. */
|
|
12
|
+
drain(): OtlpPayload[];
|
|
13
|
+
get size(): number;
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const MAX_ENTRIES = 1000;
|
|
2
|
+
/**
|
|
3
|
+
* In-memory retry buffer. The plugin is a long-lived in-process module
|
|
4
|
+
* (verified H-C1), so failed OTLP payloads are buffered in memory and flushed
|
|
5
|
+
* on the next event. Oldest entries are dropped past the cap. (Disk persistence
|
|
6
|
+
* across instance restarts is an optional future enhancement — SPEC §6.)
|
|
7
|
+
*/
|
|
8
|
+
export class RetryQueue {
|
|
9
|
+
entries = [];
|
|
10
|
+
enqueue(payload) {
|
|
11
|
+
this.entries.push(payload);
|
|
12
|
+
if (this.entries.length > MAX_ENTRIES) {
|
|
13
|
+
this.entries.splice(0, this.entries.length - MAX_ENTRIES);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Remove and return all buffered payloads. */
|
|
17
|
+
drain() {
|
|
18
|
+
const out = this.entries;
|
|
19
|
+
this.entries = [];
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
get size() {
|
|
23
|
+
return this.entries.length;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=retry-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-queue.js","sourceRoot":"","sources":["../../src/core/retry-queue.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,GAAG,IAAI,CAAC;AAEzB;;;;;GAKG;AACH,MAAM,OAAO,UAAU;IACb,OAAO,GAAkB,EAAE,CAAC;IAEpC,OAAO,CAAC,OAAoB;QAC1B,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3B,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,WAAW,EAAE,CAAC;YACtC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IAED,+CAA+C;IAC/C,KAAK;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAClB,OAAO,GAAG,CAAC;IACb,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;IAC7B,CAAC;CACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-turn trace correlation, keyed by `sessionID`.
|
|
3
|
+
*
|
|
4
|
+
* opencode instantiates the plugin once per instance (verified H-C1), so the
|
|
5
|
+
* map lives in memory for the instance lifetime — no file persistence needed.
|
|
6
|
+
* `chat.message` (turn-START) rotates a new ULID trace for its session; every
|
|
7
|
+
* subsequent hook in the turn reuses it. Keyed by sessionID so concurrent
|
|
8
|
+
* sessions don't collide.
|
|
9
|
+
*/
|
|
10
|
+
export declare class TraceManager {
|
|
11
|
+
private map;
|
|
12
|
+
/** Start a new trace for this session (called on chat.message / turn-START). */
|
|
13
|
+
newTrace(sessionId?: string): string;
|
|
14
|
+
/** Current trace for this session; create one if none exists yet. */
|
|
15
|
+
currentTrace(sessionId?: string): string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
3
|
+
function generateUlid() {
|
|
4
|
+
const now = Date.now();
|
|
5
|
+
let ts = "";
|
|
6
|
+
let t = now;
|
|
7
|
+
for (let i = 0; i < 10; i++) {
|
|
8
|
+
ts = CROCKFORD[t & 31] + ts;
|
|
9
|
+
t = Math.floor(t / 32);
|
|
10
|
+
}
|
|
11
|
+
const rand = crypto.randomBytes(10);
|
|
12
|
+
let r = "";
|
|
13
|
+
for (let i = 0; i < 10; i++)
|
|
14
|
+
r += CROCKFORD[rand[i] & 31];
|
|
15
|
+
while (r.length < 16)
|
|
16
|
+
r += CROCKFORD[0];
|
|
17
|
+
return ts + r;
|
|
18
|
+
}
|
|
19
|
+
const MAX_SESSIONS = 200;
|
|
20
|
+
/**
|
|
21
|
+
* Per-turn trace correlation, keyed by `sessionID`.
|
|
22
|
+
*
|
|
23
|
+
* opencode instantiates the plugin once per instance (verified H-C1), so the
|
|
24
|
+
* map lives in memory for the instance lifetime — no file persistence needed.
|
|
25
|
+
* `chat.message` (turn-START) rotates a new ULID trace for its session; every
|
|
26
|
+
* subsequent hook in the turn reuses it. Keyed by sessionID so concurrent
|
|
27
|
+
* sessions don't collide.
|
|
28
|
+
*/
|
|
29
|
+
export class TraceManager {
|
|
30
|
+
map = new Map();
|
|
31
|
+
/** Start a new trace for this session (called on chat.message / turn-START). */
|
|
32
|
+
newTrace(sessionId) {
|
|
33
|
+
const key = sessionId || "default";
|
|
34
|
+
const traceId = generateUlid();
|
|
35
|
+
this.map.set(key, traceId);
|
|
36
|
+
// Cap to avoid unbounded growth over a long-lived instance.
|
|
37
|
+
if (this.map.size > MAX_SESSIONS) {
|
|
38
|
+
const oldest = this.map.keys().next().value;
|
|
39
|
+
if (oldest !== undefined)
|
|
40
|
+
this.map.delete(oldest);
|
|
41
|
+
}
|
|
42
|
+
return traceId;
|
|
43
|
+
}
|
|
44
|
+
/** Current trace for this session; create one if none exists yet. */
|
|
45
|
+
currentTrace(sessionId) {
|
|
46
|
+
const key = sessionId || "default";
|
|
47
|
+
return this.map.get(key) ?? this.newTrace(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=trace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.js","sourceRoot":"","sources":["../../src/core/trace.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,MAAM,SAAS,GAAG,kCAAkC,CAAC;AAErD,SAAS,YAAY;IACnB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,IAAI,CAAC,GAAG,GAAG,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,EAAE,GAAG,SAAS,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;QAC5B,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IACzB,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACpC,IAAI,CAAC,GAAG,EAAE,CAAC;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE;QAAE,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IAC1D,OAAO,CAAC,CAAC,MAAM,GAAG,EAAE;QAAE,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC;IACxC,OAAO,EAAE,GAAG,CAAC,CAAC;AAChB,CAAC;AAED,MAAM,YAAY,GAAG,GAAG,CAAC;AAEzB;;;;;;;;GAQG;AACH,MAAM,OAAO,YAAY;IACf,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IAExC,gFAAgF;IAChF,QAAQ,CAAC,SAAkB;QACzB,MAAM,GAAG,GAAG,SAAS,IAAI,SAAS,CAAC;QACnC,MAAM,OAAO,GAAG,YAAY,EAAE,CAAC;QAC/B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC3B,4DAA4D;QAC5D,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,YAAY,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAC5C,IAAI,MAAM,KAAK,SAAS;gBAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,qEAAqE;IACrE,YAAY,CAAC,SAAkB;QAC7B,MAAM,GAAG,GAAG,SAAS,IAAI,SAAS,CAAC;QACnC,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IACjD,CAAC;CACF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type OtlpPayload } from "./otlp.js";
|
|
2
|
+
export interface TransportConfig {
|
|
3
|
+
/** Full OTLP/HTTP traces URL. Undefined → telemetry silently disabled. */
|
|
4
|
+
endpoint?: string;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Best-effort OTLP/HTTP transport with an in-memory retry buffer. Never throws;
|
|
9
|
+
* failures are buffered and re-sent (batched) on the next event. Silent-disable
|
|
10
|
+
* when no endpoint is configured.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Transport {
|
|
13
|
+
private config;
|
|
14
|
+
private queue;
|
|
15
|
+
constructor(config: TransportConfig);
|
|
16
|
+
send(payload: OtlpPayload): Promise<void>;
|
|
17
|
+
/** Drain the retry buffer as one batched POST; re-buffer on failure. */
|
|
18
|
+
flush(): Promise<void>;
|
|
19
|
+
private post;
|
|
20
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { RetryQueue } from "./retry-queue.js";
|
|
2
|
+
import { mergeBatch } from "./otlp.js";
|
|
3
|
+
const TIMEOUT_MS = 5000;
|
|
4
|
+
/**
|
|
5
|
+
* Best-effort OTLP/HTTP transport with an in-memory retry buffer. Never throws;
|
|
6
|
+
* failures are buffered and re-sent (batched) on the next event. Silent-disable
|
|
7
|
+
* when no endpoint is configured.
|
|
8
|
+
*/
|
|
9
|
+
export class Transport {
|
|
10
|
+
config;
|
|
11
|
+
queue = new RetryQueue();
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
}
|
|
15
|
+
async send(payload) {
|
|
16
|
+
if (!this.config.endpoint)
|
|
17
|
+
return;
|
|
18
|
+
const ok = await this.post(payload);
|
|
19
|
+
if (!ok)
|
|
20
|
+
this.queue.enqueue(payload);
|
|
21
|
+
}
|
|
22
|
+
/** Drain the retry buffer as one batched POST; re-buffer on failure. */
|
|
23
|
+
async flush() {
|
|
24
|
+
if (!this.config.endpoint)
|
|
25
|
+
return;
|
|
26
|
+
if (this.queue.size === 0)
|
|
27
|
+
return;
|
|
28
|
+
const entries = this.queue.drain();
|
|
29
|
+
const ok = await this.post(mergeBatch(entries));
|
|
30
|
+
if (!ok)
|
|
31
|
+
for (const e of entries)
|
|
32
|
+
this.queue.enqueue(e);
|
|
33
|
+
}
|
|
34
|
+
async post(payload) {
|
|
35
|
+
const endpoint = this.config.endpoint;
|
|
36
|
+
if (!endpoint)
|
|
37
|
+
return false;
|
|
38
|
+
const ctrl = new AbortController();
|
|
39
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(endpoint, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "Content-Type": "application/json", ...this.config.headers },
|
|
44
|
+
body: JSON.stringify(payload),
|
|
45
|
+
signal: ctrl.signal,
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const hint = res.status === 401 || res.status === 403
|
|
49
|
+
? " — check relay token"
|
|
50
|
+
: res.status === 404
|
|
51
|
+
? " — check traces endpoint path"
|
|
52
|
+
: res.status >= 500
|
|
53
|
+
? " — collector may be down"
|
|
54
|
+
: "";
|
|
55
|
+
process.stderr.write(`[pinta-opencode] OTLP POST ${res.status} ${endpoint}${hint}\n`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
process.stderr.write(`[pinta-opencode] OTLP POST failed: ${err.message ?? String(err)}\n`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/core/transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function envFilePath(): string;
|
|
2
|
+
export declare function parseEnvFile(content: string): Record<string, string>;
|
|
3
|
+
/** Load the env file (if present) and merge only-unset keys into process.env. */
|
|
4
|
+
export declare function loadEnvFile(filePath?: string): void;
|
package/dist/env-file.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graceful env-file loader.
|
|
3
|
+
*
|
|
4
|
+
* pinta-opencode can read config from `~/.config/opencode/pinta-opencode.env`
|
|
5
|
+
* (or `$OPENCODE_CONFIG_DIR/pinta-opencode.env`) — `KEY=VALUE` per line. This is
|
|
6
|
+
* the lowest-priority source; plugin options and explicit process.env win.
|
|
7
|
+
*
|
|
8
|
+
* Resolution precedence (highest → lowest): plugin options → process.env →
|
|
9
|
+
* this file (unset keys only). Missing file is a silent no-op.
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
function opencodeConfigDir() {
|
|
15
|
+
return process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), ".config", "opencode");
|
|
16
|
+
}
|
|
17
|
+
export function envFilePath() {
|
|
18
|
+
return path.join(opencodeConfigDir(), "pinta-opencode.env");
|
|
19
|
+
}
|
|
20
|
+
export function parseEnvFile(content) {
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const raw of content.split("\n")) {
|
|
23
|
+
const line = raw.trim();
|
|
24
|
+
if (!line || line.startsWith("#"))
|
|
25
|
+
continue;
|
|
26
|
+
const idx = line.indexOf("=");
|
|
27
|
+
if (idx < 0)
|
|
28
|
+
continue;
|
|
29
|
+
const key = line.slice(0, idx).trim();
|
|
30
|
+
let value = line.slice(idx + 1).trim();
|
|
31
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
32
|
+
value = value.slice(1, -1);
|
|
33
|
+
}
|
|
34
|
+
if (key)
|
|
35
|
+
out[key] = value;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** Load the env file (if present) and merge only-unset keys into process.env. */
|
|
40
|
+
export function loadEnvFile(filePath = envFilePath()) {
|
|
41
|
+
let content;
|
|
42
|
+
try {
|
|
43
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return; // missing/unreadable → no-op
|
|
47
|
+
}
|
|
48
|
+
for (const [key, value] of Object.entries(parseEnvFile(content))) {
|
|
49
|
+
if (process.env[key] === undefined)
|
|
50
|
+
process.env[key] = value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=env-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-file.js","sourceRoot":"","sources":["../src/env-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,SAAS,iBAAiB;IACxB,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;AAC3F,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,oBAAoB,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,GAAG,GAAG,CAAC;YAAE,SAAS;QACtB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACtC,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACvC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACrG,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7B,CAAC;QACD,IAAI,GAAG;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IAC5B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,WAAW,CAAC,WAAmB,WAAW,EAAE;IAC1D,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,6BAA6B;IACvC,CAAC;IACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;QACjE,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS;YAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IAC/D,CAAC;AACH,CAAC"}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type PintaOptions } from "./config.js";
|
|
2
|
+
import { type OpencodeEvent, type ToolBeforeInput, type ToolAfterOutput } from "./telemetry.js";
|
|
3
|
+
/**
|
|
4
|
+
* pinta-opencode plugin entry. opencode invokes this once per instance with
|
|
5
|
+
* `(input, options)` and keeps the returned hooks for the instance lifetime
|
|
6
|
+
* (verified H-C1), so config + transport + trace state live in this closure.
|
|
7
|
+
*
|
|
8
|
+
* - Telemetry (event / tool.execute.after): best-effort OTLP, never blocks.
|
|
9
|
+
* - Governance (tool.execute.before): guard query; DENY → throw(reason), which
|
|
10
|
+
* blocks only that tool and surfaces the reason to the model (verified H-A1).
|
|
11
|
+
*
|
|
12
|
+
* Fail-open invariant: every telemetry/guard error is swallowed; the only
|
|
13
|
+
* intentional throw is a guard DENY.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PintaOpencode: (_input: unknown, options?: PintaOptions) => Promise<{
|
|
16
|
+
"chat.message": (input: {
|
|
17
|
+
sessionID?: string;
|
|
18
|
+
}) => Promise<void>;
|
|
19
|
+
event: (input: {
|
|
20
|
+
event?: OpencodeEvent;
|
|
21
|
+
}) => Promise<void>;
|
|
22
|
+
"tool.execute.before": (input: ToolBeforeInput, output: {
|
|
23
|
+
args: unknown;
|
|
24
|
+
}) => Promise<void>;
|
|
25
|
+
"tool.execute.after": (input: ToolBeforeInput, output: ToolAfterOutput) => Promise<void>;
|
|
26
|
+
}>;
|
|
27
|
+
export default PintaOpencode;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolveConfig } from "./config.js";
|
|
2
|
+
import { Transport } from "./core/transport.js";
|
|
3
|
+
import { TraceManager } from "./core/trace.js";
|
|
4
|
+
import { evaluateGuard } from "./core/guard.js";
|
|
5
|
+
import { Telemetry } from "./telemetry.js";
|
|
6
|
+
function warn(scope, err) {
|
|
7
|
+
process.stderr.write(`[pinta-opencode] ${scope}: ${err?.message ?? String(err)}\n`);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* pinta-opencode plugin entry. opencode invokes this once per instance with
|
|
11
|
+
* `(input, options)` and keeps the returned hooks for the instance lifetime
|
|
12
|
+
* (verified H-C1), so config + transport + trace state live in this closure.
|
|
13
|
+
*
|
|
14
|
+
* - Telemetry (event / tool.execute.after): best-effort OTLP, never blocks.
|
|
15
|
+
* - Governance (tool.execute.before): guard query; DENY → throw(reason), which
|
|
16
|
+
* blocks only that tool and surfaces the reason to the model (verified H-A1).
|
|
17
|
+
*
|
|
18
|
+
* Fail-open invariant: every telemetry/guard error is swallowed; the only
|
|
19
|
+
* intentional throw is a guard DENY.
|
|
20
|
+
*/
|
|
21
|
+
export const PintaOpencode = async (_input, options) => {
|
|
22
|
+
const config = resolveConfig(options ?? {});
|
|
23
|
+
const transport = new Transport({ endpoint: config.endpoint, headers: config.headers });
|
|
24
|
+
const trace = new TraceManager();
|
|
25
|
+
const telemetry = new Telemetry(transport, trace, config);
|
|
26
|
+
return {
|
|
27
|
+
// turn-START → rotate a new trace for this session.
|
|
28
|
+
"chat.message": async (input) => {
|
|
29
|
+
try {
|
|
30
|
+
trace.newTrace(input?.sessionID);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
warn("chat.message", err);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
// lifecycle telemetry; flushes the retry buffer on session.idle (turn-END).
|
|
37
|
+
event: async (input) => {
|
|
38
|
+
try {
|
|
39
|
+
if (input?.event)
|
|
40
|
+
await telemetry.lifecycle(input.event);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
warn("event", err);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
// ★ governance gate: guard query → DENY throws (blocks just this tool).
|
|
47
|
+
"tool.execute.before": async (input, output) => {
|
|
48
|
+
let guard = null;
|
|
49
|
+
try {
|
|
50
|
+
guard = await evaluateGuard({
|
|
51
|
+
spanId: input.sessionID,
|
|
52
|
+
toolName: input.tool,
|
|
53
|
+
toolInput: output?.args,
|
|
54
|
+
rawTextFields: { toolInput: safeStringify(output?.args) },
|
|
55
|
+
}, config.guardEndpoint, { timeoutMs: config.guardTimeoutMs, token: config.relayToken, disabled: config.guardDisabled });
|
|
56
|
+
await telemetry.toolBefore(input, output?.args, guard);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
warn("tool.execute.before", err); // telemetry/guard infra errors are fail-open
|
|
60
|
+
}
|
|
61
|
+
if (guard?.decision === "DENY") {
|
|
62
|
+
throw new Error(guard.userMessage ?? guard.reason ?? "guard_deny");
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
// tool-result telemetry.
|
|
66
|
+
"tool.execute.after": async (input, output) => {
|
|
67
|
+
try {
|
|
68
|
+
await telemetry.toolAfter(input, output);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
warn("tool.execute.after", err);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
function safeStringify(v) {
|
|
77
|
+
try {
|
|
78
|
+
return typeof v === "string" ? v : JSON.stringify(v) ?? "";
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return String(v);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export default PintaOpencode;
|
|
85
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Transport } from "./core/transport.js";
|
|
2
|
+
import { TraceManager } from "./core/trace.js";
|
|
3
|
+
import type { GuardResult } from "./core/guard.js";
|
|
4
|
+
import type { ResolvedConfig } from "./config.js";
|
|
5
|
+
export interface OpencodeEvent {
|
|
6
|
+
id?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
properties?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface ToolBeforeInput {
|
|
11
|
+
tool: string;
|
|
12
|
+
sessionID: string;
|
|
13
|
+
callID: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ToolAfterOutput {
|
|
16
|
+
title?: string;
|
|
17
|
+
output?: unknown;
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Maps opencode hook payloads to OTLP spans (verified payload shapes — SPEC §5).
|
|
22
|
+
* Tool spans are built from tool.execute.before/after (richer: args, output,
|
|
23
|
+
* exit) rather than the event bus; `event` covers lifecycle + turn boundaries.
|
|
24
|
+
*/
|
|
25
|
+
export declare class Telemetry {
|
|
26
|
+
private transport;
|
|
27
|
+
private trace;
|
|
28
|
+
private config;
|
|
29
|
+
constructor(transport: Transport, trace: TraceManager, config: ResolvedConfig);
|
|
30
|
+
private emit;
|
|
31
|
+
/** Lifecycle span from the `event` hook. Flushes the retry buffer on turn-END. */
|
|
32
|
+
lifecycle(ev: OpencodeEvent): Promise<void>;
|
|
33
|
+
/** Tool span from `tool.execute.before`, carrying the guard decision. */
|
|
34
|
+
toolBefore(input: ToolBeforeInput, args: unknown, guard: GuardResult | null): Promise<void>;
|
|
35
|
+
/** Tool result span from `tool.execute.after`, incl. exit code / truncation. */
|
|
36
|
+
toolAfter(input: ToolBeforeInput, output: ToolAfterOutput): Promise<void>;
|
|
37
|
+
}
|